initial proyect
This commit is contained in:
commit
509272a643
31 files changed
+7170
No files matched your search
+24
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,75 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>courses-rynext</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3269
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "courses-rynext",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-router": "^7.18.2",
|
||||
"tailwindcss": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^8.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filtLine truncated
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
+194
@@ -0,0 +1,194 @@
|
||||
import { useState } from 'react';
|
||||
import type { Course, CartItem, ViewMode } from './types/course';
|
||||
import { mockCourses } from './data/mockCourses';
|
||||
import { Navbar } from './components/layout/Navbar';
|
||||
import { Footer } from './components/layout/Footer';
|
||||
import { HeroSection } from './components/buyer/HeroSection';
|
||||
import { CourseGrid } from './components/buyer/CourseGrid';
|
||||
import { FeaturesBanner } from './components/buyer/FeaturesBanner';
|
||||
import { CourseModal } from './components/buyer/CourseModal';
|
||||
import { CartDrawer } from './components/buyer/CartDrawer';
|
||||
import { SellerHero } from './components/seller/SellerHero';
|
||||
import { SellerDashboard } from './components/seller/SellerDashboard';
|
||||
import { SellerCalculator } from './components/seller/SellerCalculator';
|
||||
import { SellerCourseList } from './components/seller/SellerCourseList';
|
||||
import { CreateCourseModal } from './components/seller/CreateCourseModal';
|
||||
import { FiCheckCircle} from 'react-icons/fi';
|
||||
|
||||
export function App() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('buyer');
|
||||
const [courses, setCourses] = useState<Course[]>(mockCourses);
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Modals & Drawers state
|
||||
const [selectedCourseForModal, setSelectedCourseForModal] = useState<Course | null>(null);
|
||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
// Toast feedback state
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
const showToast = (msg: string) => {
|
||||
setToastMessage(msg);
|
||||
setTimeout(() => {
|
||||
setToastMessage(null);
|
||||
}, 3500);
|
||||
};
|
||||
|
||||
// Cart operations
|
||||
const handleAddToCart = (course: Course) => {
|
||||
if (!cartItems.some((item) => item.course.id === course.id)) {
|
||||
setCartItems([...cartItems, { course, addedAt: new Date().toISOString() }]);
|
||||
showToast(`"${course.title.slice(0, 30)}..." añadido al carrito`);
|
||||
} else {
|
||||
setIsCartOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBuyNow = (course: Course) => {
|
||||
if (!cartItems.some((item) => item.course.id === course.id)) {
|
||||
setCartItems([...cartItems, { course, addedAt: new Date().toISOString() }]);
|
||||
}
|
||||
setIsCartOpen(true);
|
||||
};
|
||||
|
||||
const handleRemoveFromCart = (courseId: string) => {
|
||||
setCartItems(cartItems.filter((item) => item.course.id !== courseId));
|
||||
};
|
||||
|
||||
const handleClearCart = () => {
|
||||
setCartItems([]);
|
||||
};
|
||||
|
||||
// Course Management
|
||||
const handleCreateCourse = (newCourse: Course) => {
|
||||
setCourses([newCourse, ...courses]);
|
||||
showToast('¡Curso creado y publicado exitosamente en el marketplace!');
|
||||
};
|
||||
|
||||
const handleDeleteCourse = (courseId: string) => {
|
||||
setCourses(courses.filter((c) => c.id !== courseId));
|
||||
showToast('Curso eliminado correctamente');
|
||||
};
|
||||
|
||||
const handleScrollToCatalog = () => {
|
||||
const el = document.getElementById('catalog-section');
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollToCalculator = () => {
|
||||
const el = document.getElementById('seller-calculator');
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const cartCourseIds = cartItems.map((i) => i.course.id);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col selection:bg-cyan-500 selection:text-slate-950">
|
||||
|
||||
{/* Toast notification banner */}
|
||||
{toastMessage && (
|
||||
<div className="fixed bottom-6 right-6 z-50 flex items-center gap-3 px-5 py-3.5 rounded-2xl bg-slate-900/95 border border-cyan-500/40 text-white shadow-2xl shadow-cyan-500/20 backdrop-blur-md animate-in slide-in-from-bottom-5 duration-300">
|
||||
<FiCheckCircle className="text-cyan-400 text-lg shrink-0" />
|
||||
<span className="text-xs font-semibold">{toastMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Navigation Bar */}
|
||||
<Navbar
|
||||
viewMode={viewMode}
|
||||
onToggleViewMode={setViewMode}
|
||||
cartCount={cartItems.length}
|
||||
onOpenCart={() => setIsCartOpen(true)}
|
||||
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
/>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1">
|
||||
{viewMode === 'buyer' ? (
|
||||
/* ==================== MODO ESTUDIANTE (COMPRAR) ==================== */
|
||||
<>
|
||||
<HeroSection
|
||||
onExploreClick={handleScrollToCatalog}
|
||||
onOpenPreview={(courseId) => {
|
||||
const target = courses.find((c) => c.id === courseId) || courses[0];
|
||||
setSelectedCourseForModal(target);
|
||||
}}
|
||||
/>
|
||||
|
||||
<CourseGrid
|
||||
courses={courses}
|
||||
cartCourseIds={cartCourseIds}
|
||||
searchQuery={searchQuery}
|
||||
onAddToCart={handleAddToCart}
|
||||
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
||||
onClearSearch={() => setSearchQuery('')}
|
||||
/>
|
||||
|
||||
<FeaturesBanner />
|
||||
</>
|
||||
) : (
|
||||
/* ==================== MODO INSTRUCTOR (VENDER) ==================== */
|
||||
<>
|
||||
<SellerHero
|
||||
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
|
||||
onScrollToCalculator={handleScrollToCalculator}
|
||||
/>
|
||||
|
||||
<SellerDashboard />
|
||||
|
||||
<SellerCourseList
|
||||
courses={courses}
|
||||
onOpenCreateModal={() => setIsCreateModalOpen(true)}
|
||||
onPreviewCourse={(course) => setSelectedCourseForModal(course)}
|
||||
onDeleteCourse={handleDeleteCourse}
|
||||
/>
|
||||
|
||||
<SellerCalculator
|
||||
onStartCourse={() => setIsCreateModalOpen(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Course Detail Modal */}
|
||||
<CourseModal
|
||||
course={selectedCourseForModal}
|
||||
isOpen={!!selectedCourseForModal}
|
||||
isInCart={selectedCourseForModal ? cartCourseIds.includes(selectedCourseForModal.id) : false}
|
||||
onClose={() => setSelectedCourseForModal(null)}
|
||||
onAddToCart={handleAddToCart}
|
||||
onBuyNow={handleBuyNow}
|
||||
/>
|
||||
|
||||
{/* Slide-over Shopping Cart */}
|
||||
<CartDrawer
|
||||
isOpen={isCartOpen}
|
||||
cartItems={cartItems}
|
||||
onClose={() => setIsCartOpen(false)}
|
||||
onRemoveItem={handleRemoveFromCart}
|
||||
onClearCart={handleClearCart}
|
||||
/>
|
||||
|
||||
{/* Create Course Modal */}
|
||||
<CreateCourseModal
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onCreateCourse={handleCreateCourse}
|
||||
/>
|
||||
|
||||
{/* Global Footer */}
|
||||
<Footer />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FiX,
|
||||
FiTrash2,
|
||||
FiShoppingCart,
|
||||
FiTag,
|
||||
FiCheckCircle,
|
||||
FiArrowRight,
|
||||
FiLock,
|
||||
FiShield
|
||||
} from 'react-icons/fi';
|
||||
import type { CartItem } from '../../types/course';
|
||||
|
||||
interface CartDrawerProps {
|
||||
isOpen: boolean;
|
||||
cartItems: CartItem[];
|
||||
onClose: () => void;
|
||||
onRemoveItem: (courseId: string) => void;
|
||||
onClearCart: () => void;
|
||||
}
|
||||
|
||||
export const CartDrawer: React.FC<CartDrawerProps> = ({
|
||||
isOpen,
|
||||
cartItems,
|
||||
onClose,
|
||||
onRemoveItem,
|
||||
onClearCart,
|
||||
}) => {
|
||||
const [couponCode, setCouponCode] = useState('');
|
||||
const [appliedDiscount, setAppliedDiscount] = useState<number>(0);
|
||||
const [couponMessage, setCouponMessage] = useState<string | null>(null);
|
||||
const [isCheckingOut, setIsCheckingOut] = useState(false);
|
||||
const [checkoutSuccess, setCheckoutSuccess] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const rawSubtotal = cartItems.reduce((acc, item) => acc + item.course.price, 0);
|
||||
const discountAmount = (rawSubtotal * appliedDiscount) / 100;
|
||||
const finalTotal = Math.max(0, rawSubtotal - discountAmount);
|
||||
|
||||
const handleApplyCoupon = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const code = couponCode.trim().toUpperCase();
|
||||
if (code === 'DESCUENTO20' || code === 'PROMO20') {
|
||||
setAppliedDiscount(20);
|
||||
setCouponMessage('¡Cupón del 20% aplicado con éxito!');
|
||||
} else if (code === 'PROMO50' || code === 'RYNEXT50') {
|
||||
setAppliedDiscount(50);
|
||||
setCouponMessage('¡Cupón especial del 50% de descuento aplicado!');
|
||||
} else if (code.length > 0) {
|
||||
setCouponMessage('Cupón no válido. Prueba con "DESCUENTO20" o "RYNEXT50"');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckout = () => {
|
||||
setIsCheckingOut(true);
|
||||
setTimeout(() => {
|
||||
setIsCheckingOut(false);
|
||||
setCheckoutSuccess(true);
|
||||
setTimeout(() => {
|
||||
setCheckoutSuccess(false);
|
||||
onClearCart();
|
||||
onClose();
|
||||
}, 3000);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-hidden bg-slate-950/70 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="absolute inset-y-0 right-0 max-w-full flex pl-10">
|
||||
|
||||
{/* Slide panel */}
|
||||
<div className="w-screen max-w-md bg-slate-900 border-l border-slate-800 shadow-2xl flex flex-col justify-between">
|
||||
|
||||
{/* Top Header */}
|
||||
<div className="p-6 border-b border-slate-800 flex items-center justify-between bg-slate-950/50">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-lg bg-cyan-500/10 text-cyan-400">
|
||||
<FiShoppingCart className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white">Carrito de Compras</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
{cartItems.length} {cartItems.length === 1 ? 'curso seleccionado' : 'cursos seleccionados'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
aria-label="Cerrar carrito"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cart Items List */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
{checkoutSuccess ? (
|
||||
<div className="text-center py-12 space-y-4 animate-in zoom-in-95 duration-300">
|
||||
<div className="w-16 h-16 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto text-3xl">
|
||||
<FiCheckCircle />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white">¡Compra Realizada con Éxito!</h4>
|
||||
<p className="text-xs text-slate-300 max-w-xs mx-auto">
|
||||
Hemos enviado los accesos a tu correo. Tus cursos ya están disponibles en tu biblioteca de aprendizaje.
|
||||
</p>
|
||||
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 text-xs text-cyan-400 font-mono">
|
||||
Orden #RY-{Math.floor(100000 + Math.random() * 900000)}
|
||||
</div>
|
||||
</div>
|
||||
) : cartItems.length > 0 ? (
|
||||
cartItems.map((item) => (
|
||||
<div
|
||||
key={item.course.id}
|
||||
className="flex gap-3.5 p-3 rounded-xl bg-slate-950/60 border border-slate-800/80 hover:border-slate-700 transition"
|
||||
>
|
||||
<img
|
||||
src={item.course.thumbnail}
|
||||
alt={item.course.title}
|
||||
className="w-20 h-14 object-cover rounded-lg bg-slate-800 shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white line-clamp-1">
|
||||
{item.course.title}
|
||||
</h4>
|
||||
<p className="text-[11px] text-slate-400 truncate">
|
||||
Por {item.course.instructor.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<span className="text-sm font-bold text-white font-mono">
|
||||
${item.course.price.toFixed(2)}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onRemoveItem(item.course.id)}
|
||||
className="text-slate-500 hover:text-rose-400 text-xs flex items-center gap-1 transition"
|
||||
title="Eliminar curso"
|
||||
>
|
||||
<FiTrash2 />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-16 space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 text-slate-500 flex items-center justify-center mx-auto text-2xl">
|
||||
<FiShoppingCart />
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-white">Tu carrito está vacío</h4>
|
||||
<p className="text-xs text-slate-400 max-w-xs mx-auto">
|
||||
Explora nuestro catálogo de cursos en desarrollo, IA y diseño para comenzar.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Summary & Checkout Form */}
|
||||
{cartItems.length > 0 && !checkoutSuccess && (
|
||||
<div className="p-6 border-t border-slate-800 bg-slate-950/90 space-y-4">
|
||||
|
||||
{/* Coupon Form */}
|
||||
<form onSubmit={handleApplyCoupon} className="space-y-1.5">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<FiTag className="absolute left-3 top-3 text-slate-500 text-xs" />
|
||||
<input
|
||||
type="text"
|
||||
value={couponCode}
|
||||
onChange={(e) => setCouponCode(e.target.value)}
|
||||
placeholder="Cupón (ej. DESCUENTO20)"
|
||||
className="w-full pl-8 pr-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-xs text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500 uppercase"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold text-xs rounded-lg transition"
|
||||
>
|
||||
Aplicar
|
||||
</button>
|
||||
</div>
|
||||
{couponMessage && (
|
||||
<p className={`text-[11px] ${appliedDiscount > 0 ? 'text-emerald-400' : 'text-amber-400'}`}>
|
||||
{couponMessage}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{/* Price Breakdown */}
|
||||
<div className="space-y-1.5 text-xs text-slate-400 pt-2 border-t border-slate-800">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span className="font-mono text-slate-200">${rawSubtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
{appliedDiscount > 0 && (
|
||||
<div className="flex justify-between text-emerald-400">
|
||||
<span>Descuento ({appliedDiscount}%)</span>
|
||||
<span className="font-mono">-${discountAmount.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-slate-800">
|
||||
<span>Total a pagar</span>
|
||||
<span className="text-xl text-cyan-400 font-mono">${finalTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkout Button */}
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
disabled={isCheckingOut}
|
||||
className="w-full py-3.5 bg-gradient-to-r from-blue-600 via-cyan-600 to-teal-500 hover:from-blue-500 hover:to-cyan-400 text-white font-bold rounded-xl text-sm shadow-xl shadow-cyan-500/25 transition-all flex items-center justify-center gap-2 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{isCheckingOut ? (
|
||||
<span>Procesando pago seguro...</span>
|
||||
) : (
|
||||
<>
|
||||
<FiLock className="text-sm" />
|
||||
<span>Completar Pago Seguro</span>
|
||||
<FiArrowRight className="text-sm" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-[11px] text-slate-500">
|
||||
<FiShield className="text-emerald-400" />
|
||||
<span>Garantía total de devolución de 30 días</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FiGrid,
|
||||
FiCode,
|
||||
FiCpu,
|
||||
FiLayout,
|
||||
FiShield,
|
||||
FiTrendingUp,
|
||||
FiDollarSign
|
||||
} from 'react-icons/fi';
|
||||
import { mockCategories } from '../../data/mockCourses';
|
||||
|
||||
interface CategoryPillsProps {
|
||||
selectedCategory: string;
|
||||
onSelectCategory: (categoryName: string) => void;
|
||||
}
|
||||
|
||||
export const CategoryPills: React.FC<CategoryPillsProps> = ({
|
||||
selectedCategory,
|
||||
onSelectCategory,
|
||||
}) => {
|
||||
const getCategoryIcon = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case 'FiCode':
|
||||
return <FiCode className="text-base" />;
|
||||
case 'FiCpu':
|
||||
return <FiCpu className="text-base" />;
|
||||
case 'FiLayout':
|
||||
return <FiLayout className="text-base" />;
|
||||
case 'FiShield':
|
||||
return <FiShield className="text-base" />;
|
||||
case 'FiTrendingUp':
|
||||
return <FiTrendingUp className="text-base" />;
|
||||
case 'FiDollarSign':
|
||||
return <FiDollarSign className="text-base" />;
|
||||
default:
|
||||
return <FiGrid className="text-base" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto pb-3 pt-2 no-scrollbar">
|
||||
<div className="flex items-center gap-2.5 min-w-max">
|
||||
{mockCategories.map((cat) => {
|
||||
const isSelected =
|
||||
(cat.id === 'all' && selectedCategory === 'Todos') ||
|
||||
cat.name === selectedCategory ||
|
||||
(cat.id === 'all' && selectedCategory === '');
|
||||
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => onSelectCategory(cat.id === 'all' ? 'Todos' : cat.name)}
|
||||
className={`flex items-center gap-2.5 px-4 py-2.5 rounded-xl text-xs font-semibold transition-all cursor-pointer select-none ${
|
||||
isSelected
|
||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-lg shadow-cyan-500/20 scale-[1.02]'
|
||||
: 'bg-slate-900/80 hover:bg-slate-800 text-slate-300 hover:text-white border border-slate-800 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className={isSelected ? 'text-white' : 'text-cyan-400'}>
|
||||
{getCategoryIcon(cat.icon)}
|
||||
</span>
|
||||
<span>{cat.name}</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded-full font-mono ${
|
||||
isSelected
|
||||
? 'bg-white/20 text-white'
|
||||
: 'bg-slate-800 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
{cat.count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FiStar,
|
||||
FiClock,
|
||||
FiBook,
|
||||
FiShoppingCart,
|
||||
FiCheck,
|
||||
FiEye,
|
||||
FiZap
|
||||
} from 'react-icons/fi';
|
||||
import type { Course } from '../../types/course';
|
||||
|
||||
interface CourseCardProps {
|
||||
course: Course;
|
||||
isInCart: boolean;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onOpenDetails: (course: Course) => void;
|
||||
}
|
||||
|
||||
export const CourseCard: React.FC<CourseCardProps> = ({
|
||||
course,
|
||||
isInCart,
|
||||
onAddToCart,
|
||||
onOpenDetails,
|
||||
}) => {
|
||||
const discountPercent = Math.round(
|
||||
((course.originalPrice - course.price) / course.originalPrice) * 100
|
||||
);
|
||||
|
||||
const getBadgeStyle = (badge?: string) => {
|
||||
switch (badge) {
|
||||
case 'Bestseller':
|
||||
return 'bg-amber-500 text-slate-950';
|
||||
case 'Nuevo':
|
||||
return 'bg-emerald-500 text-white';
|
||||
case 'Oferta':
|
||||
return 'bg-rose-500 text-white';
|
||||
case 'Destacado':
|
||||
return 'bg-indigo-500 text-white';
|
||||
default:
|
||||
return 'bg-blue-500 text-white';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative flex flex-col justify-between rounded-2xl bg-slate-900/90 border border-slate-800 hover:border-slate-700 transition-all duration-300 hover:shadow-2xl hover:shadow-cyan-500/10 hover:-translate-y-1 overflow-hidden">
|
||||
|
||||
{/* Top Image Section */}
|
||||
<div>
|
||||
<div className="relative aspect-video w-full overflow-hidden bg-slate-800 cursor-pointer" onClick={() => onOpenDetails(course)}>
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-black/30" />
|
||||
|
||||
{/* Badge */}
|
||||
{course.badge && (
|
||||
<span
|
||||
className={`absolute top-3 left-3 px-2.5 py-1 rounded-md text-[10px] font-black uppercase tracking-wider shadow-md ${getBadgeStyle(
|
||||
course.badge
|
||||
)}`}
|
||||
>
|
||||
{course.badge === 'Bestseller' && '🔥 '}
|
||||
{course.badge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Discount Tag */}
|
||||
{discountPercent > 0 && (
|
||||
<span className="absolute top-3 right-3 bg-rose-600/95 text-white px-2 py-0.5 rounded text-[10px] font-black tracking-wide shadow-md">
|
||||
-{discountPercent}% OFF
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Quick Preview Hover Overlay */}
|
||||
<div className="absolute inset-0 bg-slate-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3 backdrop-blur-[2px]">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenDetails(course);
|
||||
}}
|
||||
className="px-3.5 py-2 bg-white/90 hover:bg-white text-slate-950 rounded-xl text-xs font-bold flex items-center gap-1.5 shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-transform"
|
||||
>
|
||||
<FiEye /> Vista Rápida
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Duration & Lessons Overlay */}
|
||||
<div className="absolute bottom-2 left-3 right-3 flex items-center justify-between text-[11px] text-slate-300 font-medium">
|
||||
<span className="flex items-center gap-1 bg-slate-950/80 backdrop-blur-md px-2 py-0.5 rounded">
|
||||
<FiClock className="text-cyan-400" /> {course.duration}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 bg-slate-950/80 backdrop-blur-md px-2 py-0.5 rounded">
|
||||
<FiBook className="text-cyan-400" /> {course.lessonsCount} lecciones
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="p-5 space-y-3">
|
||||
{/* Category & Level */}
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="font-semibold text-cyan-400 uppercase tracking-wider">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded bg-slate-800 text-slate-400 border border-slate-700/50">
|
||||
{course.level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3
|
||||
onClick={() => onOpenDetails(course)}
|
||||
className="text-base font-bold text-white group-hover:text-cyan-300 transition-colors line-clamp-2 cursor-pointer leading-snug"
|
||||
>
|
||||
{course.title}
|
||||
</h3>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">
|
||||
{course.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Instructor info */}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<img
|
||||
src={course.instructor.avatar}
|
||||
alt={course.instructor.name}
|
||||
className="w-6 h-6 rounded-full object-cover ring-1 ring-cyan-500/30"
|
||||
/>
|
||||
<span className="text-xs text-slate-300 font-medium truncate">
|
||||
{course.instructor.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Rating and Reviews */}
|
||||
<div className="flex items-center gap-2 pt-1 text-xs">
|
||||
<div className="flex items-center gap-1 font-bold text-amber-400">
|
||||
<span>{course.rating.toFixed(1)}</span>
|
||||
<div className="flex text-amber-400 text-xs">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<FiStar
|
||||
key={i}
|
||||
className={`inline ${
|
||||
i < Math.floor(course.rating)
|
||||
? 'fill-amber-400 text-amber-400'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-slate-500 font-mono text-[11px]">
|
||||
({course.reviewsCount.toLocaleString()})
|
||||
</span>
|
||||
<span className="text-slate-600">•</span>
|
||||
<span className="text-slate-400 text-[11px]">
|
||||
{course.studentsCount.toLocaleString()} alumnos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing & Cart Action Bottom */}
|
||||
<div className="p-5 pt-3 border-t border-slate-800/80 bg-slate-950/40 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="text-xl font-black text-white font-mono">
|
||||
${course.price.toFixed(2)}
|
||||
</span>
|
||||
{course.originalPrice > course.price && (
|
||||
<span className="text-xs text-slate-500 line-through font-mono">
|
||||
${course.originalPrice.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-emerald-400 flex items-center gap-0.5">
|
||||
<FiZap /> Acceso total + Certificado
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onAddToCart(course)}
|
||||
disabled={isInCart}
|
||||
className={`px-3.5 py-2.5 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-all shadow-md ${
|
||||
isInCart
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 cursor-default'
|
||||
: 'bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white shadow-cyan-500/20 hover:scale-105 active:scale-95'
|
||||
}`}
|
||||
>
|
||||
{isInCart ? (
|
||||
<>
|
||||
<FiCheck className="text-sm" />
|
||||
<span>En Carrito</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FiShoppingCart className="text-sm" />
|
||||
<span>Añadir</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
FiFilter,
|
||||
FiFlag,
|
||||
FiStar,
|
||||
FiPercent,
|
||||
FiSpeaker,
|
||||
FiLayers,
|
||||
FiRefreshCw
|
||||
} from 'react-icons/fi';
|
||||
import type { Course, ActiveTabFilter } from '../../types/course';
|
||||
import { CourseCard } from './CourseCard';
|
||||
import { CategoryPills } from './CategoryPills';
|
||||
|
||||
interface CourseGridProps {
|
||||
courses: Course[];
|
||||
cartCourseIds: string[];
|
||||
searchQuery: string;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onOpenDetails: (course: Course) => void;
|
||||
onClearSearch: () => void;
|
||||
}
|
||||
|
||||
export const CourseGrid: React.FC<CourseGridProps> = ({
|
||||
courses,
|
||||
cartCourseIds,
|
||||
searchQuery,
|
||||
onAddToCart,
|
||||
onOpenDetails,
|
||||
onClearSearch,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<ActiveTabFilter>('all');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('Todos');
|
||||
const [selectedLevel, setSelectedLevel] = useState<string>('Todos');
|
||||
const [sortBy, setSortBy] = useState<'rating' | 'price-asc' | 'price-desc' | 'students'>('students');
|
||||
|
||||
// Filtered & Sorted courses computation
|
||||
const filteredCourses = useMemo(() => {
|
||||
return courses
|
||||
.filter((course) => {
|
||||
// Search query filter
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchTitle = course.title.toLowerCase().includes(q);
|
||||
const matchSub = course.subtitle.toLowerCase().includes(q);
|
||||
const matchCat = course.category.toLowerCase().includes(q);
|
||||
const matchInst = course.instructor.name.toLowerCase().includes(q);
|
||||
if (!matchTitle && !matchSub && !matchCat && !matchInst) return false;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (selectedCategory !== 'Todos') {
|
||||
if (course.category !== selectedCategory) return false;
|
||||
}
|
||||
|
||||
// Level filter
|
||||
if (selectedLevel !== 'Todos') {
|
||||
if (course.level !== selectedLevel && course.level !== 'Todos los niveles') return false;
|
||||
}
|
||||
|
||||
// Tab filter
|
||||
if (activeTab === 'popular' && course.studentsCount < 6000) return false;
|
||||
if (activeTab === 'top-rated' && course.rating < 4.9) return false;
|
||||
if (activeTab === 'offers' && !course.badge?.includes('Oferta') && course.originalPrice <= course.price) return false;
|
||||
if (activeTab === 'new' && course.badge !== 'Nuevo') return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'rating') return b.rating - a.rating;
|
||||
if (sortBy === 'price-asc') return a.price - b.price;
|
||||
if (sortBy === 'price-desc') return b.price - a.price;
|
||||
return b.studentsCount - a.studentsCount; // default: popular
|
||||
});
|
||||
}, [courses, searchQuery, selectedCategory, selectedLevel, activeTab, sortBy]);
|
||||
|
||||
const resetAllFilters = () => {
|
||||
setActiveTab('all');
|
||||
setSelectedCategory('Todos');
|
||||
setSelectedLevel('Todos');
|
||||
setSortBy('students');
|
||||
onClearSearch();
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="catalog-section" className="py-12 bg-slate-950/60">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-cyan-400 text-xs font-bold uppercase tracking-wider mb-2">
|
||||
<FiLayers />
|
||||
<span>Catálogo Completo</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
|
||||
Explora Cursos Destacados
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
Filtra por tu área de interés, nivel o aprovecha promociones de temporada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Tabs */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-slate-900 border border-slate-800 rounded-xl">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'all'
|
||||
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({courses.length})
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('popular')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'popular'
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiFlag className="text-amber-400" /> Populares
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('top-rated')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'top-rated'
|
||||
? 'bg-blue-500/20 text-blue-300 border border-blue-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiStar className="text-blue-400" /> Top Calificados
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('offers')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'offers'
|
||||
? 'bg-rose-500/20 text-rose-300 border border-rose-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiPercent className="text-rose-400" /> Ofertas
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('new')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'new'
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiSpeaker className="text-emerald-400" /> Nuevos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories Bar */}
|
||||
<div className="mb-6">
|
||||
<CategoryPills
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={setSelectedCategory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Secondary Filter & Sorting Controls */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 p-4 rounded-xl bg-slate-900/50 border border-slate-800/80 mb-8">
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="flex items-center gap-1 text-xs font-semibold text-slate-400">
|
||||
<FiFilter className="text-cyan-400" /> Filtros:
|
||||
</span>
|
||||
|
||||
{/* Level Filter */}
|
||||
<select
|
||||
value={selectedLevel}
|
||||
onChange={(e) => setSelectedLevel(e.target.value)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700 rounded-lg px-3 py-1.5 text-xs font-medium focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="Todos">Nivel: Todos</option>
|
||||
<option value="Principiante">Principiante</option>
|
||||
<option value="Intermedio">Intermedio</option>
|
||||
<option value="Avanzado">Avanzado</option>
|
||||
</select>
|
||||
|
||||
{/* Clear Filters if active */}
|
||||
{(selectedCategory !== 'Todos' || selectedLevel !== 'Todos' || activeTab !== 'all' || searchQuery.trim()) && (
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="flex items-center gap-1 text-xs text-rose-400 hover:text-rose-300 font-medium px-2 py-1 rounded bg-rose-500/10 border border-rose-500/20 transition"
|
||||
>
|
||||
<FiRefreshCw className="text-[10px]" /> Limpiar filtros
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sort By Dropdown & Results Counter */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-400">
|
||||
Mostrando <strong className="text-white font-mono">{filteredCourses.length}</strong> cursos
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-400">Ordenar por:</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700 rounded-lg px-3 py-1.5 text-xs font-medium focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="students">Más populares (Alumnos)</option>
|
||||
<option value="rating">Mejor valorados</option>
|
||||
<option value="price-asc">Precio: Menor a Mayor</option>
|
||||
<option value="price-desc">Precio: Mayor a Menor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Courses Grid */}
|
||||
{filteredCourses.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{filteredCourses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.id}
|
||||
course={course}
|
||||
isInCart={cartCourseIds.includes(course.id)}
|
||||
onAddToCart={onAddToCart}
|
||||
onOpenDetails={onOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 bg-slate-900/40 rounded-2xl border border-slate-800 p-8 space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center mx-auto text-2xl">
|
||||
<FiFilter />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">No se encontraron cursos con estos filtros</h3>
|
||||
<p className="text-xs text-slate-400 max-w-md mx-auto">
|
||||
Intenta cambiar los términos de búsqueda, la categoría o restablecer los filtros para ver todos los cursos disponibles.
|
||||
</p>
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-cyan-500/20"
|
||||
>
|
||||
Restablecer todos los filtros
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,388 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
FiX,
|
||||
FiCheckCircle,
|
||||
FiPlay,
|
||||
FiClock,
|
||||
FiBook,
|
||||
FiStar,
|
||||
FiGlobe,
|
||||
FiAward,
|
||||
FiShield,
|
||||
FiChevronDown,
|
||||
FiChevronUp,
|
||||
FiShoppingCart
|
||||
} from 'react-icons/fi';
|
||||
import type { Course } from '../../types/course';
|
||||
|
||||
interface CourseModalProps {
|
||||
course: Course | null;
|
||||
isOpen: boolean;
|
||||
isInCart: boolean;
|
||||
onClose: () => void;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onBuyNow: (course: Course) => void;
|
||||
}
|
||||
|
||||
export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
course,
|
||||
isOpen,
|
||||
isInCart,
|
||||
onClose,
|
||||
onAddToCart,
|
||||
onBuyNow,
|
||||
}) => {
|
||||
const [expandedModules, setExpandedModules] = useState<Record<string, boolean>>({
|
||||
m1: true,
|
||||
m2: true,
|
||||
m3: true
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<'curriculum' | 'instructor' | 'reviews'>('curriculum');
|
||||
const [isPlayingVideo, setIsPlayingVideo] = useState(false);
|
||||
|
||||
if (!isOpen || !course) return null;
|
||||
|
||||
const toggleModule = (modId: string) => {
|
||||
setExpandedModules((prev) => ({ ...prev, [modId]: !prev[modId] }));
|
||||
};
|
||||
|
||||
const discountPercent = Math.round(
|
||||
((course.originalPrice - course.price) / course.originalPrice) * 100
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-2 sm:p-4 animate-in fade-in duration-200">
|
||||
|
||||
{/* Modal Container */}
|
||||
<div
|
||||
className="relative w-full max-w-5xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl overflow-hidden my-8 max-h-[92vh] flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
{/* Top Header Bar */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/60 sticky top-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-semibold px-2.5 py-1 rounded bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">• Nivel: {course.level}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
aria-label="Cerrar modal"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body Scrollable */}
|
||||
<div className="overflow-y-auto flex-1 p-6 space-y-8">
|
||||
|
||||
{/* Top Hero & Video Preview Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
|
||||
{/* Main Info */}
|
||||
<div className="lg:col-span-7 space-y-4">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white leading-tight">
|
||||
{course.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
{course.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Meta stats */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs pt-2 text-slate-400 border-t border-slate-800/80">
|
||||
<div className="flex items-center gap-1.5 text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" />
|
||||
<span>{course.rating.toFixed(1)}</span>
|
||||
<span className="text-slate-400 font-normal">({course.reviewsCount} reseñas)</span>
|
||||
</div>
|
||||
<div>👥 {course.studentsCount.toLocaleString()} estudiantes</div>
|
||||
<div className="flex items-center gap-1"><FiGlobe /> {course.language}</div>
|
||||
<div className="flex items-center gap-1"><FiClock /> Actualizado: {course.updatedAt}</div>
|
||||
</div>
|
||||
|
||||
{/* What You Will Learn Card */}
|
||||
<div className="p-5 rounded-xl bg-slate-950/60 border border-slate-800 space-y-3">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<FiAward className="text-cyan-400" /> Lo que aprenderás en este curso
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-2">
|
||||
{course.whatYouWillLearn.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-xs text-slate-300">
|
||||
<FiCheckCircle className="text-emerald-400 text-sm mt-0.5 shrink-0" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video Preview & Buy Box */}
|
||||
<div className="lg:col-span-5 space-y-4">
|
||||
<div className="rounded-xl overflow-hidden bg-slate-950 border border-slate-800 shadow-xl relative aspect-video group">
|
||||
{!isPlayingVideo ? (
|
||||
<>
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-slate-950/40 flex flex-col items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsPlayingVideo(true)}
|
||||
className="w-14 h-14 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-xl shadow-lg shadow-cyan-500/50 hover:scale-110 transition-transform"
|
||||
>
|
||||
<FiPlay className="ml-1" />
|
||||
</button>
|
||||
<span className="text-xs font-bold text-white bg-slate-900/80 px-2.5 py-1 rounded-md border border-slate-700">
|
||||
Vista previa gratuita de lección
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center bg-slate-950 p-4 text-center space-y-3">
|
||||
<div className="w-12 h-12 rounded-full bg-cyan-500/20 text-cyan-400 flex items-center justify-center text-xl animate-pulse">
|
||||
<FiPlay />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Reproduciendo Clase Muestra (HD)</p>
|
||||
<p className="text-[11px] text-slate-400">Lección 1: Novedades y Configuración Inicial</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPlayingVideo(false)}
|
||||
className="text-xs text-rose-400 hover:underline"
|
||||
>
|
||||
Pausar y volver a portada
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price & Checkout Card */}
|
||||
<div className="p-5 rounded-xl bg-slate-950/80 border border-slate-800 space-y-4 shadow-xl">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-black text-white font-mono">
|
||||
${course.price.toFixed(2)}
|
||||
</span>
|
||||
{course.originalPrice > course.price && (
|
||||
<span className="text-sm text-slate-500 line-through font-mono">
|
||||
${course.originalPrice.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{discountPercent > 0 && (
|
||||
<span className="px-2 py-1 rounded bg-rose-500/20 border border-rose-500/30 text-rose-400 text-xs font-bold">
|
||||
{discountPercent}% Descuento
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onBuyNow(course);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full py-3 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-bold rounded-xl text-sm shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.01]"
|
||||
>
|
||||
Comprar Ahora
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onAddToCart(course)}
|
||||
disabled={isInCart}
|
||||
className={`w-full py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all ${
|
||||
isInCart
|
||||
? 'bg-slate-800 text-slate-400 border border-slate-700 cursor-default'
|
||||
: 'bg-slate-800 hover:bg-slate-700 text-white border border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<FiShoppingCart />
|
||||
<span>{isInCart ? 'Ya está en el carrito' : 'Añadir al Carrito'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 text-[11px] text-slate-400 pt-2 border-t border-slate-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiClock className="text-cyan-400" /> {course.duration} de video bajo demanda
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiBook className="text-cyan-400" /> {course.lessonsCount} lecciones descargables
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiShield className="text-emerald-400" /> Garantía de 30 días o te devolvemos el dinero
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiAward className="text-cyan-400" /> Certificado de acreditación oficial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Nav Tabs inside modal */}
|
||||
<div className="border-b border-slate-800 flex gap-6 text-sm font-semibold">
|
||||
<button
|
||||
onClick={() => setActiveTab('curriculum')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'curriculum'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Contenido del Curso ({course.modules.length} Módulos)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('instructor')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'instructor'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Instructor
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('reviews')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'reviews'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Valoraciones ({course.reviews.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content 1: Curriculum */}
|
||||
{activeTab === 'curriculum' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>{course.modules.length} módulos • {course.lessonsCount} lecciones • {course.duration} duración total</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{course.modules.map((mod) => {
|
||||
const isExpanded = !!expandedModules[mod.id];
|
||||
return (
|
||||
<div key={mod.id} className="rounded-xl border border-slate-800 overflow-hidden bg-slate-950/40">
|
||||
<button
|
||||
onClick={() => toggleModule(mod.id)}
|
||||
className="w-full flex items-center justify-between p-4 bg-slate-900/60 hover:bg-slate-900 transition text-left"
|
||||
>
|
||||
<div className="font-bold text-sm text-slate-200 flex items-center gap-2">
|
||||
{isExpanded ? <FiChevronUp className="text-cyan-400" /> : <FiChevronDown className="text-slate-500" />}
|
||||
<span>{mod.title}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 font-mono">
|
||||
{mod.lessons.length} lecciones
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="divide-y divide-slate-850 p-2 space-y-1">
|
||||
{mod.lessons.map((lesson) => (
|
||||
<div
|
||||
key={lesson.id}
|
||||
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-slate-900/80 text-xs transition"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-slate-300">
|
||||
<div className="w-5 h-5 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center text-[10px]">
|
||||
<FiPlay />
|
||||
</div>
|
||||
<span>{lesson.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{lesson.isFreePreview && (
|
||||
<span className="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 text-[10px] font-bold">
|
||||
Vista Previa
|
||||
</span>
|
||||
)}
|
||||
<span className="text-slate-400 font-mono">{lesson.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content 2: Instructor */}
|
||||
{activeTab === 'instructor' && (
|
||||
<div className="p-6 rounded-xl bg-slate-950/60 border border-slate-800 space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<img
|
||||
src={course.instructor.avatar}
|
||||
alt={course.instructor.name}
|
||||
className="w-16 h-16 rounded-full object-cover ring-2 ring-cyan-500/50"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">{course.instructor.name}</h3>
|
||||
<p className="text-xs text-cyan-400 font-medium">{course.instructor.role}</p>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-slate-400 mt-2">
|
||||
<span className="flex items-center gap-1 text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" /> {course.instructor.rating} Calificación
|
||||
</span>
|
||||
<span>👨🎓 {course.instructor.totalStudents.toLocaleString()} Estudiantes</span>
|
||||
<span>📚 {course.instructor.totalCourses} Cursos creados</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed border-t border-slate-800 pt-4">
|
||||
{course.instructor.bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content 3: Reviews */}
|
||||
{activeTab === 'reviews' && (
|
||||
<div className="space-y-4">
|
||||
{course.reviews.map((rev) => (
|
||||
<div key={rev.id} className="p-4 rounded-xl bg-slate-950/60 border border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img
|
||||
src={rev.userAvatar}
|
||||
alt={rev.userName}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white">{rev.userName}</h4>
|
||||
<span className="text-[10px] text-slate-500">{rev.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex text-amber-400 text-xs">
|
||||
{[...Array(rev.rating)].map((_, i) => (
|
||||
<FiStar key={i} className="fill-amber-400" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed pl-10">
|
||||
"{rev.comment}"
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { FiAward, FiClock, FiUsers, FiShield } from 'react-icons/fi';
|
||||
|
||||
export const FeaturesBanner: React.FC = () => {
|
||||
const features = [
|
||||
{
|
||||
icon: <FiAward className="text-2xl text-cyan-400" />,
|
||||
title: 'Certificados Verificables',
|
||||
description: 'Comparte tus logros con enlaces verificables en LinkedIn y tu CV profesional.'
|
||||
},
|
||||
{
|
||||
icon: <FiClock className="text-2xl text-blue-400" />,
|
||||
title: 'Acceso Ilimitado de por Vida',
|
||||
description: 'Aprende a tu ritmo. Vuelve a consultar el material y las actualizaciones cuando quieras.'
|
||||
},
|
||||
{
|
||||
icon: <FiUsers className="text-2xl text-indigo-400" />,
|
||||
title: 'Comunidad Exclusiva en Discord',
|
||||
description: 'Resuelve dudas directamente con profesores y conecta con otros estudiantes.'
|
||||
},
|
||||
{
|
||||
icon: <FiShield className="text-2xl text-emerald-400" />,
|
||||
title: 'Garantía 100% de 30 Días',
|
||||
description: 'Si el curso no cumple con tus expectativas, te devolvemos el dinero sin preguntas.'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-slate-950/80 border-t border-slate-800/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<div className="text-center max-w-3xl mx-auto mb-12 space-y-3">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-cyan-400 bg-cyan-500/10 px-3 py-1 rounded-full border border-cyan-500/20">
|
||||
¿Por qué elegir RyCourses?
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-4xl font-extrabold text-white">
|
||||
Diseñado para acelerar tu crecimiento profesional
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400">
|
||||
No somos solo videos; somos una plataforma integral de educación moderna para la era de la Inteligencia Artificial.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{features.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 hover:border-slate-700 transition-all hover:-translate-y-1 hover:shadow-xl hover:shadow-cyan-500/5 group"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-800 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
||||
{f.icon}
|
||||
</div>
|
||||
<h3 className="text-base font-bold text-white mb-2">{f.title}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{f.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { FiArrowRight, FiPlay, FiStar, FiUsers, FiAward, FiCheckCircle } from 'react-icons/fi';
|
||||
import { FaGraduationCap } from 'react-icons/fa';
|
||||
|
||||
interface HeroSectionProps {
|
||||
onExploreClick: () => void;
|
||||
onOpenPreview: (courseId: string) => void;
|
||||
}
|
||||
|
||||
export const HeroSection: React.FC<HeroSectionProps> = ({ onExploreClick, onOpenPreview }) => {
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-slate-950 pt-10 pb-16 lg:pt-16 lg:pb-24 border-b border-slate-800/60">
|
||||
|
||||
{/* Background glow effects */}
|
||||
<div className="absolute -top-40 -left-40 w-96 h-96 bg-blue-600/20 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute top-1/3 -right-40 w-96 h-96 bg-cyan-500/15 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -bottom-20 left-1/3 w-80 h-80 bg-indigo-600/15 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
|
||||
|
||||
{/* Left Column: Copy & CTAs */}
|
||||
<div className="lg:col-span-7 space-y-6 text-center lg:text-left">
|
||||
|
||||
{/* Promo Pill */}
|
||||
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold bg-gradient-to-r from-blue-500/15 to-cyan-500/15 border border-cyan-500/30 text-cyan-300">
|
||||
<span className="flex h-2 w-2 rounded-full bg-cyan-400 animate-ping" />
|
||||
<FaGraduationCap className="text-cyan-400 text-sm" />
|
||||
<span>Nuevos cursos actualizados para 2026</span>
|
||||
</div>
|
||||
|
||||
{/* Main Headline */}
|
||||
<h1 className="text-3xl sm:text-5xl lg:text-6xl font-black text-white tracking-tight leading-[1.15]">
|
||||
Aprende las habilidades{' '}
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-400 to-teal-300">
|
||||
más demandadas
|
||||
</span>{' '}
|
||||
con expertos reales.
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-base sm:text-lg text-slate-300 max-w-2xl mx-auto lg:mx-0 font-normal leading-relaxed">
|
||||
Cursos prácticos basados en proyectos reales. Desde <span className="text-cyan-300 font-medium">Desarrollo Web y Apps</span> hasta <span className="text-cyan-300 font-medium">Inteligencia Artificial y Ciberseguridad</span>. Aprende a tu propio ritmo y obtén certificaciones verificadas.
|
||||
</p>
|
||||
|
||||
{/* Actions & Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 pt-2">
|
||||
<button
|
||||
onClick={onExploreClick}
|
||||
className="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-blue-600 via-cyan-600 to-teal-500 hover:from-blue-500 hover:to-cyan-400 text-white font-bold rounded-xl text-base shadow-xl shadow-cyan-500/25 hover:shadow-cyan-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<span>Explorar Catálogo</span>
|
||||
<FiArrowRight className="text-lg" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onOpenPreview('course-1')}
|
||||
className="w-full sm:w-auto px-6 py-4 bg-slate-900/80 hover:bg-slate-800 text-slate-200 hover:text-white font-semibold rounded-xl text-base border border-slate-700 hover:border-slate-600 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-cyan-500/20 text-cyan-400 flex items-center justify-center text-xs">
|
||||
<FiPlay className="ml-0.5" />
|
||||
</div>
|
||||
<span>Ver Clase Muestra Gratis</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Trust checkmarks */}
|
||||
<div className="pt-3 flex flex-wrap items-center justify-center lg:justify-start gap-x-6 gap-y-2 text-xs text-slate-400">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FiCheckCircle className="text-emerald-400" /> Acceso de por vida
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FiCheckCircle className="text-emerald-400" /> Garantía de reembolso 30 días
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FiCheckCircle className="text-emerald-400" /> Certificado de finalización
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Hero Visual Card */}
|
||||
<div className="lg:col-span-5 relative">
|
||||
<div className="relative mx-auto max-w-md lg:max-w-none">
|
||||
|
||||
{/* Main Course Highlight Card */}
|
||||
<div className="rounded-2xl bg-gradient-to-b from-slate-800/80 to-slate-900/90 border border-slate-700/80 p-5 shadow-2xl backdrop-blur-md relative overflow-hidden group">
|
||||
|
||||
{/* Thumbnail image */}
|
||||
<div className="relative rounded-xl overflow-hidden aspect-video mb-4 bg-slate-800">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&auto=format&fit=crop&q=80"
|
||||
alt="React 19 Preview"
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/90 via-slate-950/20 to-transparent" />
|
||||
|
||||
{/* Play badge */}
|
||||
<button
|
||||
onClick={() => onOpenPreview('course-1')}
|
||||
className="absolute inset-0 m-auto w-14 h-14 rounded-full bg-gradient-to-r from-blue-600 to-cyan-500 text-white flex items-center justify-center shadow-lg shadow-cyan-500/50 hover:scale-110 active:scale-95 transition-transform"
|
||||
aria-label="Reproducir video"
|
||||
>
|
||||
<FiPlay className="text-xl ml-1" />
|
||||
</button>
|
||||
|
||||
<span className="absolute top-3 left-3 bg-amber-500/90 text-slate-950 text-[11px] font-black px-2.5 py-1 rounded-md uppercase tracking-wider shadow">
|
||||
🔥 Curso Más Vendido
|
||||
</span>
|
||||
|
||||
<span className="absolute bottom-3 right-3 bg-slate-950/80 backdrop-blur-md text-white text-xs px-2 py-0.5 rounded font-mono">
|
||||
38h 45m
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Course Meta in card */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span className="font-semibold text-cyan-400">Desarrollo Web & Next.js</span>
|
||||
<div className="flex items-center gap-1 text-amber-400">
|
||||
<FiStar className="fill-amber-400" />
|
||||
<span className="font-bold text-white">4.9</span>
|
||||
<span>(1.4k reseñas)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-white leading-snug">
|
||||
Master en React 19, Next.js 15 y TypeScript Profesional
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
|
||||
alt="Instructor"
|
||||
className="w-7 h-7 rounded-full object-cover ring-1 ring-cyan-400/50"
|
||||
/>
|
||||
<span className="text-xs text-slate-300 font-medium">Ing. Carlos Mendoza</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-xs text-slate-500 line-through font-mono">$89.99</span>
|
||||
<span className="text-xl font-black text-emerald-400 font-mono">$24.99</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Stat Badge: Students Active */}
|
||||
<div className="absolute -bottom-6 -left-6 bg-slate-900/90 border border-slate-700/80 rounded-xl p-3.5 shadow-2xl backdrop-blur-md flex items-center gap-3 hidden sm:flex animate-bounce duration-1000">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 text-blue-400 flex items-center justify-center text-lg">
|
||||
<FiUsers />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">50,000+ Estudiantes</p>
|
||||
<p className="text-[11px] text-slate-400">Aprendiendo activamente</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Stat Badge: Top Rated */}
|
||||
<div className="absolute -top-6 -right-6 bg-slate-900/90 border border-slate-700/80 rounded-xl p-3.5 shadow-2xl backdrop-blur-md flex items-center gap-3 hidden sm:flex">
|
||||
<div className="w-10 h-10 rounded-lg bg-amber-500/20 text-amber-400 flex items-center justify-center text-lg">
|
||||
<FiAward />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Calidad Top 1%</p>
|
||||
<p className="text-[11px] text-slate-400">Instructores validados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom stats banner */}
|
||||
<div className="mt-16 pt-10 border-t border-slate-800/80 grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-white font-mono">1,200+</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Cursos en Alta Definición</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-cyan-400 font-mono">50,000+</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Alumnos Graduados</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-amber-400 font-mono">4.9 / 5.0</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Puntuación Media</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-emerald-400 font-mono">98%</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Tasa de Contratación</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FiBookOpen,
|
||||
FiMail,
|
||||
FiCheckCircle,
|
||||
FiGlobe,
|
||||
FiShield,
|
||||
FiAward,
|
||||
FiHeart
|
||||
} from 'react-icons/fi';
|
||||
import { FaTwitter, FaYoutube, FaGithub, FaLinkedin, FaDiscord } from 'react-icons/fa';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email.trim()) {
|
||||
setSubscribed(true);
|
||||
setTimeout(() => setSubscribed(false), 4000);
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-slate-950 border-t border-slate-800 text-slate-400 text-sm">
|
||||
{/* Top Banner / Newsletter */}
|
||||
<div className="border-b border-slate-800/80 bg-gradient-to-r from-blue-950/40 via-indigo-950/40 to-slate-950/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||
<div className="max-w-xl text-center lg:text-left">
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 mb-3">
|
||||
<FiAward /> Actualizaciones semanales de cursos
|
||||
</span>
|
||||
<h3 className="text-2xl font-bold text-white">
|
||||
Suscríbete para recibir descuentos exclusivos y nuevos lanzamientos
|
||||
</h3>
|
||||
<p className="text-slate-400 text-xs sm:text-sm mt-2">
|
||||
Únete a más de 50,000 profesionales que potencian su carrera cada semana. Sin spam.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Newsletter form */}
|
||||
<form onSubmit={handleSubscribe} className="w-full max-w-md">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="relative flex-1">
|
||||
<FiMail className="absolute left-3.5 top-3.5 text-slate-500" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="[email protected]"
|
||||
className="w-full pl-10 pr-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white font-semibold rounded-xl text-sm transition-all shadow-lg shadow-cyan-500/20 whitespace-nowrap"
|
||||
>
|
||||
{subscribed ? '¡Suscrito!' : 'Suscribirme'}
|
||||
</button>
|
||||
</div>
|
||||
{subscribed && (
|
||||
<p className="flex items-center gap-1.5 text-emerald-400 text-xs mt-2 animate-fadeIn">
|
||||
<FiCheckCircle /> ¡Gracias por suscribirte! Revisa tu bandeja de entrada pronto.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Footer Links */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-14">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 lg:gap-12">
|
||||
|
||||
{/* Brand col */}
|
||||
<div className="col-span-2 lg:col-span-2">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-indigo-600 via-blue-500 to-cyan-400 p-[2px]">
|
||||
<div className="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center">
|
||||
<FiBookOpen className="text-cyan-400 text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-white tracking-tight">
|
||||
Ry<span className="text-cyan-400">Courses</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 leading-relaxed mb-6 max-w-sm">
|
||||
La plataforma líder para aprender habilidades de alta demanda y convertir tu conocimiento profesional en un negocio digital rentable.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-cyan-400 hover:border-cyan-500/40 transition">
|
||||
<FaTwitter />
|
||||
</a>
|
||||
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-red-400 hover:border-red-500/40 transition">
|
||||
<FaYoutube />
|
||||
</a>
|
||||
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-white hover:border-slate-600 transition">
|
||||
<FaGithub />
|
||||
</a>
|
||||
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-blue-400 hover:border-blue-500/40 transition">
|
||||
<FaLinkedin />
|
||||
</a>
|
||||
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-indigo-400 hover:border-indigo-500/40 transition">
|
||||
<FaDiscord />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 1: Estudiantes */}
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-200 mb-4">Para Estudiantes</h4>
|
||||
<ul className="space-y-2.5 text-xs">
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Explorar Cursos</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Rutas de Especialización</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Certificados Oficiales</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Becas y Descuentos</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Preguntas Frecuentes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Instructores */}
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-200 mb-4">Para Instructores</h4>
|
||||
<ul className="space-y-2.5 text-xs">
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Enseñar en RyCourses</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Calculadora de Ganancias</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Guía del Creador</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Políticas de Contenido</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Comunidad de Profesores</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Empresa & Legal */}
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-200 mb-4">Empresa & Soporte</h4>
|
||||
<ul className="space-y-2.5 text-xs">
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Sobre Nosotros</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">RyCourses para Empresas</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Términos de Servicio</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Política de Privacidad</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Centro de Ayuda</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="mt-12 pt-8 border-t border-slate-800/80 flex flex-col md:flex-row items-center justify-between gap-4 text-xs">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex items-center gap-1.5 text-slate-400">
|
||||
<FiGlobe className="text-cyan-400" /> Español (América Latina)
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 text-slate-400">
|
||||
<FiShield className="text-emerald-400" /> Pagos cifrados SSL 256-bit
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 text-slate-500">
|
||||
<span>© 2026 RyCourses Inc. Hecho con</span>
|
||||
<FiHeart className="text-rose-500 inline text-xs" />
|
||||
<span>para creadores y estudiantes.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,264 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FiSearch,
|
||||
FiShoppingCart,
|
||||
FiHeart,
|
||||
FiBell,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiBookOpen,
|
||||
FiPlusCircle,
|
||||
FiCompass,
|
||||
} from 'react-icons/fi';
|
||||
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
||||
import type { ViewMode } from '../../types/course';
|
||||
|
||||
interface NavbarProps {
|
||||
viewMode: ViewMode;
|
||||
onToggleViewMode: (mode: ViewMode) => void;
|
||||
cartCount: number;
|
||||
onOpenCart: () => void;
|
||||
onOpenCreateCourseModal: () => void;
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
}
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({
|
||||
viewMode,
|
||||
onToggleViewMode,
|
||||
cartCount,
|
||||
onOpenCart,
|
||||
onOpenCreateCourseModal,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
}) => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [categoryDropdown, setCategoryDropdown] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/80 backdrop-blur-md transition-all">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-20 gap-4">
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-8">
|
||||
<div
|
||||
onClick={() => onToggleViewMode('buyer')}
|
||||
className="flex items-center gap-3 cursor-pointer group select-none"
|
||||
>
|
||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-tr from-indigo-600 via-blue-500 to-cyan-400 p-[2px] shadow-lg shadow-indigo-500/20 group-hover:scale-105 transition-transform">
|
||||
<div className="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center">
|
||||
<FiBookOpen className="text-cyan-400 text-xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xl font-black tracking-tight text-white group-hover:text-cyan-300 transition-colors">
|
||||
Ry<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-cyan-400">Courses</span>
|
||||
</span>
|
||||
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
|
||||
PRO
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 font-medium tracking-wide">Plataforma Educativa</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Categories dropdown (Desktop) */}
|
||||
{viewMode === 'buyer' && (
|
||||
<div className="relative hidden lg:block">
|
||||
<button
|
||||
onClick={() => setCategoryDropdown(!categoryDropdown)}
|
||||
className="flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-800/60 border border-slate-700/60 transition"
|
||||
>
|
||||
<FiCompass className="text-cyan-400" />
|
||||
<span>Explorar</span>
|
||||
</button>
|
||||
|
||||
{categoryDropdown && (
|
||||
<div
|
||||
onMouseLeave={() => setCategoryDropdown(false)}
|
||||
className="absolute top-full left-0 mt-2 w-56 rounded-xl bg-slate-900 border border-slate-800 shadow-2xl p-2 z-50 animate-in fade-in slide-in-from-top-2 duration-200"
|
||||
>
|
||||
{['Desarrollo Web', 'Inteligencia Artificial', 'Diseño UI/UX', 'Ciberseguridad', 'Marketing Digital', 'Negocios'].map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => {
|
||||
onSearchChange(cat);
|
||||
setCategoryDropdown(false);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs font-medium text-slate-300 hover:text-cyan-300 hover:bg-slate-800/80 rounded-lg transition"
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search bar (Desktop) */}
|
||||
{viewMode === 'buyer' && (
|
||||
<div className="hidden md:flex flex-1 max-w-md mx-4">
|
||||
<div className="relative w-full">
|
||||
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||
<FiSearch className="text-base" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="¿Qué habilidad quieres aprender hoy? (React, Python, Figma...)"
|
||||
className="w-full pl-10 pr-16 py-2.5 bg-slate-900/90 border border-slate-700/70 rounded-xl text-sm text-slate-100 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 transition-all shadow-inner"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<kbd className="hidden xl:inline-block px-1.5 py-0.5 text-[10px] font-semibold text-slate-400 bg-slate-800 border border-slate-700 rounded">
|
||||
Ctrl + K
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View Mode Switcher (Estudiante vs Instructor) & User Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
{/* Mode Switcher pill */}
|
||||
<div className="bg-slate-900/90 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
|
||||
<button
|
||||
onClick={() => onToggleViewMode('buyer')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
viewMode === 'buyer'
|
||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-md shadow-cyan-500/20'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
title="Modo Estudiante - Explorar y Comprar cursos"
|
||||
>
|
||||
<FaGraduationCap className="text-sm" />
|
||||
<span className="hidden sm:inline">Comprar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onToggleViewMode('seller')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
viewMode === 'seller'
|
||||
? 'bg-gradient-to-r from-indigo-600 to-violet-500 text-white shadow-md shadow-violet-500/20'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
title="Modo Instructor - Vender y Gestionar cursos"
|
||||
>
|
||||
<FaChalkboardTeacher className="text-sm" />
|
||||
<span className="hidden sm:inline">Vender</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* If seller mode: Create Course button */}
|
||||
{viewMode === 'seller' && (
|
||||
<button
|
||||
onClick={onOpenCreateCourseModal}
|
||||
className="hidden sm:flex items-center gap-2 px-4 py-2 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-medium text-xs shadow-lg shadow-violet-600/30 hover:scale-[1.02] transition-all"
|
||||
>
|
||||
<FiPlusCircle className="text-base" />
|
||||
<span>Crear Curso</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Cart button (buyer mode) */}
|
||||
{viewMode === 'buyer' && (
|
||||
<button
|
||||
onClick={onOpenCart}
|
||||
className="relative p-2.5 text-slate-300 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 rounded-xl transition"
|
||||
aria-label="Carrito de compras"
|
||||
>
|
||||
<FiShoppingCart className="text-lg" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-gradient-to-tr from-rose-500 to-pink-500 text-white text-[11px] font-bold rounded-full flex items-center justify-center animate-bounce shadow-md">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Wishlist */}
|
||||
{viewMode === 'buyer' && (
|
||||
<button className="hidden sm:flex p-2.5 text-slate-400 hover:text-rose-400 bg-slate-900/60 hover:bg-slate-800/80 border border-slate-800 rounded-xl transition">
|
||||
<FiHeart className="text-lg" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Notification Bell */}
|
||||
<button className="relative hidden sm:flex p-2.5 text-slate-400 hover:text-amber-400 bg-slate-900/60 hover:bg-slate-800/80 border border-slate-800 rounded-xl transition">
|
||||
<FiBell className="text-lg" />
|
||||
<span className="absolute top-2 right-2 w-2 h-2 bg-amber-400 rounded-full"></span>
|
||||
</button>
|
||||
|
||||
{/* User Profile Avatar */}
|
||||
<div className="flex items-center gap-2 pl-2 border-l border-slate-800">
|
||||
<div className="relative cursor-pointer">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
|
||||
alt="Usuario"
|
||||
className="w-9 h-9 rounded-full object-cover ring-2 ring-indigo-500/50 hover:ring-cyan-400 transition"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-500 ring-2 ring-slate-950 rounded-full"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu trigger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="p-2 text-slate-400 hover:text-white md:hidden"
|
||||
>
|
||||
{mobileMenuOpen ? <FiX className="text-2xl" /> : <FiMenu className="text-2xl" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile search & menu expanded */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden py-4 border-t border-slate-800 space-y-4">
|
||||
<div className="relative">
|
||||
<FiSearch className="absolute left-3.5 top-3 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="Buscar cursos..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => { onToggleViewMode('buyer'); setMobileMenuOpen(false); }}
|
||||
className={`flex items-center justify-center gap-2 p-2.5 rounded-lg text-xs font-semibold ${
|
||||
viewMode === 'buyer' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||
}`}
|
||||
>
|
||||
<FaGraduationCap /> Modo Comprador
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onToggleViewMode('seller'); setMobileMenuOpen(false); }}
|
||||
className={`flex items-center justify-center gap-2 p-2.5 rounded-lg text-xs font-semibold ${
|
||||
viewMode === 'seller' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||
}`}
|
||||
>
|
||||
<FaChalkboardTeacher /> Modo Vendedor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{viewMode === 'seller' && (
|
||||
<button
|
||||
onClick={() => { onOpenCreateCourseModal(); setMobileMenuOpen(false); }}
|
||||
className="w-full py-2.5 bg-gradient-to-r from-violet-600 to-indigo-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiPlusCircle /> Crear Nuevo Curso
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,390 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FiX,
|
||||
FiCheck,
|
||||
FiArrowRight,
|
||||
FiArrowLeft,
|
||||
FiBookOpen,
|
||||
FiDollarSign,
|
||||
FiPlus,
|
||||
FiCheckCircle
|
||||
} from 'react-icons/fi';
|
||||
import type { Course, CourseLevel } from '../../types/course';
|
||||
import { mockInstructors } from '../../data/mockCourses';
|
||||
|
||||
interface CreateCourseModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateCourse: (newCourse: Course) => void;
|
||||
}
|
||||
|
||||
export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateCourse,
|
||||
}) => {
|
||||
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||
|
||||
// Form State
|
||||
const [title, setTitle] = useState('');
|
||||
const [subtitle, setSubtitle] = useState('');
|
||||
const [category, setCategory] = useState('Desarrollo Web');
|
||||
const [level, setLevel] = useState<CourseLevel>('Todos los niveles');
|
||||
const [price, setPrice] = useState('29.99');
|
||||
const [originalPrice, setOriginalPrice] = useState('99.99');
|
||||
const [thumbnail, setThumbnail] = useState(
|
||||
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80'
|
||||
);
|
||||
const [duration, setDuration] = useState('20h 30min');
|
||||
const [whatYouWillLearn, setWhatYouWillLearn] = useState([
|
||||
'Aprenderás conceptos clave y buenas prácticas de la industria',
|
||||
'Construirás proyectos prácticos de portafolio para conseguir empleo'
|
||||
]);
|
||||
const [newOutcome, setNewOutcome] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleAddOutcome = () => {
|
||||
if (newOutcome.trim()) {
|
||||
setWhatYouWillLearn([...whatYouWillLearn, newOutcome.trim()]);
|
||||
setNewOutcome('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveOutcome = (index: number) => {
|
||||
setWhatYouWillLearn(whatYouWillLearn.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const createdCourse: Course = {
|
||||
id: `course-${Date.now()}`,
|
||||
title: title || 'Nuevo Curso Especializado',
|
||||
subtitle: subtitle || 'Aprende los fundamentos y técnicas avanzadas en esta guía paso a paso.',
|
||||
category,
|
||||
level,
|
||||
price: parseFloat(price) || 19.99,
|
||||
originalPrice: parseFloat(originalPrice) || 59.99,
|
||||
rating: 5.0,
|
||||
reviewsCount: 1,
|
||||
studentsCount: 0,
|
||||
thumbnail: thumbnail || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80',
|
||||
duration: duration || '15h 00min',
|
||||
lessonsCount: 45,
|
||||
badge: 'Nuevo',
|
||||
instructor: mockInstructors.diego,
|
||||
updatedAt: 'Febrero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: whatYouWillLearn.length > 0 ? whatYouWillLearn : ['Dominar los temas principales del curso'],
|
||||
requirements: ['Ganas de aprender y una computadora con acceso a internet'],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: Introducción y Fundamentos',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Bienvenida al curso y objetivos', duration: '10:00', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Configuración del entorno de trabajo', duration: '15:30', isFreePreview: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
title: 'Módulo 2: Casos Prácticos y Proyecto Final',
|
||||
lessons: [
|
||||
{ id: 'l3', title: 'Construcción del proyecto paso a paso', duration: '30:00' },
|
||||
{ id: 'l4', title: 'Entrega y certificación', duration: '12:00' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [],
|
||||
isPublished: true,
|
||||
salesCount: 0,
|
||||
totalRevenue: 0
|
||||
};
|
||||
|
||||
onCreateCourse(createdCourse);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-3 sm:p-6 animate-in fade-in duration-200">
|
||||
|
||||
<div
|
||||
className="relative w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl overflow-hidden my-6"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-slate-800 bg-slate-950/60 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-violet-500/20 text-violet-400 flex items-center justify-center text-lg">
|
||||
<FiBookOpen />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white">Publicar Nuevo Curso</h3>
|
||||
<p className="text-xs text-slate-400">Paso {step} de 3</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Step Progress Bar */}
|
||||
<div className="w-full bg-slate-800 h-1.5">
|
||||
<div
|
||||
className="bg-gradient-to-r from-violet-600 to-indigo-500 h-full transition-all duration-300"
|
||||
style={{ width: `${(step / 3) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Form Body */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
||||
|
||||
{/* Step 1: Basic Information */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4 animate-in fade-in">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Título del Curso *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Ej. Master en Desarrollo de Software Full Stack"
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Subtítulo / Breve Descripción *
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
required
|
||||
value={subtitle}
|
||||
onChange={(e) => setSubtitle(e.target.value)}
|
||||
placeholder="Explica en 2 frases lo que el estudiante logrará al finalizar este curso."
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Categoría
|
||||
</label>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
|
||||
>
|
||||
<option value="Desarrollo Web">Desarrollo Web</option>
|
||||
<option value="Inteligencia Artificial">Inteligencia Artificial</option>
|
||||
<option value="Diseño UI/UX">Diseño UI/UX</option>
|
||||
<option value="Ciberseguridad">Ciberseguridad</option>
|
||||
<option value="Marketing Digital">Marketing Digital</option>
|
||||
<option value="Negocios & Finanzas">Negocios & Finanzas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Nivel del Curso
|
||||
</label>
|
||||
<select
|
||||
value={level}
|
||||
onChange={(e) => setLevel(e.target.value as CourseLevel)}
|
||||
className="w-full px-3 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
|
||||
>
|
||||
<option value="Principiante">Principiante</option>
|
||||
<option value="Intermedio">Intermedio</option>
|
||||
<option value="Avanzado">Avanzado</option>
|
||||
<option value="Todos los niveles">Todos los niveles</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Pricing & Duration */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-4 animate-in fade-in">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Precio de Venta ($ USD) *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiDollarSign className="absolute left-3 top-3 text-slate-400" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
required
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
placeholder="29.99"
|
||||
className="w-full pl-8 pr-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Precio Original Tachado ($ USD)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiDollarSign className="absolute left-3 top-3 text-slate-400" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={originalPrice}
|
||||
onChange={(e) => setOriginalPrice(e.target.value)}
|
||||
placeholder="89.99"
|
||||
className="w-full pl-8 pr-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Duración Total Estimada de Video
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
placeholder="Ej. 24h 45min"
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-violet-950/30 border border-violet-500/20 text-xs text-slate-300 space-y-1">
|
||||
<p className="font-bold text-violet-300">💡 Consejo de fijación de precios:</p>
|
||||
<p>Los cursos con precios entre $19.99 y $34.99 tienen una tasa de conversión 45% más alta en nuestra plataforma.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Media & What you'll learn */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4 animate-in fade-in">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
URL de Imagen de Portada (Thumbnail)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="url"
|
||||
value={thumbnail}
|
||||
onChange={(e) => setThumbnail(e.target.value)}
|
||||
placeholder="https://images.unsplash.com/..."
|
||||
className="flex-1 px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
{thumbnail && (
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt="Preview"
|
||||
className="mt-2 h-24 w-40 object-cover rounded-lg border border-slate-700"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Lo que aprenderá el alumno (Puntos clave)
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newOutcome}
|
||||
onChange={(e) => setNewOutcome(e.target.value)}
|
||||
placeholder="Ej. Crear APIs RESTful y GraphQL con Node.js"
|
||||
className="flex-1 px-3 py-2 bg-slate-950 border border-slate-700 rounded-lg text-xs text-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddOutcome}
|
||||
className="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg text-xs flex items-center gap-1"
|
||||
>
|
||||
<FiPlus /> Añadir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-h-32 overflow-y-auto">
|
||||
{whatYouWillLearn.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center justify-between p-2 rounded-lg bg-slate-950 border border-slate-800 text-xs text-slate-300"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 truncate">
|
||||
<FiCheckCircle className="text-emerald-400 shrink-0" /> {item}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveOutcome(idx)}
|
||||
className="text-slate-500 hover:text-rose-400 pl-2"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||
{step > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep((s) => (s - 1) as any)}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl flex items-center gap-1.5 transition"
|
||||
>
|
||||
<FiArrowLeft /> Anterior
|
||||
</button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{step < 3 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (step === 1 && !title.trim()) {
|
||||
setTitle('Nuevo Curso Especializado');
|
||||
}
|
||||
setStep((s) => (s + 1) as any);
|
||||
}}
|
||||
className="px-5 py-2 bg-violet-600 hover:bg-violet-500 text-white text-xs font-bold rounded-xl flex items-center gap-1.5 transition shadow-lg shadow-violet-600/30"
|
||||
>
|
||||
<span>Siguiente</span>
|
||||
<FiArrowRight />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2.5 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white text-xs font-bold rounded-xl flex items-center gap-2 transition shadow-lg shadow-emerald-500/30"
|
||||
>
|
||||
<FiCheck className="text-base" />
|
||||
<span>Publicar Curso Ahora</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react';
|
||||
import { FiDollarSign, FiUsers, FiCheckCircle } from 'react-icons/fi';
|
||||
|
||||
interface SellerCalculatorProps {
|
||||
onStartCourse: () => void;
|
||||
}
|
||||
|
||||
export const SellerCalculator: React.FC<SellerCalculatorProps> = ({ onStartCourse }) => {
|
||||
const [coursePrice, setCoursePrice] = useState<number>(29);
|
||||
const [monthlyStudents, setMonthlyStudents] = useState<number>(150);
|
||||
|
||||
const instructorRevenueShare = 0.90; // 90% revenue
|
||||
const monthlyRevenue = coursePrice * monthlyStudents * instructorRevenueShare;
|
||||
const annualRevenue = monthlyRevenue * 12;
|
||||
|
||||
return (
|
||||
<section id="seller-calculator" className="py-16 bg-slate-950/90 border-b border-slate-800/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<div className="text-center max-w-3xl mx-auto mb-12 space-y-3">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-violet-400 bg-violet-500/10 px-3 py-1 rounded-full border border-violet-500/20">
|
||||
Simulador Interactivo
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-4xl font-extrabold text-white">
|
||||
Calcula tu potencial de ingresos como instructor
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400">
|
||||
Ajusta el precio de tu curso y la cantidad de alumnos estimados al mes para ver cuánto podrías ganar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center bg-slate-900/60 border border-slate-800 rounded-3xl p-6 sm:p-10 shadow-2xl backdrop-blur-sm">
|
||||
|
||||
{/* Controls column */}
|
||||
<div className="lg:col-span-6 space-y-8">
|
||||
|
||||
{/* Price Slider */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<FiDollarSign className="text-violet-400 text-base" /> Precio por curso
|
||||
</label>
|
||||
<span className="text-xl font-black text-cyan-400 font-mono">
|
||||
${coursePrice} USD
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="9"
|
||||
max="199"
|
||||
step="5"
|
||||
value={coursePrice}
|
||||
onChange={(e) => setCoursePrice(Number(e.target.value))}
|
||||
className="w-full h-2.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-violet-500"
|
||||
/>
|
||||
<div className="flex justify-between text-[11px] text-slate-500 font-mono">
|
||||
<span>$9 (Básico)</span>
|
||||
<span>$99 (Especialización)</span>
|
||||
<span>$199 (Master/Bootcamp)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Students Slider */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<FiUsers className="text-violet-400 text-base" /> Alumnos nuevos por mes
|
||||
</label>
|
||||
<span className="text-xl font-black text-violet-400 font-mono">
|
||||
{monthlyStudents} alumnos
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="1000"
|
||||
step="10"
|
||||
value={monthlyStudents}
|
||||
onChange={(e) => setMonthlyStudents(Number(e.target.value))}
|
||||
className="w-full h-2.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-violet-500"
|
||||
/>
|
||||
<div className="flex justify-between text-[11px] text-slate-500 font-mono">
|
||||
<span>10 alumnos</span>
|
||||
<span>500 alumnos</span>
|
||||
<span>1,000+ alumnos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features checkmarks */}
|
||||
<div className="space-y-2 pt-2 text-xs text-slate-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiCheckCircle className="text-emerald-400" /> Cobras el 90% del valor de cada venta
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiCheckCircle className="text-emerald-400" /> Sin cuotas mensuales ni cobros por alojamiento de video
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiCheckCircle className="text-emerald-400" /> Tus derechos de autor y propiedad intelectual 100% protegidos
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Results Display Column */}
|
||||
<div className="lg:col-span-6 bg-gradient-to-br from-violet-950/60 to-slate-950 p-8 rounded-2xl border border-violet-500/30 text-center space-y-6 shadow-xl">
|
||||
|
||||
<span className="text-xs font-semibold text-violet-300 uppercase tracking-wider">
|
||||
Ingresos Estimados Netos
|
||||
</span>
|
||||
|
||||
{/* Monthly Estimate */}
|
||||
<div>
|
||||
<div className="text-4xl sm:text-5xl font-black text-transparent bg-clip-text bg-gradient-to-r from-violet-400 via-purple-300 to-cyan-300 font-mono">
|
||||
${monthlyRevenue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1 font-medium">al mes proyectado</p>
|
||||
</div>
|
||||
|
||||
{/* Annual Estimate */}
|
||||
<div className="p-4 rounded-xl bg-slate-900/80 border border-slate-800 flex items-center justify-around">
|
||||
<div>
|
||||
<span className="text-[11px] text-slate-400">Proyección Anual</span>
|
||||
<p className="text-xl font-bold text-white font-mono">
|
||||
${annualRevenue.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} USD
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-8 w-[1px] bg-slate-800" />
|
||||
<div>
|
||||
<span className="text-[11px] text-slate-400">Comisión RyCourses</span>
|
||||
<p className="text-xl font-bold text-emerald-400 font-mono">Solo 10%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onStartCourse}
|
||||
className="w-full py-3.5 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold rounded-xl text-sm shadow-xl shadow-violet-600/30 transition-all hover:scale-[1.02] cursor-pointer"
|
||||
>
|
||||
Comenzar a Crear mi Curso Ahora
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FiPlusCircle,
|
||||
FiEdit,
|
||||
FiEye,
|
||||
FiTrash2,
|
||||
FiStar,
|
||||
FiUsers,
|
||||
FiSearch
|
||||
} from 'react-icons/fi';
|
||||
import type { Course } from '../../types/course';
|
||||
|
||||
interface SellerCourseListProps {
|
||||
courses: Course[];
|
||||
onOpenCreateModal: () => void;
|
||||
onPreviewCourse: (course: Course) => void;
|
||||
onDeleteCourse: (courseId: string) => void;
|
||||
}
|
||||
|
||||
export const SellerCourseList: React.FC<SellerCourseListProps> = ({
|
||||
courses,
|
||||
onOpenCreateModal,
|
||||
onPreviewCourse,
|
||||
onDeleteCourse,
|
||||
}) => {
|
||||
const [filterQuery, setFilterQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'draft'>('all');
|
||||
|
||||
const filtered = courses.filter((c) => {
|
||||
if (filterQuery.trim()) {
|
||||
const match = c.title.toLowerCase().includes(filterQuery.toLowerCase());
|
||||
if (!match) return false;
|
||||
}
|
||||
if (statusFilter === 'published' && !c.isPublished) return false;
|
||||
if (statusFilter === 'draft' && c.isPublished) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="py-12 bg-slate-950/70 border-t border-slate-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
{/* Header & New Course trigger */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Mis Cursos Gestionados</h2>
|
||||
<p className="text-xs text-slate-400">
|
||||
Administra tus contenidos, actualiza precios y supervisa las ventas de tus cursos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onOpenCreateModal}
|
||||
className="px-5 py-2.5 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-violet-600/30 flex items-center gap-2 self-start sm:self-auto cursor-pointer"
|
||||
>
|
||||
<FiPlusCircle className="text-base" />
|
||||
<span>Crear Nuevo Curso</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 p-4 rounded-xl bg-slate-900/60 border border-slate-800">
|
||||
<div className="relative w-full sm:w-72">
|
||||
<FiSearch className="absolute left-3 top-3 text-slate-400 text-xs" />
|
||||
<input
|
||||
type="text"
|
||||
value={filterQuery}
|
||||
onChange={(e) => setFilterQuery(e.target.value)}
|
||||
placeholder="Buscar en mis cursos..."
|
||||
className="w-full pl-8 pr-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-xs text-white placeholder-slate-400 focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 self-start sm:self-auto">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
statusFilter === 'all'
|
||||
? 'bg-violet-600 text-white'
|
||||
: 'bg-slate-800 text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({courses.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('published')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
statusFilter === 'published'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-slate-800 text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Publicados
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('draft')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
statusFilter === 'draft'
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'bg-slate-800 text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Borradores
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Course Cards / Table list */}
|
||||
<div className="space-y-3">
|
||||
{filtered.map((course) => (
|
||||
<div
|
||||
key={course.id}
|
||||
className="p-4 sm:p-5 rounded-2xl bg-slate-900/80 border border-slate-800/90 hover:border-slate-700 transition flex flex-col md:flex-row items-start md:items-center justify-between gap-4"
|
||||
>
|
||||
|
||||
{/* Left info */}
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-24 h-16 object-cover rounded-xl bg-slate-800 shrink-0"
|
||||
/>
|
||||
|
||||
<div className="space-y-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-[11px] font-bold text-violet-400 uppercase">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${
|
||||
course.isPublished !== false
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
|
||||
: 'bg-amber-500/20 text-amber-300 border border-amber-500/30'
|
||||
}`}>
|
||||
{course.isPublished !== false ? '● Publicado' : '● Borrador'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold text-white truncate max-w-lg">
|
||||
{course.title}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-slate-400">
|
||||
<span className="flex items-center gap-1 text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" /> {course.rating.toFixed(1)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FiUsers /> {course.studentsCount.toLocaleString()} alumnos
|
||||
</span>
|
||||
<span>{course.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right financials & actions */}
|
||||
<div className="flex items-center justify-between md:justify-end gap-6 w-full md:w-auto pt-3 md:pt-0 border-t md:border-t-0 border-slate-800">
|
||||
|
||||
<div className="text-left md:text-right">
|
||||
<span className="text-[11px] text-slate-400">Ventas Generadas</span>
|
||||
<div className="text-base font-black text-emerald-400 font-mono">
|
||||
${(course.totalRevenue || course.price * (course.salesCount || 100)).toLocaleString('en-US', { minimumFractionDigits: 2 })}
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
Precio: ${course.price.toFixed(2)} USD
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onPreviewCourse(course)}
|
||||
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition"
|
||||
title="Vista previa del curso"
|
||||
>
|
||||
<FiEye />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition"
|
||||
title="Editar contenido"
|
||||
>
|
||||
<FiEdit />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onDeleteCourse(course.id)}
|
||||
className="p-2.5 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 transition"
|
||||
title="Eliminar curso"
|
||||
>
|
||||
<FiTrash2 />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FiDollarSign,
|
||||
FiUsers,
|
||||
FiStar,
|
||||
FiBookOpen,
|
||||
FiTrendingUp,
|
||||
FiArrowUpRight,
|
||||
FiActivity
|
||||
} from 'react-icons/fi';
|
||||
|
||||
export const SellerDashboard: React.FC = () => {
|
||||
const salesHistory = [
|
||||
{ month: 'Sep', amount: 3200, height: '45%' },
|
||||
{ month: 'Oct', amount: 4100, height: '60%' },
|
||||
{ month: 'Nov', amount: 3800, height: '55%' },
|
||||
{ month: 'Dic', amount: 5600, height: '80%' },
|
||||
{ month: 'Ene', amount: 6200, height: '90%' },
|
||||
{ month: 'Feb', amount: 7400, height: '100%' },
|
||||
];
|
||||
|
||||
const recentEnrollments = [
|
||||
{ student: 'Marcos R.', course: 'Master en React 19', date: 'Hace 5 min', amount: '$24.99', country: '🇲🇽' },
|
||||
{ student: 'Ana Paula L.', course: 'IA & Machine Learning', date: 'Hace 18 min', amount: '$29.99', country: '🇨🇴' },
|
||||
{ student: 'Gabriel V.', course: 'Diseño UX/UI en Figma', date: 'Hace 42 min', amount: '$19.99', country: '🇦🇷' },
|
||||
{ student: 'Elena T.', course: 'DevOps con AWS', date: 'Hace 1 hora', amount: '$32.99', country: '🇪🇸' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-12 bg-slate-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-violet-400 uppercase tracking-wider mb-1">
|
||||
<FiActivity />
|
||||
<span>Resumen Ejecutivo</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white">
|
||||
Panel de Rendimiento del Creador
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 bg-slate-900 border border-slate-800 px-3 py-1.5 rounded-lg w-fit">
|
||||
Últimos 30 días
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 4 Metric Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
|
||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Ingresos Totales</span>
|
||||
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 text-emerald-400 flex items-center justify-center text-base">
|
||||
<FiDollarSign />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-white font-mono">$110,480.90</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-emerald-400 font-semibold">
|
||||
<FiArrowUpRight /> +18.4% este mes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Alumnos Totales</span>
|
||||
<div className="w-8 h-8 rounded-lg bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-base">
|
||||
<FiUsers />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-white font-mono">34,500</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-cyan-400 font-semibold">
|
||||
<FiArrowUpRight /> +420 nuevos esta semana
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Calificación Media</span>
|
||||
<div className="w-8 h-8 rounded-lg bg-amber-500/10 text-amber-400 flex items-center justify-center text-base">
|
||||
<FiStar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-white font-mono">4.93 / 5.0</div>
|
||||
<div className="text-[11px] text-slate-400">Basado en 4,210 reseñas</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Cursos Publicados</span>
|
||||
<div className="w-8 h-8 rounded-lg bg-violet-500/10 text-violet-400 flex items-center justify-center text-base">
|
||||
<FiBookOpen />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-white font-mono">6 Activos</div>
|
||||
<div className="text-[11px] text-violet-300 font-medium">1 curso en borrador</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Analytics Chart & Recent Activity Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
|
||||
{/* Visual Sales Growth Chart */}
|
||||
<div className="lg:col-span-7 p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white">Evolución de Ventas Mensuales</h3>
|
||||
<p className="text-xs text-slate-400">Ingresos brutos acumulados en USD</p>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-md border border-emerald-500/20">
|
||||
+130% Crecimiento Semestral
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar chart mockup */}
|
||||
<div className="h-48 flex items-end justify-between gap-3 pt-6 pb-2 border-b border-slate-800">
|
||||
{salesHistory.map((item, idx) => (
|
||||
<div key={idx} className="flex-1 flex flex-col items-center gap-2 group">
|
||||
<span className="text-[10px] font-mono text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
${item.amount}
|
||||
</span>
|
||||
<div
|
||||
style={{ height: item.height }}
|
||||
className="w-full max-w-[42px] bg-gradient-to-t from-violet-600 to-indigo-400 rounded-t-lg group-hover:from-cyan-500 group-hover:to-teal-300 transition-all duration-300 shadow-md shadow-violet-500/10"
|
||||
/>
|
||||
<span className="text-xs font-medium text-slate-400">{item.month}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Real-time Sales Feed */}
|
||||
<div className="lg:col-span-5 p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-white">Inscripciones Recientes</h3>
|
||||
<span className="flex h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-800/80">
|
||||
{recentEnrollments.map((enr, i) => (
|
||||
<div key={i} className="py-3 flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-base">{enr.country}</span>
|
||||
<div>
|
||||
<h4 className="font-bold text-white">{enr.student}</h4>
|
||||
<p className="text-[11px] text-slate-400">{enr.course}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<span className="font-mono font-bold text-emerald-400">{enr.amount}</span>
|
||||
<p className="text-[10px] text-slate-500">{enr.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FiPlusCircle,
|
||||
FiDollarSign,
|
||||
FiTrendingUp,
|
||||
} from 'react-icons/fi';
|
||||
import { FaChalkboardTeacher } from 'react-icons/fa';
|
||||
|
||||
interface SellerHeroProps {
|
||||
onOpenCreateCourse: () => void;
|
||||
onScrollToCalculator: () => void;
|
||||
}
|
||||
|
||||
export const SellerHero: React.FC<SellerHeroProps> = ({
|
||||
onOpenCreateCourse,
|
||||
onScrollToCalculator,
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-slate-950 pt-12 pb-16 lg:pt-20 lg:pb-24 border-b border-slate-800">
|
||||
|
||||
{/* Background ambient lighting */}
|
||||
<div className="absolute top-0 right-1/4 w-96 h-96 bg-violet-600/20 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-1/4 w-96 h-96 bg-indigo-600/15 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
|
||||
|
||||
{/* Left Column: Heading & Value props */}
|
||||
<div className="lg:col-span-7 space-y-6 text-center lg:text-left">
|
||||
|
||||
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold bg-violet-500/15 border border-violet-500/30 text-violet-300">
|
||||
<FaChalkboardTeacher className="text-violet-400" />
|
||||
<span>RyCourses Creator Studio</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl sm:text-5xl lg:text-6xl font-black text-white tracking-tight leading-[1.15]">
|
||||
Convierte tu conocimiento en{' '}
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 via-purple-300 to-indigo-300">
|
||||
ingresos recurrentes.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-base sm:text-lg text-slate-300 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-normal">
|
||||
Únete a miles de instructores y profesionales que comparten su pasión, forman a la próxima generación de talento y generan ingresos mensuales vendiendo cursos de alta calidad.
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 pt-2">
|
||||
<button
|
||||
onClick={onOpenCreateCourse}
|
||||
className="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold rounded-xl text-base shadow-xl shadow-violet-600/30 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
<FiPlusCircle className="text-xl" />
|
||||
<span>Crear y Publicar Curso</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onScrollToCalculator}
|
||||
className="w-full sm:w-auto px-6 py-4 bg-slate-900/80 hover:bg-slate-800 text-slate-200 hover:text-white font-semibold rounded-xl text-base border border-slate-700 hover:border-slate-600 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiDollarSign className="text-violet-400" />
|
||||
<span>Calcular Mis Ganancias</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Creator Perks */}
|
||||
<div className="pt-4 grid grid-cols-2 sm:grid-cols-3 gap-4 text-left">
|
||||
<div className="p-3 rounded-xl bg-slate-900/60 border border-slate-800">
|
||||
<div className="text-lg font-bold text-violet-400 font-mono">Hasta 95%</div>
|
||||
<div className="text-xs text-slate-400">Ganancias por venta</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl bg-slate-900/60 border border-slate-800">
|
||||
<div className="text-lg font-bold text-cyan-400 font-mono">$0 Costo</div>
|
||||
<div className="text-xs text-slate-400">Para publicar cursos</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl bg-slate-900/60 border border-slate-800 col-span-2 sm:col-span-1">
|
||||
<div className="text-lg font-bold text-emerald-400 font-mono">Pagos Mensuales</div>
|
||||
<div className="text-xs text-slate-400">Vía Stripe y PayPal</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Instructor Success Preview Card */}
|
||||
<div className="lg:col-span-5">
|
||||
<div className="rounded-2xl bg-gradient-to-b from-slate-900 to-slate-950 border border-violet-500/30 p-6 shadow-2xl space-y-6">
|
||||
|
||||
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-violet-500/20 text-violet-400 flex items-center justify-center text-xl">
|
||||
<FiTrendingUp />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white">Panel de Ventas en Vivo</h3>
|
||||
<p className="text-[11px] text-emerald-400 font-medium">● Actualizado hace 2 min</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs px-2.5 py-1 rounded bg-violet-500/20 text-violet-300 font-bold">
|
||||
Top Creador
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats highlights */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-xl bg-slate-900 border border-slate-800">
|
||||
<span className="text-xs text-slate-400">Ventas Este Mes</span>
|
||||
<p className="text-2xl font-black text-white font-mono mt-1">$4,850.00</p>
|
||||
<span className="text-[10px] text-emerald-400 font-medium">↑ +24% vs mes anterior</span>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900 border border-slate-800">
|
||||
<span className="text-xs text-slate-400">Nuevos Alumnos</span>
|
||||
<p className="text-2xl font-black text-cyan-400 font-mono mt-1">+184</p>
|
||||
<span className="text-[10px] text-slate-400 font-medium">12 países diferentes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial highlight */}
|
||||
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800/80 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
|
||||
alt="Instructor"
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white">Ing. Carlos Mendoza</h4>
|
||||
<p className="text-[10px] text-slate-400">Instructor de Desarrollo Web</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 italic">
|
||||
"Publicar mi curso en RyCourses me permitió alcanzar a más de 30,000 alumnos en toda Latinoamérica y generar ingresos pasivos recurrentes."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,426 @@
|
||||
import type { Course, Instructor } from '../types/course';
|
||||
|
||||
export const mockInstructors: Record<string, Instructor> = {
|
||||
diego: {
|
||||
id: 'inst-1',
|
||||
name: 'Ing. Carlos Mendoza',
|
||||
role: 'Senior Full Stack & Cloud Architect',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&auto=format&fit=crop&q=80',
|
||||
rating: 4.9,
|
||||
totalStudents: 34500,
|
||||
totalCourses: 8,
|
||||
bio: 'Más de 12 años desarrollando sistemas escalables en Silicon Valley y liderando equipos de ingeniería en empresas Fortune 500.'
|
||||
},
|
||||
lucia: {
|
||||
id: 'inst-2',
|
||||
name: 'Lucía Fernández',
|
||||
role: 'Lead UI/UX Designer & Design Systems Expert',
|
||||
avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=150&auto=format&fit=crop&q=80',
|
||||
rating: 4.95,
|
||||
totalStudents: 22800,
|
||||
totalCourses: 5,
|
||||
bio: 'Especialista en Product Design con más de 8 años diseñando experiencias digitales para startups globales y marcas líderes.'
|
||||
},
|
||||
mateo: {
|
||||
id: 'inst-3',
|
||||
name: 'Dr. Mateo Valenzuela',
|
||||
role: 'AI Researcher & Machine Learning Engineer',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&auto=format&fit=crop&q=80',
|
||||
rating: 4.88,
|
||||
totalStudents: 18900,
|
||||
totalCourses: 6,
|
||||
bio: 'Doctor en Ciencias de la Computación, consultor internacional en Inteligencia Artificial Generativa y LLMs.'
|
||||
},
|
||||
sofia: {
|
||||
id: 'inst-4',
|
||||
name: 'Sofía Navarro',
|
||||
role: 'Growth Marketer & E-Commerce Strategist',
|
||||
avatar: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&auto=format&fit=crop&q=80',
|
||||
rating: 4.85,
|
||||
totalStudents: 14200,
|
||||
totalCourses: 4,
|
||||
bio: 'Ayudo a creadores y empresas a escalar sus ventas digitales mediante funnels de alta conversión y performance marketing.'
|
||||
},
|
||||
javier: {
|
||||
id: 'inst-5',
|
||||
name: 'Javier Silva',
|
||||
role: 'Ethical Hacker & Cybersecurity Specialist (CEH)',
|
||||
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&auto=format&fit=crop&q=80',
|
||||
rating: 4.92,
|
||||
totalStudents: 27100,
|
||||
totalCourses: 7,
|
||||
bio: 'Auditor de seguridad ofensiva, pentester certificado y conferencista en eventos de ciberseguridad en Iberoamérica.'
|
||||
}
|
||||
};
|
||||
|
||||
export const mockCourses: Course[] = [
|
||||
{
|
||||
id: 'course-1',
|
||||
title: 'Master en React 19, Next.js 15 y TypeScript Profesional',
|
||||
subtitle: 'Domina el ecosistema moderno de desarrollo web construyendo 5 aplicaciones reales desde cero con arquitecturas escalables.',
|
||||
category: 'Desarrollo Web',
|
||||
level: 'Intermedio',
|
||||
price: 24.99,
|
||||
originalPrice: 89.99,
|
||||
rating: 4.9,
|
||||
reviewsCount: 1420,
|
||||
studentsCount: 9540,
|
||||
thumbnail: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&auto=format&fit=crop&q=80',
|
||||
duration: '38h 45min',
|
||||
lessonsCount: 142,
|
||||
badge: 'Bestseller',
|
||||
instructor: mockInstructors.diego,
|
||||
updatedAt: 'Febrero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: [
|
||||
'Construir aplicaciones con React 19 Server Components y Next.js 15 App Router',
|
||||
'Dominar TypeScript estricto aplicado a proyectos de gran escala',
|
||||
'Gestión de estado global con Zustand, TanStack Query y Context',
|
||||
'Autenticación segura con Auth.js / NextAuth y base de datos con Prisma y PostgreSQL',
|
||||
'Despliegue continuo (CI/CD) en Vercel, Docker y AWS'
|
||||
],
|
||||
requirements: [
|
||||
'Conocimientos básicos de JavaScript (ES6+)',
|
||||
'Familiaridad básica con HTML y CSS',
|
||||
'Una computadora con Node.js instalado'
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: Fundamentos Avanzados de React 19',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Novedades de React 19: useActionState, useOptimistic y use', duration: '18:40', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Server Components vs Client Components', duration: '22:15', isFreePreview: true },
|
||||
{ id: 'l3', title: 'Optimización de Renderizado y Memoización Real', duration: '25:10' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
title: 'Módulo 2: Next.js 15 App Router & Server Actions',
|
||||
lessons: [
|
||||
{ id: 'l4', title: 'Arquitectura de Rutas, Layouts y Templates', duration: '20:30' },
|
||||
{ id: 'l5', title: 'Server Actions y Mutaciones Seguras', duration: '28:15' },
|
||||
{ id: 'l6', title: 'Caching y Revalidación bajo demanda (ISR)', duration: '19:45' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'm3',
|
||||
title: 'Módulo 3: Proyecto Final - Plataforma SaaS Completa',
|
||||
lessons: [
|
||||
{ id: 'l7', title: 'Modelado de Base de Datos con Prisma y Supabase', duration: '35:20' },
|
||||
{ id: 'l8', title: 'Integración de Pagos con Stripe Webhooks', duration: '40:10' },
|
||||
{ id: 'l9', title: 'Testing E2E con Playwright y Despliegue en Producción', duration: '30:00' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 'r1',
|
||||
userName: 'Alejandro Ramos',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 3 días',
|
||||
comment: 'El mejor curso de React y Next.js que he tomado. Todo está al día con la versión 19 y las explicaciones son clarísimas.'
|
||||
},
|
||||
{
|
||||
id: 'r2',
|
||||
userName: 'Mariana Gómez',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 1 semana',
|
||||
comment: 'Conseguí trabajo como Frontend Developer gracias a los proyectos del curso. ¡100% recomendado!'
|
||||
}
|
||||
],
|
||||
isPublished: true,
|
||||
salesCount: 1450,
|
||||
totalRevenue: 36235.50
|
||||
},
|
||||
{
|
||||
id: 'course-2',
|
||||
title: 'Inteligencia Artificial y Machine Learning con Python & LLMs',
|
||||
subtitle: 'De cero a experto: Redes Neuronales, Deep Learning, LangChain, RAG y creación de Agentes Inteligentes con Gemini y OpenAI.',
|
||||
category: 'Inteligencia Artificial',
|
||||
level: 'Todos los niveles',
|
||||
price: 29.99,
|
||||
originalPrice: 119.99,
|
||||
rating: 4.95,
|
||||
reviewsCount: 980,
|
||||
studentsCount: 7820,
|
||||
thumbnail: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&auto=format&fit=crop&q=80',
|
||||
duration: '42h 10min',
|
||||
lessonsCount: 168,
|
||||
badge: 'Destacado',
|
||||
instructor: mockInstructors.mateo,
|
||||
updatedAt: 'Enero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: [
|
||||
'Fundamentos matemáticos y programación en Python para Data Science',
|
||||
'Crear modelos de clasificación y regresión con Scikit-Learn y PyTorch',
|
||||
'Implementar arquitecturas RAG (Retrieval Augmented Generation) con Vector Databases',
|
||||
'Construir agentes autónomos y asistentes empresariales personalizados',
|
||||
'Fine-tuning de modelos de lenguaje de código abierto'
|
||||
],
|
||||
requirements: [
|
||||
'Ganas de aprender e interés por el mundo de los datos y la IA',
|
||||
'No se requieren conocimientos matemáticos avanzados previos'
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: Fundamentos de Python & Análisis de Datos',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Python moderno y manipulación con Pandas & NumPy', duration: '24:00', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Visualización de datos con Matplotlib y Seaborn', duration: '19:30' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
title: 'Módulo 2: Modelos Generativos y LLMs en Acción',
|
||||
lessons: [
|
||||
{ id: 'l3', title: 'Embeddings y Bases de Datos Vectoriales (Pinecone & Qdrant)', duration: '31:20', isFreePreview: true },
|
||||
{ id: 'l4', title: 'LangChain & LlamaIndex para RAG Empresarial', duration: '36:15' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 'r3',
|
||||
userName: 'Esteban Morales',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 5 días',
|
||||
comment: 'El Dr. Mateo explica conceptos complejos de forma sumamente sencilla y aplicable a proyectos reales.'
|
||||
}
|
||||
],
|
||||
isPublished: true,
|
||||
salesCount: 890,
|
||||
totalRevenue: 26691.10
|
||||
},
|
||||
{
|
||||
id: 'course-3',
|
||||
title: 'Diseño UX/UI Avanzado & Sistemas de Diseño en Figma',
|
||||
subtitle: 'Aprende metodologías de Research, Prototipado interactivo, Tokens de diseño y cómo preparar entregables listos para desarrolladores.',
|
||||
category: 'Diseño UI/UX',
|
||||
level: 'Principiante',
|
||||
price: 19.99,
|
||||
originalPrice: 69.99,
|
||||
rating: 4.92,
|
||||
reviewsCount: 1120,
|
||||
studentsCount: 8430,
|
||||
thumbnail: 'https://images.unsplash.com/photo-1581291518857-4e27b48ff24e?w=800&auto=format&fit=crop&q=80',
|
||||
duration: '28h 15min',
|
||||
lessonsCount: 96,
|
||||
badge: 'Bestseller',
|
||||
instructor: mockInstructors.lucia,
|
||||
updatedAt: 'Febrero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: [
|
||||
'Metodologías de UX Research, User Personas y Journey Maps',
|
||||
'Dominar Auto Layout, Variables y Componentes avanzados en Figma',
|
||||
'Crear y documentar un Design System escalable de nivel empresarial',
|
||||
'Crear microinteracciones y prototipos de alta fidelidad',
|
||||
'Construir un portafolio profesional que atraiga ofertas de trabajo'
|
||||
],
|
||||
requirements: [
|
||||
'Una cuenta gratuita en Figma',
|
||||
'No se necesita experiencia previa en diseño'
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: UX Research y Arquitectura de Información',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Fundamentos de la Experiencia de Usuario', duration: '15:20', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Entrevistas con usuarios y mapas de empatía', duration: '22:10' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
title: 'Módulo 2: Creación de Sistemas de Diseño en Figma',
|
||||
lessons: [
|
||||
{ id: 'l3', title: 'Variables de Color, Tipografía y Espaciado (Design Tokens)', duration: '29:40', isFreePreview: true },
|
||||
{ id: 'l4', title: 'Componentes Atómicos y Variantes dinámicas', duration: '33:15' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 'r4',
|
||||
userName: 'Camila Herrera',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 2 semanas',
|
||||
comment: 'Mi forma de diseñar cambió por completo. La sección de Design Tokens y Figma Variables vale oro.'
|
||||
}
|
||||
],
|
||||
isPublished: true,
|
||||
salesCount: 1100,
|
||||
totalRevenue: 21989.00
|
||||
},
|
||||
{
|
||||
id: 'course-4',
|
||||
title: 'Ciberseguridad Práctica: Pentesting & Ethical Hacking',
|
||||
subtitle: 'Aprende hacking ético, análisis de vulnerabilidades web (OWASP Top 10), seguridad en redes y pruebas de penetración profesionales.',
|
||||
category: 'Ciberseguridad',
|
||||
level: 'Intermedio',
|
||||
price: 26.99,
|
||||
originalPrice: 99.99,
|
||||
rating: 4.89,
|
||||
reviewsCount: 760,
|
||||
studentsCount: 6150,
|
||||
thumbnail: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&auto=format&fit=crop&q=80',
|
||||
duration: '34h 50min',
|
||||
lessonsCount: 125,
|
||||
badge: 'Oferta',
|
||||
instructor: mockInstructors.javier,
|
||||
updatedAt: 'Febrero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: [
|
||||
'Montar tu propio laboratorio virtual de pruebas con Kali Linux',
|
||||
'Auditorías web identificando vulnerabilidades OWASP Top 10 (SQLi, XSS, SSRF)',
|
||||
'Escaneo y enumeración con Nmap, Burp Suite y Metasploit',
|
||||
'Ingeniería social defensiva y auditoría de contraseñas',
|
||||
'Redacción de reportes ejecutivos de pentesting para clientes corporativos'
|
||||
],
|
||||
requirements: [
|
||||
'Conocimientos básicos sobre cómo funciona Internet y redes TCP/IP',
|
||||
'Computadora con al menos 8GB de RAM para máquinas virtuales'
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: Configuración del Laboratorio Ofensivo',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Instalación de Kali Linux y Metasploitable en VirtualBox', duration: '20:10', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Comandos esenciales de Linux para Hackers', duration: '25:40' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 'r5',
|
||||
userName: 'Rodrigo Paez',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 4 días',
|
||||
comment: 'Excelente enfoque ético y práctico. Las prácticas con Burp Suite y máquinas vulnerables son brutales.'
|
||||
}
|
||||
],
|
||||
isPublished: true,
|
||||
salesCount: 650,
|
||||
totalRevenue: 17543.50
|
||||
},
|
||||
{
|
||||
id: 'course-5',
|
||||
title: 'Marketing Digital y Embudos de Venta de Alta Conversión',
|
||||
subtitle: 'Multiplica tus ventas online con Meta Ads, TikTok Ads, Email Marketing automatizado y copywriting persuasivo.',
|
||||
category: 'Marketing Digital',
|
||||
level: 'Principiante',
|
||||
price: 18.99,
|
||||
originalPrice: 59.99,
|
||||
rating: 4.86,
|
||||
reviewsCount: 540,
|
||||
studentsCount: 4890,
|
||||
thumbnail: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&auto=format&fit=crop&q=80',
|
||||
duration: '22h 30min',
|
||||
lessonsCount: 78,
|
||||
badge: 'Nuevo',
|
||||
instructor: mockInstructors.sofia,
|
||||
updatedAt: 'Febrero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: [
|
||||
'Crear campañas publicitarias rentables en Facebook, Instagram y TikTok',
|
||||
'Diseñar landing pages que convierten tráfico frío en clientes pagadores',
|
||||
'Automatizar secuencias de correo electrónico con Klaviyo y ActiveCampaign',
|
||||
'Estrategias de retargeting y medición de ROAS con Google Analytics 4'
|
||||
],
|
||||
requirements: [
|
||||
'Una idea de negocio, producto, servicio o ganas de trabajar como freelancer'
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: Estrategia y Segmentación de Audiencias',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Definición del Buyer Persona y propuesta de valor única', duration: '18:15', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Configuración del Píxel de Meta y API de Conversiones', duration: '24:00' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 'r6',
|
||||
userName: 'Valeria Castro',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 6 días',
|
||||
comment: 'Aumenté las ventas de mi tienda un 40% aplicando los embudos que enseña Sofía. Muy clara y directa.'
|
||||
}
|
||||
],
|
||||
isPublished: true,
|
||||
salesCount: 420,
|
||||
totalRevenue: 7975.80
|
||||
},
|
||||
{
|
||||
id: 'course-6',
|
||||
title: 'Cloud Computing & DevOps con AWS, Docker y Kubernetes',
|
||||
subtitle: 'Aprende a desplegar arquitecturas en la nube resilientes, seguras y automatizadas para proyectos de nivel enterprise.',
|
||||
category: 'Desarrollo Web',
|
||||
level: 'Avanzado',
|
||||
price: 32.99,
|
||||
originalPrice: 129.99,
|
||||
rating: 4.93,
|
||||
reviewsCount: 890,
|
||||
studentsCount: 5410,
|
||||
thumbnail: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&auto=format&fit=crop&q=80',
|
||||
duration: '45h 00min',
|
||||
lessonsCount: 154,
|
||||
badge: 'Destacado',
|
||||
instructor: mockInstructors.diego,
|
||||
updatedAt: 'Febrero 2026',
|
||||
language: 'Español',
|
||||
whatYouWillLearn: [
|
||||
'Infraestructura como código con Terraform en AWS',
|
||||
'Containerización de microservicios con Docker y Docker Compose',
|
||||
'Orquestación y escalabilidad con clústeres de Kubernetes (EKS)',
|
||||
'Pipelines CI/CD robustos con GitHub Actions',
|
||||
'Monitoreo y observabilidad con Prometheus y Grafana'
|
||||
],
|
||||
requirements: [
|
||||
'Conocimientos previos de Linux y desarrollo backend'
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: 'm1',
|
||||
title: 'Módulo 1: Docker y Microservicios desde Cero',
|
||||
lessons: [
|
||||
{ id: 'l1', title: 'Imágenes, Contenedores y Volúmenes', duration: '22:00', isFreePreview: true },
|
||||
{ id: 'l2', title: 'Optimización de Dockerfiles multi-stage', duration: '26:30' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 'r7',
|
||||
userName: 'Ignacio Ortiz',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=100&auto=format&fit=crop&q=80',
|
||||
rating: 5,
|
||||
date: 'Hace 1 mes',
|
||||
comment: 'Imprescindible para cualquier desarrollador que quiera dar el salto a Senior DevOps.'
|
||||
}
|
||||
],
|
||||
isPublished: true,
|
||||
salesCount: 510,
|
||||
totalRevenue: 16824.90
|
||||
}
|
||||
];
|
||||
|
||||
export const mockCategories = [
|
||||
{ id: 'all', name: 'Todos los cursos', icon: 'FiGrid', count: 120 },
|
||||
{ id: 'dev', name: 'Desarrollo Web', icon: 'FiCode', count: 48 },
|
||||
{ id: 'ai', name: 'Inteligencia Artificial', icon: 'FiCpu', count: 26 },
|
||||
{ id: 'design', name: 'Diseño UI/UX', icon: 'FiLayout', count: 22 },
|
||||
{ id: 'security', name: 'Ciberseguridad', icon: 'FiShield', count: 14 },
|
||||
{ id: 'marketing', name: 'Marketing Digital', icon: 'FiTrendingUp', count: 18 },
|
||||
{ id: 'business', name: 'Negocios & Finanzas', icon: 'FiDollarSign', count: 15 }
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom modern scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
export type CourseLevel = 'Principiante' | 'Intermedio' | 'Avanzado' | 'Todos los niveles';
|
||||
|
||||
export interface Lesson {
|
||||
id: string;
|
||||
title: string;
|
||||
duration: string;
|
||||
isFreePreview?: boolean;
|
||||
}
|
||||
|
||||
export interface Module {
|
||||
id: string;
|
||||
title: string;
|
||||
lessons: Lesson[];
|
||||
}
|
||||
|
||||
export interface Instructor {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
avatar: string;
|
||||
rating: number;
|
||||
totalStudents: number;
|
||||
totalCourses: number;
|
||||
bio: string;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
id: string;
|
||||
userName: string;
|
||||
userAvatar: string;
|
||||
rating: number;
|
||||
date: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
category: string;
|
||||
level: CourseLevel;
|
||||
price: number;
|
||||
originalPrice: number;
|
||||
rating: number;
|
||||
reviewsCount: number;
|
||||
studentsCount: number;
|
||||
thumbnail: string;
|
||||
duration: string; // e.g. "24h 30min"
|
||||
lessonsCount: number;
|
||||
badge?: 'Bestseller' | 'Nuevo' | 'Oferta' | 'Destacado';
|
||||
instructor: Instructor;
|
||||
updatedAt: string;
|
||||
language: string;
|
||||
whatYouWillLearn: string[];
|
||||
requirements: string[];
|
||||
modules: Module[];
|
||||
reviews: Review[];
|
||||
isPublished?: boolean;
|
||||
salesCount?: number;
|
||||
totalRevenue?: number;
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
course: Course;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
export type ActiveTabFilter = 'all' | 'popular' | 'top-rated' | 'offers' | 'new';
|
||||
export type ViewMode = 'buyer' | 'seller';
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"allowArbitraryExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"module": "nodenext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
||||
Reference in new issue
Block a user