aboutsummaryrefslogtreecommitdiffstats
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/package-lock.json40
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/src/app/(main)/layout.tsx246
-rw-r--r--frontend/src/components/shared/PageTransition.tsx55
-rw-r--r--frontend/src/components/shared/ThemeToggle.tsx53
-rw-r--r--frontend/src/components/ui/button.tsx31
-rw-r--r--frontend/src/components/ui/card.tsx23
-rw-r--r--frontend/src/components/ui/input.tsx34
8 files changed, 425 insertions, 58 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b46cf51..5eaa75f 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@tanstack/react-query": "^5.74.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "framer-motion": "^11.18.2",
"lucide-react": "^0.503.0",
"next": "15.3.1",
"react": "^19.0.0",
@@ -3353,6 +3354,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/framer-motion": {
+ "version": "11.18.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
+ "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
+ "dependencies": {
+ "motion-dom": "^11.18.1",
+ "motion-utils": "^11.18.1",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4495,6 +4522,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
+ "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
+ "dependencies": {
+ "motion-utils": "^11.18.1"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
+ "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 1b8aac7..eb04e83 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,7 @@
"@tanstack/react-query": "^5.74.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "framer-motion": "^11.18.2",
"lucide-react": "^0.503.0",
"next": "15.3.1",
"react": "^19.0.0",
diff --git a/frontend/src/app/(main)/layout.tsx b/frontend/src/app/(main)/layout.tsx
index 846a118..5902dd6 100644
--- a/frontend/src/app/(main)/layout.tsx
+++ b/frontend/src/app/(main)/layout.tsx
@@ -6,6 +6,18 @@ import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { authApi, userApi } from '@/lib/api';
import { ThemeToggle } from '@/components/shared/ThemeToggle';
+import { PageTransition } from '@/components/shared/PageTransition';
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ LayoutDashboardIcon,
+ CoinsIcon,
+ TargetIcon,
+ SettingsIcon,
+ LogOutIcon,
+ MenuIcon
+} from 'lucide-react';
+import { usePathname } from 'next/navigation';
export default function MainLayout({
@@ -14,8 +26,11 @@ export default function MainLayout({
children: React.ReactNode;
}>) {
const router = useRouter();
+ const pathname = usePathname();
const [userName, setUserName] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
useEffect(() => {
// Check if user is authenticated
@@ -35,58 +50,251 @@ export default function MainLayout({
// If error fetching profile, redirect to login
router.push('/login');
});
+
+ // Load sidebar state from localStorage
+ const savedSidebarState = localStorage.getItem('sidebarCollapsed');
+ if (savedSidebarState !== null) {
+ setIsSidebarCollapsed(savedSidebarState === 'true');
+ }
}, [router]);
+ // Close mobile menu when changing routes
+ useEffect(() => {
+ setIsMobileMenuOpen(false);
+ }, [pathname]);
+
const handleLogout = () => {
authApi.logout();
};
+ const toggleSidebar = () => {
+ const newState = !isSidebarCollapsed;
+ setIsSidebarCollapsed(newState);
+ localStorage.setItem('sidebarCollapsed', newState.toString());
+ };
+
+ const toggleMobileMenu = () => {
+ setIsMobileMenuOpen(!isMobileMenuOpen);
+ };
+
if (isLoading) {
- return <div className="flex h-screen items-center justify-center">Loading...</div>;
+ return (
+ <div className="flex h-screen items-center justify-center bg-background">
+ <div className="flex flex-col items-center space-y-4">
+ <div className="relative h-12 w-12">
+ <div className="absolute top-0 h-full w-full rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
+ </div>
+ <p className="text-sm font-medium text-muted-foreground animate-pulse">Loading...</p>
+ </div>
+ </div>
+ );
}
return (
- <div className="min-h-screen bg-background">
+ <div className="min-h-screen bg-background flex flex-col">
{/* Top Navigation */}
- <header className="border-b">
- <div className="w-full flex h-16 items-center px-4">
+ <header className="border-b shadow-sm backdrop-blur-sm bg-background/90 sticky top-0 z-50">
+ <div className="w-full flex h-16 items-center px-4 md:px-6">
<div className="flex items-center gap-4">
- <div className="font-semibold text-xl">Finance Management</div>
+ {/* Mobile menu button - only visible on small screens */}
+ <button
+ className="md:hidden rounded-full p-2 hover:bg-muted transition-colors"
+ onClick={toggleMobileMenu}
+ aria-label="Toggle mobile menu"
+ >
+ <MenuIcon size={20} />
+ </button>
+ <div className="font-semibold text-xl">
+ <span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/70">
+ Finance Management
+ </span>
+ </div>
</div>
<div className="flex items-center ms-auto gap-4">
- <span className='text-sm font-medium'>Welcome!, {userName}</span>
+ <div className="hidden md:flex items-center gap-1 bg-muted/50 px-3 py-1.5 rounded-full">
+ <span className="relative flex h-2 w-2">
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75"></span>
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
+ </span>
+ <span className='text-sm font-medium ml-1.5 transition-all duration-300'>
+ {userName}
+ </span>
+ </div>
<ThemeToggle />
- <Button variant="ghost" size="sm" onClick={handleLogout}>
- Logout
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={handleLogout}
+ className="hover:bg-destructive/10 transition-colors duration-300"
+ aria-label="Logout"
+ >
+ <LogOutIcon size={18} className="text-destructive/90 transition-transform hover:scale-110 duration-300" />
</Button>
</div>
</div>
</header>
- {/* Main Content */}
- <div className="container flex flex-row">
- {/* Sidebar */}
- <aside className="w-64 pt-8 pr-6">
- <nav className="space-y-2">
- <Link href="/dashboard" className="block p-2 rounded-md hover:bg-muted">
+ {/* Mobile Menu - outside the normal flow and only visible when toggled */}
+ <div
+ className={`
+ md:hidden fixed inset-0 z-40 bg-background/95 backdrop-blur-sm transition-all duration-300
+ ${isMobileMenuOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
+ `}
+ >
+ <div className="pt-20 px-4">
+ <nav className="space-y-4">
+ <Link
+ href="/dashboard"
+ className={`
+ flex items-center p-3 rounded-lg transition-colors
+ ${pathname === '/dashboard' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}
+ `}
+ >
+ <LayoutDashboardIcon size={20} className="mr-3" />
Dashboard
</Link>
- <Link href="/loans" className="block p-2 rounded-md hover:bg-muted">
+ <Link
+ href="/loans"
+ className={`
+ flex items-center p-3 rounded-lg transition-colors
+ ${pathname === '/loans' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}
+ `}
+ >
+ <CoinsIcon size={20} className="mr-3" />
Loans
</Link>
- <Link href="/goals" className="block p-2 rounded-md hover:bg-muted">
+ <Link
+ href="/goals"
+ className={`
+ flex items-center p-3 rounded-lg transition-colors
+ ${pathname === '/goals' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}
+ `}
+ >
+ <TargetIcon size={20} className="mr-3" />
Goals
</Link>
- <Link href="/settings" className="block p-2 rounded-md hover:bg-muted">
+ <Link
+ href="/settings"
+ className={`
+ flex items-center p-3 rounded-lg transition-colors
+ ${pathname === '/settings' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}
+ `}
+ >
+ <SettingsIcon size={20} className="mr-3" />
Settings
</Link>
</nav>
+ <div className="mt-8 border-t pt-4">
+ <Button
+ variant="outline"
+ className="w-full justify-start text-destructive hover:text-destructive hover:bg-destructive/10"
+ onClick={handleLogout}
+ >
+ <LogOutIcon size={18} className="mr-2" />
+ Logout
+ </Button>
+ </div>
+ </div>
+ </div>
+
+ {/* Main Content */}
+ <div className="flex flex-1 h-[calc(100vh-4rem)]">
+ {/* Sidebar - hidden on mobile */}
+ <aside
+ className={`
+ hidden md:block relative h-full transition-all duration-300 ease-in-out border-r
+ ${isSidebarCollapsed ? 'w-16' : 'w-64'}
+ `}
+ >
+ {/* Sidebar Toggle Button */}
+ <button
+ onClick={toggleSidebar}
+ className="absolute -right-3 top-10 bg-primary text-primary-foreground rounded-full p-1 shadow-md hover:bg-primary/90 transition-colors duration-300 hover:scale-110 group z-10"
+ aria-label={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
+ >
+ <div className="transition-transform duration-300 group-hover:rotate-[360deg]">
+ {isSidebarCollapsed ? <ChevronRightIcon size={16} /> : <ChevronLeftIcon size={16} />}
+ </div>
+ </button>
+
+ <nav className="space-y-2 px-2 mt-4">
+ <Link
+ href="/dashboard"
+ className={`
+ flex items-center p-2 rounded-md transition-all duration-200
+ ${pathname === '/dashboard'
+ ? 'bg-primary/10 text-primary font-medium'
+ : 'hover:bg-muted text-foreground/80 hover:text-foreground'
+ }
+ ${isSidebarCollapsed ? 'justify-center' : 'justify-start'}
+ `}
+ title="Dashboard"
+ >
+ <LayoutDashboardIcon size={18} className={`transition-transform duration-300 ${pathname === '/dashboard' ? 'scale-110' : ''}`} />
+ {!isSidebarCollapsed && (
+ <span className="ml-2 transition-opacity duration-300 opacity-100">Dashboard</span>
+ )}
+ </Link>
+ <Link
+ href="/loans"
+ className={`
+ flex items-center p-2 rounded-md transition-all duration-200
+ ${pathname === '/loans'
+ ? 'bg-primary/10 text-primary font-medium'
+ : 'hover:bg-muted text-foreground/80 hover:text-foreground'
+ }
+ ${isSidebarCollapsed ? 'justify-center' : 'justify-start'}
+ `}
+ title="Loans"
+ >
+ <CoinsIcon size={18} className={`transition-transform duration-300 ${pathname === '/loans' ? 'scale-110' : ''}`} />
+ {!isSidebarCollapsed && (
+ <span className="ml-2 transition-opacity duration-300 opacity-100">Loans</span>
+ )}
+ </Link>
+ <Link
+ href="/goals"
+ className={`
+ flex items-center p-2 rounded-md transition-all duration-200
+ ${pathname === '/goals'
+ ? 'bg-primary/10 text-primary font-medium'
+ : 'hover:bg-muted text-foreground/80 hover:text-foreground'
+ }
+ ${isSidebarCollapsed ? 'justify-center' : 'justify-start'}
+ `}
+ title="Goals"
+ >
+ <TargetIcon size={18} className={`transition-transform duration-300 ${pathname === '/goals' ? 'scale-110' : ''}`} />
+ {!isSidebarCollapsed && (
+ <span className="ml-2 transition-opacity duration-300 opacity-100">Goals</span>
+ )}
+ </Link>
+ <Link
+ href="/settings"
+ className={`
+ flex items-center p-2 rounded-md transition-all duration-200
+ ${pathname === '/settings'
+ ? 'bg-primary/10 text-primary font-medium'
+ : 'hover:bg-muted text-foreground/80 hover:text-foreground'
+ }
+ ${isSidebarCollapsed ? 'justify-center' : 'justify-start'}
+ `}
+ title="Settings"
+ >
+ <SettingsIcon size={18} className={`transition-transform duration-300 ${pathname === '/settings' ? 'scale-110' : ''}`} />
+ {!isSidebarCollapsed && (
+ <span className="ml-2 transition-opacity duration-300 opacity-100">Settings</span>
+ )}
+ </Link>
+ </nav>
</aside>
{/* Page Content */}
- <main className="flex-1 p-8">
- {children}
+ <main className="flex-1 p-4 md:p-8 overflow-auto transition-all duration-300">
+ <PageTransition>
+ {children}
+ </PageTransition>
</main>
</div>
</div>
diff --git a/frontend/src/components/shared/PageTransition.tsx b/frontend/src/components/shared/PageTransition.tsx
new file mode 100644
index 0000000..fc95865
--- /dev/null
+++ b/frontend/src/components/shared/PageTransition.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { usePathname } from 'next/navigation';
+import { ReactNode, useEffect, useState } from 'react';
+
+interface PageTransitionProps {
+ children: ReactNode;
+}
+
+export function PageTransition({ children }: PageTransitionProps) {
+ const pathname = usePathname();
+ const [isFirstRender, setIsFirstRender] = useState(true);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setIsFirstRender(false);
+ }, 500);
+
+ return () => clearTimeout(timeout);
+ }, []);
+
+ const variants = {
+ hidden: { opacity: 0, y: 20 },
+ enter: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -20 },
+ };
+
+ // Only apply softer animation on initial render
+ const initialAnimation = isFirstRender ? {
+ initial: { opacity: 0 },
+ animate: { opacity: 1 },
+ transition: { duration: 0.5 }
+ } : {
+ initial: "hidden",
+ animate: "enter",
+ exit: "exit",
+ variants,
+ transition: {
+ type: "tween",
+ ease: "easeInOut",
+ duration: 0.3
+ }
+ };
+
+ return (
+ <motion.div
+ key={pathname}
+ {...initialAnimation}
+ className="w-full h-full"
+ >
+ {children}
+ </motion.div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/components/shared/ThemeToggle.tsx b/frontend/src/components/shared/ThemeToggle.tsx
index 679bbc5..b54bda9 100644
--- a/frontend/src/components/shared/ThemeToggle.tsx
+++ b/frontend/src/components/shared/ThemeToggle.tsx
@@ -1,36 +1,69 @@
'use client';
+import { useState, useEffect } from 'react';
import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTheme } from './ThemeProvider';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ // Track initial mount to prevent animation on first render
+ const [isMounted, setIsMounted] = useState(false);
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
const toggleTheme = () => {
- if (theme === 'dark') {
- setTheme('light');
- } else {
- setTheme('dark');
- }
+ if (isAnimating) return;
+
+ setIsAnimating(true);
+
+ // Set new theme after a small delay for animation
+ const newTheme = theme === 'dark' ? 'light' : 'dark';
+ setTimeout(() => {
+ setTheme(newTheme);
+ setTimeout(() => {
+ setIsAnimating(false);
+ }, 300);
+ }, 200);
};
+ if (!isMounted) {
+ return (
+ <Button
+ variant="ghost"
+ size="icon"
+ className="opacity-0"
+ >
+ <Sun className="h-[1.2rem] w-[1.2rem]" />
+ </Button>
+ );
+ }
+
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
+ className={`relative overflow-hidden hover:bg-muted/80 hover:text-foreground transition-all duration-300 ${isAnimating ? 'cursor-wait' : ''}`}
aria-label={
theme === 'dark'
? 'Switch to light theme'
: 'Switch to dark theme'
}
+ disabled={isAnimating}
>
- {theme === 'dark' ? (
- <Sun className="h-[1.2rem] w-[1.2rem]" />
- ) : (
- <Moon className="h-[1.2rem] w-[1.2rem]" />
- )}
+ <div className={`absolute inset-0 bg-primary/5 rounded-full transition-all duration-500 ${isAnimating ? 'scale-[5] opacity-0' : 'scale-0 opacity-0'}`}></div>
+
+ <div className="relative">
+ {theme === 'dark' ? (
+ <Sun className={`h-5 w-5 transition-all duration-300 ${isAnimating ? 'rotate-90 scale-50 opacity-0' : 'rotate-0 scale-100 opacity-100'}`} />
+ ) : (
+ <Moon className={`h-5 w-5 transition-all duration-300 ${isAnimating ? 'rotate-90 scale-50 opacity-0' : 'rotate-0 scale-100 opacity-100'}`} />
+ )}
+ </div>
</Button>
);
} \ No newline at end of file
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index a2df8dc..347beaf 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -5,32 +5,44 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-300 ease-in-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.98] will-change-transform",
{
variants: {
variant: {
default:
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ "bg-primary text-primary-foreground shadow-sm hover:shadow-md hover:bg-primary/90 hover:translate-y-[-1px]",
destructive:
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ "bg-destructive text-white shadow-sm hover:shadow-md hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 hover:translate-y-[-1px]",
outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ "border bg-background shadow-sm hover:shadow-md hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 hover:border-primary/50",
secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ "bg-secondary text-secondary-foreground shadow-sm hover:shadow-md hover:bg-secondary/80 hover:translate-y-[-1px]",
ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 hover:scale-[1.02]",
+ link: "text-primary underline-offset-4 hover:underline hover:text-primary/80",
+ gradient:
+ "bg-gradient-to-r from-primary to-primary/80 text-primary-foreground shadow-sm hover:shadow-md hover:opacity-90 hover:translate-y-[-1px] transition-all duration-300",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
- icon: "size-9",
+ xl: "h-12 rounded-md px-8 text-base has-[>svg]:px-6",
+ icon: "size-9 p-0 aspect-square",
+ "icon-sm": "size-8 p-0 aspect-square rounded-md",
},
+ animation: {
+ none: "",
+ pulse: "animate-pulse",
+ bounce: "animate-bounce",
+ spin: "[&_svg]:animate-spin",
+ fadeIn: "animate-in fade-in",
+ }
},
defaultVariants: {
variant: "default",
size: "default",
+ animation: "none",
},
}
)
@@ -39,6 +51,7 @@ function Button({
className,
variant,
size,
+ animation,
asChild = false,
...props
}: React.ComponentProps<"button"> &
@@ -50,7 +63,7 @@ function Button({
return (
<Comp
data-slot="button"
- className={cn(buttonVariants({ variant, size, className }))}
+ className={cn(buttonVariants({ variant, size, animation, className }))}
{...props}
/>
)
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
index d05bbc6..9ec9328 100644
--- a/frontend/src/components/ui/card.tsx
+++ b/frontend/src/components/ui/card.tsx
@@ -2,12 +2,24 @@ import * as React from "react"
import { cn } from "@/lib/utils"
-function Card({ className, ...props }: React.ComponentProps<"div">) {
+const cardVariants = {
+ default: "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm transition-all duration-300",
+ interactive: "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm hover:shadow-md transition-all duration-300 hover:border-primary/20 hover:translate-y-[-2px]",
+ elevated: "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border-0 py-6 shadow-md transition-all duration-300",
+ outline: "bg-transparent text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-none transition-all duration-300",
+ gradient: "bg-gradient-to-br from-card to-background text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm transition-all duration-300",
+}
+
+interface CardProps extends React.ComponentProps<"div"> {
+ variant?: keyof typeof cardVariants;
+}
+
+function Card({ className, variant = "default", ...props }: CardProps) {
return (
<div
data-slot="card"
className={cn(
- "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+ cardVariants[variant],
className
)}
{...props}
@@ -20,7 +32,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
- "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 animate-in fade-in-50 duration-300",
className
)}
{...props}
@@ -65,7 +77,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
- className={cn("px-6", className)}
+ className={cn("px-6 animate-in fade-in-50 duration-300 delay-75", className)}
{...props}
/>
)
@@ -75,7 +87,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
- className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
+ className={cn("flex items-center px-6 [.border-t]:pt-6 animate-in fade-in-50 duration-300 delay-150", className)}
{...props}
/>
)
@@ -89,4 +101,5 @@ export {
CardAction,
CardDescription,
CardContent,
+ cardVariants,
}
diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx
index 03295ca..91d3df1 100644
--- a/frontend/src/components/ui/input.tsx
+++ b/frontend/src/components/ui/input.tsx
@@ -2,20 +2,24 @@ import * as React from "react"
import { cn } from "@/lib/utils"
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
- return (
- <input
- type={type}
- data-slot="input"
- className={cn(
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- className
- )}
- {...props}
- />
- )
-}
+export interface InputProps
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
+
+const Input = React.forwardRef<HTMLInputElement, InputProps>(
+ ({ className, type, ...props }, ref) => {
+ return (
+ <input
+ type={type}
+ className={cn(
+ "flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-all duration-200 shadow-sm hover:shadow-md hover:border-primary/30 file:border-0 file:bg-transparent file:text-sm file:font-medium",
+ className
+ )}
+ ref={ref}
+ {...props}
+ />
+ )
+ }
+)
+Input.displayName = "Input"
export { Input }