fix al la posicion de donde se encuentra el carrito de compra y animacion al carrito al hacer hover
This commit is contained in:
1 parent
2ae7091a3a
commit
b76ae84bf6
6 files changed
+347
-96
No files matched your search
+21
-3
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Routes, Route } from 'react-router';
|
import { Routes, Route } from 'react-router';
|
||||||
import type { Course, CartItem } from './types/course';
|
import type { Course, CartItem, User } from './types/course';
|
||||||
import { mockCourses } from './data/mockCourses';
|
import { mockCourses } from './data/mockCourses';
|
||||||
import { Navbar } from './components/layout/Navbar';
|
import { Navbar } from './components/layout/Navbar';
|
||||||
import { Footer } from './components/layout/Footer';
|
import { Footer } from './components/layout/Footer';
|
||||||
@@ -27,6 +27,22 @@ export function App() {
|
|||||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Auth State
|
||||||
|
const [currentUser, setCurrentUser] = useState<User | null>(() => {
|
||||||
|
const savedUser = localStorage.getItem('rycourses_user');
|
||||||
|
return savedUser ? JSON.parse(savedUser) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLogin = (user: User) => {
|
||||||
|
setCurrentUser(user);
|
||||||
|
localStorage.setItem('rycourses_user', JSON.stringify(user));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setCurrentUser(null);
|
||||||
|
localStorage.removeItem('rycourses_user');
|
||||||
|
};
|
||||||
|
|
||||||
// Toast feedback state
|
// Toast feedback state
|
||||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -88,6 +104,8 @@ export function App() {
|
|||||||
|
|
||||||
{/* Main Navigation Bar */}
|
{/* Main Navigation Bar */}
|
||||||
<Navbar
|
<Navbar
|
||||||
|
currentUser={currentUser}
|
||||||
|
onLogout={handleLogout}
|
||||||
cartCount={cartItems.length}
|
cartCount={cartItems.length}
|
||||||
onOpenCart={() => setIsCartOpen(true)}
|
onOpenCart={() => setIsCartOpen(true)}
|
||||||
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
|
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
|
||||||
@@ -171,11 +189,11 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={<LoginView onShowToast={showToast} />}
|
element={<LoginView onShowToast={showToast} onLogin={handleLogin} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/register"
|
path="/register"
|
||||||
element={<RegisterView onShowToast={showToast} />}
|
element={<RegisterView onShowToast={showToast} onLogin={handleLogin} />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
import { useNavigate, useLocation } from 'react-router';
|
||||||
import {
|
import {
|
||||||
FiSearch,
|
FiSearch,
|
||||||
FiShoppingCart,
|
FiShoppingCart,
|
||||||
FiMenu,
|
FiMenu,
|
||||||
FiX,
|
FiX,
|
||||||
FiPlusCircle,
|
FiPlusCircle,
|
||||||
FiHome,
|
FiHome,
|
||||||
FiGrid,
|
FiGrid,
|
||||||
FiUser,
|
FiUser,
|
||||||
FiShield,
|
FiShield,
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { FaChalkboardTeacher } from 'react-icons/fa';
|
import { FaChalkboardTeacher, FaSignOutAlt } from 'react-icons/fa';
|
||||||
import { BrandLogo } from '../common/BrandLogo';
|
import { BrandLogo } from '../common/BrandLogo';
|
||||||
|
import type { User } from '../../types/course';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
|
currentUser: User | null;
|
||||||
|
onLogout: () => void;
|
||||||
cartCount: number;
|
cartCount: number;
|
||||||
onOpenCart: () => void;
|
onOpenCart: () => void;
|
||||||
onOpenCreateCourseModal?: () => void;
|
onOpenCreateCourseModal?: () => void;
|
||||||
@@ -23,6 +26,8 @@ interface NavbarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Navbar: React.FC<NavbarProps> = ({
|
export const Navbar: React.FC<NavbarProps> = ({
|
||||||
|
currentUser,
|
||||||
|
onLogout,
|
||||||
cartCount,
|
cartCount,
|
||||||
onOpenCart,
|
onOpenCart,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -31,6 +36,31 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [cartHovered, setCartHovered] = useState(false);
|
||||||
|
|
||||||
|
// Inject keyframe animation styles inline (no external CSS needed)
|
||||||
|
useEffect(() => {
|
||||||
|
const styleId = 'cart-run-keyframes';
|
||||||
|
if (!document.getElementById(styleId)) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleId;
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes cart-run {
|
||||||
|
0% { transform: rotate(0deg) translateY(0px); }
|
||||||
|
25% { transform: rotate(-8deg) translateY(-1px); }
|
||||||
|
50% { transform: rotate(0deg) translateY(0px); }
|
||||||
|
75% { transform: rotate(-5deg) translateY(-1px); }
|
||||||
|
100% { transform: rotate(0deg) translateY(0px); }
|
||||||
|
}
|
||||||
|
@keyframes speed-streak {
|
||||||
|
0% { opacity: 0; transform: translateX(2px) scaleX(0.1); }
|
||||||
|
40% { opacity: 0.6; transform: translateX(-3px) scaleX(1); }
|
||||||
|
100% { opacity: 0; transform: translateX(-6px) scaleX(0); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const currentPath = location.pathname;
|
const currentPath = location.pathname;
|
||||||
const isSellerMode = currentPath === '/seller' || currentPath.startsWith('/seller/');
|
const isSellerMode = currentPath === '/seller' || currentPath.startsWith('/seller/');
|
||||||
@@ -46,7 +76,7 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/90 backdrop-blur-xl transition-all">
|
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/90 backdrop-blur-xl transition-all">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<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">
|
<div className="flex items-center justify-between h-20 gap-4">
|
||||||
|
|
||||||
{/* 1. Left Brand & Main Links */}
|
{/* 1. Left Brand & Main Links */}
|
||||||
<div className="flex items-center gap-6 shrink-0">
|
<div className="flex items-center gap-6 shrink-0">
|
||||||
<div onClick={() => navigate('/')}>
|
<div onClick={() => navigate('/')}>
|
||||||
@@ -57,11 +87,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<nav className="hidden lg:flex items-center gap-1">
|
<nav className="hidden lg:flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/'
|
||||||
currentPath === '/'
|
|
||||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FiHome />
|
<FiHome />
|
||||||
<span>Inicio</span>
|
<span>Inicio</span>
|
||||||
@@ -69,11 +98,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/courses')}
|
onClick={() => navigate('/courses')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/courses'
|
||||||
currentPath === '/courses'
|
|
||||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FiGrid />
|
<FiGrid />
|
||||||
<span>Catálogo</span>
|
<span>Catálogo</span>
|
||||||
@@ -81,11 +109,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin')}
|
onClick={() => navigate('/admin')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/admin'
|
||||||
currentPath === '/admin'
|
|
||||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FiShield />
|
<FiShield />
|
||||||
<span>Admin</span>
|
<span>Admin</span>
|
||||||
@@ -123,11 +150,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<div className="bg-slate-900 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
|
<div className="bg-slate-900 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/courses')}
|
onClick={() => navigate('/courses')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${!isSellerMode
|
||||||
!isSellerMode
|
|
||||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-md shadow-cyan-500/20'
|
? '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'
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
title="Modo Estudiante - Explorar y Comprar cursos"
|
title="Modo Estudiante - Explorar y Comprar cursos"
|
||||||
>
|
>
|
||||||
<FiGrid className="text-xs" />
|
<FiGrid className="text-xs" />
|
||||||
@@ -136,11 +162,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/seller')}
|
onClick={() => navigate('/seller')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${isSellerMode
|
||||||
isSellerMode
|
|
||||||
? 'bg-gradient-to-r from-indigo-600 to-violet-500 text-white shadow-md shadow-violet-500/20'
|
? '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'
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
title="Modo Instructor - Vender y Gestionar cursos"
|
title="Modo Instructor - Vender y Gestionar cursos"
|
||||||
>
|
>
|
||||||
<FaChalkboardTeacher className="text-xs" />
|
<FaChalkboardTeacher className="text-xs" />
|
||||||
@@ -159,54 +184,143 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cart button (Buyer mode) */}
|
{/* Login & Register Buttons OR User Profile */}
|
||||||
|
{!currentUser ? (
|
||||||
|
<div className="hidden xl:flex items-center gap-2 pl-2 border-l border-slate-800">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
className="px-3 py-1.5 text-xs font-semibold text-slate-300 hover:text-white transition"
|
||||||
|
>
|
||||||
|
Iniciar Sesión
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/register')}
|
||||||
|
className="px-3.5 py-1.5 rounded-xl bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 font-semibold text-xs border border-cyan-500/30 transition"
|
||||||
|
>
|
||||||
|
Registrarse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 pl-2 border-l border-slate-800 relative group">
|
||||||
|
<div className="relative cursor-pointer flex items-center gap-2" title="Mi Cuenta">
|
||||||
|
<div className="hidden sm:block text-right">
|
||||||
|
<p className="text-[11px] font-bold text-slate-200 leading-none">{currentUser.name}</p>
|
||||||
|
<p className="text-[9px] text-cyan-400 font-medium capitalize mt-0.5">{currentUser.role === 'buyer' ? 'Estudiante' : 'Instructor'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
{currentUser.avatar ? (
|
||||||
|
<img
|
||||||
|
src={currentUser.avatar}
|
||||||
|
alt={currentUser.name}
|
||||||
|
className="w-9 h-9 rounded-full object-cover ring-2 ring-indigo-500/50 group-hover:ring-cyan-400 transition"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-9 h-9 rounded-full bg-slate-800 flex items-center justify-center text-sm font-bold text-slate-300 ring-2 ring-indigo-500/50 group-hover:ring-cyan-400 transition">
|
||||||
|
{currentUser.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
<div className="absolute top-full right-0 mt-2 w-48 bg-slate-900 border border-slate-800 rounded-xl shadow-2xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right scale-95 group-hover:scale-100 z-50">
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
<button onClick={onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition">
|
||||||
|
<FaSignOutAlt />
|
||||||
|
<span>Cerrar Sesión</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cart button (Buyer mode) - Fixed Far Right, inline animation */}
|
||||||
{!isSellerMode && (
|
{!isSellerMode && (
|
||||||
<button
|
<button
|
||||||
onClick={onOpenCart}
|
onClick={onOpenCart}
|
||||||
className="relative p-2.5 text-slate-300 hover:text-white bg-slate-900 hover:bg-slate-800 border border-slate-700/80 rounded-xl transition"
|
onMouseEnter={() => setCartHovered(true)}
|
||||||
|
onMouseLeave={() => setCartHovered(false)}
|
||||||
|
className="group fixed top-4 right-4 z-50 p-3 text-slate-300 hover:text-white hover:bg-slate-800 rounded-xl transition-all shadow-lg shadow-black/30 backdrop-blur-sm overflow-hidden"
|
||||||
aria-label="Carrito de compras"
|
aria-label="Carrito de compras"
|
||||||
>
|
>
|
||||||
<FiShoppingCart className="text-base" />
|
{/* Cart icon with inline running animation */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
animation: cartHovered ? 'cart-run 0.45s ease-in-out infinite' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiShoppingCart style={{ fontSize: '1.2rem' }} />
|
||||||
|
|
||||||
|
{/* Speed streaks — subtle, contained inside icon area */}
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '25%',
|
||||||
|
left: '-4px',
|
||||||
|
width: '5px',
|
||||||
|
height: '1.5px',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
background: 'rgb(34 211 238)',
|
||||||
|
animation: cartHovered ? 'speed-streak 0.5s linear infinite' : 'none',
|
||||||
|
opacity: 0,
|
||||||
|
}} />
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '55%',
|
||||||
|
left: '-2px',
|
||||||
|
width: '4px',
|
||||||
|
height: '1.5px',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
background: 'rgb(34 211 238)',
|
||||||
|
animation: cartHovered ? 'speed-streak 0.5s linear 0.18s infinite' : 'none',
|
||||||
|
opacity: 0,
|
||||||
|
}} />
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '15%',
|
||||||
|
left: '-5px',
|
||||||
|
width: '6px',
|
||||||
|
height: '1.5px',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
background: 'rgb(99 102 241)',
|
||||||
|
animation: cartHovered ? 'speed-streak 0.5s linear 0.35s infinite' : 'none',
|
||||||
|
opacity: 0,
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badge counter */}
|
||||||
{cartCount > 0 && (
|
{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 shadow-md">
|
<span
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: 'linear-gradient(135deg, #f43f5e, #ec4899)',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: '9999px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(244,63,94,0.5)',
|
||||||
|
zIndex: 20,
|
||||||
|
transform: cartHovered ? 'scale(1.2)' : 'scale(1)',
|
||||||
|
transition: 'transform 0.15s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{cartCount}
|
{cartCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Login & Register Buttons */}
|
|
||||||
<div className="hidden xl:flex items-center gap-2 pl-2 border-l border-slate-800">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/login')}
|
|
||||||
className="px-3 py-1.5 text-xs font-semibold text-slate-300 hover:text-white transition"
|
|
||||||
>
|
|
||||||
Iniciar Sesión
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/register')}
|
|
||||||
className="px-3.5 py-1.5 rounded-xl bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 font-semibold text-xs border border-cyan-500/30 transition"
|
|
||||||
>
|
|
||||||
Registrarse
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Profile Avatar */}
|
|
||||||
<div className="flex items-center gap-2 pl-2 border-l border-slate-800">
|
|
||||||
<div
|
|
||||||
onClick={() => navigate('/login')}
|
|
||||||
className="relative cursor-pointer group"
|
|
||||||
title="Mi Cuenta"
|
|
||||||
>
|
|
||||||
<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 group-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 */}
|
{/* Mobile menu trigger */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
@@ -234,43 +348,65 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
|
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
|
||||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${currentPath === '/' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||||
currentPath === '/' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<FiHome /> Inicio
|
<FiHome /> Inicio
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
|
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
|
||||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${currentPath === '/courses' ? 'bg-cyan-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||||
currentPath === '/courses' ? 'bg-cyan-600 text-white' : 'bg-slate-800 text-slate-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<FiGrid /> Catálogo
|
<FiGrid /> Catálogo
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/seller'); setMobileMenuOpen(false); }}
|
onClick={() => { navigate('/seller'); setMobileMenuOpen(false); }}
|
||||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${currentPath === '/seller' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||||
currentPath === '/seller' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<FaChalkboardTeacher /> Vender
|
<FaChalkboardTeacher /> Vender
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800">
|
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800">
|
||||||
<button
|
{!currentUser ? (
|
||||||
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
|
<>
|
||||||
className="w-full py-2.5 bg-slate-800 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
<button
|
||||||
>
|
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
|
||||||
<FiUser /> Iniciar Sesión
|
className="w-full py-2.5 bg-slate-800 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
||||||
</button>
|
>
|
||||||
<button
|
<FiUser /> Iniciar Sesión
|
||||||
onClick={() => { navigate('/register'); setMobileMenuOpen(false); }}
|
</button>
|
||||||
className="w-full py-2.5 bg-cyan-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
<button
|
||||||
>
|
onClick={() => { navigate('/register'); setMobileMenuOpen(false); }}
|
||||||
Registrarse
|
className="w-full py-2.5 bg-cyan-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
||||||
</button>
|
>
|
||||||
|
Registrarse
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="w-full py-2.5 px-3 bg-slate-800 rounded-lg flex items-center gap-3">
|
||||||
|
{currentUser.avatar ? (
|
||||||
|
<img src={currentUser.avatar} alt="Avatar" className="w-6 h-6 rounded-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<div className="w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center text-[10px] font-bold text-white">
|
||||||
|
{currentUser.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-[10px] font-bold text-white truncate max-w-[100px]">{currentUser.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { onLogout(); setMobileMenuOpen(false); }}
|
||||||
|
className="w-full py-2.5 bg-rose-500/10 text-rose-400 font-semibold rounded-lg text-xs flex items-center justify-center gap-2 border border-rose-500/20"
|
||||||
|
>
|
||||||
|
<FaSignOutAlt /> Cerrar Sesión
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSellerMode && (
|
{isSellerMode && (
|
||||||
|
|||||||
@@ -24,4 +24,76 @@
|
|||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #475569;
|
background: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cart Running Animation */
|
||||||
|
@keyframes cart-run {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0) rotate(0deg) translateY(0);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: translateX(3px) rotate(-10deg) translateY(-2px);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateX(1px) rotate(-5deg) translateY(1px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateX(4px) rotate(-12deg) translateY(-2px);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateX(2px) rotate(-8deg) translateY(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0) rotate(0deg) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-cart-run {
|
||||||
|
animation: cart-run 0.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed Streaks for Cart */
|
||||||
|
@keyframes speed-streak {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(0) scaleX(0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateX(-8px) scaleX(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-16px) scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-line {
|
||||||
|
position: absolute;
|
||||||
|
height: 2px;
|
||||||
|
background: theme('colors.cyan.400');
|
||||||
|
border-radius: 9999px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-hover\\:animate-cart-run:hover .speed-line-1 {
|
||||||
|
animation: speed-streak 0.6s infinite linear;
|
||||||
|
top: 30%;
|
||||||
|
right: 110%;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-hover\\:animate-cart-run:hover .speed-line-2 {
|
||||||
|
animation: speed-streak 0.5s infinite linear 0.2s;
|
||||||
|
top: 60%;
|
||||||
|
right: 100%;
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-hover\\:animate-cart-run:hover .speed-line-3 {
|
||||||
|
animation: speed-streak 0.7s infinite linear 0.4s;
|
||||||
|
bottom: 10%;
|
||||||
|
right: 120%;
|
||||||
|
width: 14px;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
export type CourseLevel = 'Principiante' | 'Intermedio' | 'Avanzado' | 'Todos los niveles';
|
export type CourseLevel = 'Principiante' | 'Intermedio' | 'Avanzado' | 'Todos los niveles';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: 'buyer' | 'seller' | 'admin';
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Lesson {
|
export interface Lesson {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
+19
-11
@@ -3,22 +3,36 @@ import { useNavigate, Link } from 'react-router';
|
|||||||
import { BrandLogo } from '../components/common/BrandLogo';
|
import { BrandLogo } from '../components/common/BrandLogo';
|
||||||
import { FiMail, FiLock, FiArrowRight } from 'react-icons/fi';
|
import { FiMail, FiLock, FiArrowRight } from 'react-icons/fi';
|
||||||
import { FaGoogle, FaGithub } from 'react-icons/fa';
|
import { FaGoogle, FaGithub } from 'react-icons/fa';
|
||||||
|
import type { User } from '../types/course';
|
||||||
|
|
||||||
interface LoginViewProps {
|
interface LoginViewProps {
|
||||||
onShowToast: (msg: string) => void;
|
onShowToast: (msg: string) => void;
|
||||||
|
onLogin: (user: User) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginView: React.FC<LoginViewProps> = ({ onShowToast }) => {
|
export const LoginView: React.FC<LoginViewProps> = ({ onShowToast, onLogin }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [rememberMe, setRememberMe] = useState(true);
|
const [rememberMe, setRememberMe] = useState(true);
|
||||||
|
|
||||||
|
const handleMockLogin = (name: string, role: 'buyer' | 'seller', avatarUrl?: string) => {
|
||||||
|
const mockUser: User = {
|
||||||
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
|
name,
|
||||||
|
email: email || '[email protected]',
|
||||||
|
role,
|
||||||
|
avatar: avatarUrl,
|
||||||
|
};
|
||||||
|
onLogin(mockUser);
|
||||||
|
onShowToast(`¡Bienvenido de nuevo a RyCourses, ${name}!`);
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (email && password) {
|
if (email && password) {
|
||||||
onShowToast('¡Bienvenido de nuevo a RyCourses!');
|
handleMockLogin('Usuario Demo', 'buyer', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80');
|
||||||
navigate('/');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,10 +61,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onShowToast }) => {
|
|||||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => handleMockLogin('Google User', 'buyer')}
|
||||||
onShowToast('Iniciando sesión con Google...');
|
|
||||||
navigate('/');
|
|
||||||
}}
|
|
||||||
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700/80 border border-slate-700 text-xs font-semibold text-slate-200 transition"
|
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700/80 border border-slate-700 text-xs font-semibold text-slate-200 transition"
|
||||||
>
|
>
|
||||||
<FaGoogle className="text-red-400" />
|
<FaGoogle className="text-red-400" />
|
||||||
@@ -59,10 +70,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onShowToast }) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => handleMockLogin('GitHub User', 'buyer')}
|
||||||
onShowToast('Iniciando sesión con GitHub...');
|
|
||||||
navigate('/');
|
|
||||||
}}
|
|
||||||
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700/80 border border-slate-700 text-xs font-semibold text-slate-200 transition"
|
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700/80 border border-slate-700 text-xs font-semibold text-slate-200 transition"
|
||||||
>
|
>
|
||||||
<FaGithub className="text-white" />
|
<FaGithub className="text-white" />
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { useNavigate, Link } from 'react-router';
|
|||||||
import { BrandLogo } from '../components/common/BrandLogo';
|
import { BrandLogo } from '../components/common/BrandLogo';
|
||||||
import { FiMail, FiLock, FiUser, FiArrowRight, FiCheckCircle } from 'react-icons/fi';
|
import { FiMail, FiLock, FiUser, FiArrowRight, FiCheckCircle } from 'react-icons/fi';
|
||||||
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
||||||
|
import type { User } from '../types/course';
|
||||||
|
|
||||||
interface RegisterViewProps {
|
interface RegisterViewProps {
|
||||||
onShowToast: (msg: string) => void;
|
onShowToast: (msg: string) => void;
|
||||||
|
onLogin: (user: User) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast }) => {
|
export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast, onLogin }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@@ -18,6 +20,13 @@ export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast }) => {
|
|||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (name && email && password) {
|
if (name && email && password) {
|
||||||
|
const newUser: User = {
|
||||||
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
role: accountType,
|
||||||
|
};
|
||||||
|
onLogin(newUser);
|
||||||
onShowToast(`¡Cuenta de ${accountType === 'buyer' ? 'Estudiante' : 'Instructor'} creada con éxito!`);
|
onShowToast(`¡Cuenta de ${accountType === 'buyer' ? 'Estudiante' : 'Instructor'} creada con éxito!`);
|
||||||
navigate(accountType === 'seller' ? '/seller' : '/courses');
|
navigate(accountType === 'seller' ? '/seller' : '/courses');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user