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 { 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 { Navbar } from './components/layout/Navbar';
|
||||
import { Footer } from './components/layout/Footer';
|
||||
@@ -27,6 +27,22 @@ export function App() {
|
||||
const [isCartOpen, setIsCartOpen] = 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
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
@@ -88,6 +104,8 @@ export function App() {
|
||||
|
||||
{/* Main Navigation Bar */}
|
||||
<Navbar
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
cartCount={cartItems.length}
|
||||
onOpenCart={() => setIsCartOpen(true)}
|
||||
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
|
||||
@@ -171,11 +189,11 @@ export function App() {
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={<LoginView onShowToast={showToast} />}
|
||||
element={<LoginView onShowToast={showToast} onLogin={handleLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/register"
|
||||
element={<RegisterView onShowToast={showToast} />}
|
||||
element={<RegisterView onShowToast={showToast} onLogin={handleLogin} />}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import {
|
||||
FiSearch,
|
||||
FiShoppingCart,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiPlusCircle,
|
||||
import {
|
||||
FiSearch,
|
||||
FiShoppingCart,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiPlusCircle,
|
||||
FiHome,
|
||||
FiGrid,
|
||||
FiUser,
|
||||
FiShield,
|
||||
} from 'react-icons/fi';
|
||||
import { FaChalkboardTeacher } from 'react-icons/fa';
|
||||
import { FaChalkboardTeacher, FaSignOutAlt } from 'react-icons/fa';
|
||||
import { BrandLogo } from '../common/BrandLogo';
|
||||
import type { User } from '../../types/course';
|
||||
|
||||
interface NavbarProps {
|
||||
currentUser: User | null;
|
||||
onLogout: () => void;
|
||||
cartCount: number;
|
||||
onOpenCart: () => void;
|
||||
onOpenCreateCourseModal?: () => void;
|
||||
@@ -23,6 +26,8 @@ interface NavbarProps {
|
||||
}
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({
|
||||
currentUser,
|
||||
onLogout,
|
||||
cartCount,
|
||||
onOpenCart,
|
||||
searchQuery,
|
||||
@@ -31,6 +36,31 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
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 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">
|
||||
<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">
|
||||
|
||||
|
||||
{/* 1. Left Brand & Main Links */}
|
||||
<div className="flex items-center gap-6 shrink-0">
|
||||
<div onClick={() => navigate('/')}>
|
||||
@@ -57,11 +87,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
<nav className="hidden lg:flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||
currentPath === '/'
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/'
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<FiHome />
|
||||
<span>Inicio</span>
|
||||
@@ -69,11 +98,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/courses')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||
currentPath === '/courses'
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/courses'
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<FiGrid />
|
||||
<span>Catálogo</span>
|
||||
@@ -81,11 +109,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/admin')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||
currentPath === '/admin'
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/admin'
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<FiShield />
|
||||
<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">
|
||||
<button
|
||||
onClick={() => navigate('/courses')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
!isSellerMode
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${!isSellerMode
|
||||
? '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"
|
||||
>
|
||||
<FiGrid className="text-xs" />
|
||||
@@ -136,11 +162,10 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/seller')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
isSellerMode
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${isSellerMode
|
||||
? '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-xs" />
|
||||
@@ -159,54 +184,143 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
</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 && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<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 && (
|
||||
<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}
|
||||
</span>
|
||||
)}
|
||||
</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 */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
@@ -234,43 +348,65 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
|
||||
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'
|
||||
}`}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<FiHome /> Inicio
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
|
||||
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'
|
||||
}`}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<FiGrid /> Catálogo
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/seller'); setMobileMenuOpen(false); }}
|
||||
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'
|
||||
}`}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<FaChalkboardTeacher /> Vender
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<FiUser /> Iniciar Sesión
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/register'); setMobileMenuOpen(false); }}
|
||||
className="w-full py-2.5 bg-cyan-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
||||
>
|
||||
Registrarse
|
||||
</button>
|
||||
{!currentUser ? (
|
||||
<>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<FiUser /> Iniciar Sesión
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/register'); setMobileMenuOpen(false); }}
|
||||
className="w-full py-2.5 bg-cyan-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
||||
>
|
||||
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>
|
||||
|
||||
{isSellerMode && (
|
||||
|
||||
@@ -24,4 +24,76 @@
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
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 interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'buyer' | 'seller' | 'admin';
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
+19
-11
@@ -3,22 +3,36 @@ import { useNavigate, Link } from 'react-router';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import { FiMail, FiLock, FiArrowRight } from 'react-icons/fi';
|
||||
import { FaGoogle, FaGithub } from 'react-icons/fa';
|
||||
import type { User } from '../types/course';
|
||||
|
||||
interface LoginViewProps {
|
||||
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 [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
if (email && password) {
|
||||
onShowToast('¡Bienvenido de nuevo a RyCourses!');
|
||||
navigate('/');
|
||||
handleMockLogin('Usuario Demo', 'buyer', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,10 +61,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onShowToast }) => {
|
||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onShowToast('Iniciando sesión con Google...');
|
||||
navigate('/');
|
||||
}}
|
||||
onClick={() => handleMockLogin('Google User', 'buyer')}
|
||||
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" />
|
||||
@@ -59,10 +70,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onShowToast }) => {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onShowToast('Iniciando sesión con GitHub...');
|
||||
navigate('/');
|
||||
}}
|
||||
onClick={() => handleMockLogin('GitHub User', 'buyer')}
|
||||
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" />
|
||||
|
||||
@@ -3,12 +3,14 @@ import { useNavigate, Link } from 'react-router';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import { FiMail, FiLock, FiUser, FiArrowRight, FiCheckCircle } from 'react-icons/fi';
|
||||
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
||||
import type { User } from '../types/course';
|
||||
|
||||
interface RegisterViewProps {
|
||||
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 [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -18,6 +20,13 @@ export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast }) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
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!`);
|
||||
navigate(accountType === 'seller' ? '/seller' : '/courses');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user