Add motion library and login animations

This commit is contained in:
2025-01-14 23:37:31 -05:00
parent b6f69c254c
commit 3efd171ec6
12 changed files with 163 additions and 161 deletions

View File

@@ -36,6 +36,7 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"lucide-react": "^0.469.0",
"motion": "^11.18.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
@@ -4606,6 +4607,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.18.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.0.tgz",
"integrity": "sha512-Vmjl5Al7XqKHzDFnVqzi1H9hzn5w4eN/bdqXTymVpU2UuMQuz9w6UPdsL9dFBeH7loBlnu4qcEXME+nvbkcIOw==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.16.4",
"motion-utils": "^11.16.0",
"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/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
@@ -5270,6 +5298,47 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/motion": {
"version": "11.18.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-11.18.0.tgz",
"integrity": "sha512-uJ4zNXh/4K9C5wftxHKlXLHC0Rc9dHSHPyO1P6T9XE2bTn2z8C2lOZX/M8vAmFp0gtJTJ3aYkv44lTtJSfv6+A==",
"license": "MIT",
"dependencies": {
"framer-motion": "^11.18.0",
"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/motion-dom": {
"version": "11.16.4",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.16.4.tgz",
"integrity": "sha512-2wuCie206pCiP2K23uvwJeci4pMFfyQKpWI0Vy6HrCTDzDCer4TsYtT7IVnuGbDeoIV37UuZiUr6SZMHEc1Vww==",
"license": "MIT",
"dependencies": {
"motion-utils": "^11.16.0"
}
},
"node_modules/motion-utils": {
"version": "11.16.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.16.0.tgz",
"integrity": "sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -38,6 +38,7 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"lucide-react": "^0.469.0",
"motion": "^11.18.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",

View File

@@ -2,7 +2,6 @@ import { Routes, Route, useNavigate, Navigate } from 'react-router-dom';
import { MainLayout } from './components/layout/MainLayout';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Products } from './pages/Products';
import { Import } from './pages/Import';
import { Dashboard } from './pages/Dashboard';
import { Orders } from './pages/Orders';
import { Settings } from './pages/Settings';
@@ -55,7 +54,6 @@ function App() {
<Route element={<MainLayout />}>
<Route path="/" element={<Dashboard />} />
<Route path="/products" element={<Products />} />
<Route path="/import" element={<Import />} />
<Route path="/orders" element={<Orders />} />
<Route path="/purchase-orders" element={<PurchaseOrders />} />
<Route path="/analytics" element={<Analytics />} />

View File

@@ -1,21 +1,24 @@
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "./AppSidebar";
import { Outlet } from "react-router-dom";
import { motion } from "motion/react";
export function MainLayout() {
return (
<SidebarProvider defaultOpen>
<div className="flex min-h-screen w-full pr-2">
<AppSidebar />
<main className="flex-1 overflow-hidden">
<div className="flex h-14 w-full items-center border-b px-4 gap-4">
<SidebarTrigger />
</div>
<div className="overflow-auto h-[calc(100vh-3.5rem)] max-w-[1500px]">
<Outlet />
</div>
</main>
</div>
</SidebarProvider>
<motion.div layout>
<SidebarProvider defaultOpen>
<div className="flex min-h-screen w-full pr-2">
<AppSidebar />
<main className="flex-1 overflow-hidden">
<div className="flex h-14 w-full items-center border-b px-4 gap-4">
<SidebarTrigger />
</div>
<div className="overflow-auto h-[calc(100vh-3.5rem)] max-w-[1500px]">
<Outlet />
</div>
</main>
</div>
</SidebarProvider>
</motion.div>
);
}

View File

@@ -7,6 +7,7 @@ import { StockAnalysis } from '../components/analytics/StockAnalysis';
import { PriceAnalysis } from '../components/analytics/PriceAnalysis';
import { CategoryPerformance } from '../components/analytics/CategoryPerformance';
import config from '../config';
import { motion } from 'motion/react';
interface AnalyticsStats {
profitMargin: number;
@@ -34,7 +35,7 @@ export function Analytics() {
}
return (
<div className="flex-1 space-y-4 p-8 pt-6">
<motion.div layout className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Analytics</h2>
</div>
@@ -107,6 +108,6 @@ export function Analytics() {
<CategoryPerformance />
</TabsContent>
</Tabs>
</div>
</motion.div>
);
}

View File

@@ -4,10 +4,10 @@ import { LowStockAlerts } from "@/components/dashboard/LowStockAlerts"
import { TrendingProducts } from "@/components/dashboard/TrendingProducts"
import { VendorPerformance } from "@/components/dashboard/VendorPerformance"
import { KeyMetricsCharts } from "@/components/dashboard/KeyMetricsCharts"
import { motion } from "motion/react"
export function Dashboard() {
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<motion.div layout className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
</div>
@@ -30,7 +30,7 @@ export function Dashboard() {
<VendorPerformance />
</Card>
</div>
</div>
</motion.div>
)
}

View File

@@ -1,80 +0,0 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import config from '../config';
export function Import() {
const [file, setFile] = useState<File | null>(null);
const importMutation = useMutation({
mutationFn: async (formData: FormData) => {
const response = await fetch(`${config.apiUrl}/products/import`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Import failed');
}
return response.json();
},
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files?.[0]) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
importMutation.mutate(formData);
};
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-6">Import Products</h1>
<div className="max-w-xl">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium">
Upload CSV File
</label>
<input
type="file"
accept=".csv"
onChange={handleFileChange}
className="w-full border rounded-md p-2"
/>
<p className="text-sm text-muted-foreground">
The CSV file should contain the following columns: sku, name, description (optional), category (optional)
</p>
</div>
<button
type="submit"
disabled={!file || importMutation.isPending}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
>
{importMutation.isPending ? 'Importing...' : 'Import Products'}
</button>
{importMutation.isSuccess && (
<p className="text-green-600">
Successfully imported {importMutation.data.imported} products
</p>
)}
{importMutation.isError && (
<p className="text-red-500">
Error importing products: {importMutation.error.message}
</p>
)}
</form>
</div>
</div>
);
}

View File

@@ -1,22 +1,18 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import config from '../config';
import { Loader2, Box } from 'lucide-react';
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import config from "../config";
import { Loader2, Box } from "lucide-react";
import { motion } from "motion/react";
const isDev = process.env.NODE_ENV === 'development';
const isDev = process.env.NODE_ENV === "development";
export function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
@@ -26,46 +22,46 @@ export function Login() {
try {
const url = isDev ? "/auth-inv/login" : `${config.authUrl}/login`;
console.log('Making login request:', {
console.log("Making login request:", {
url,
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: { username, password },
config
config,
});
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
credentials: 'include'
credentials: "include",
});
console.log('Login response status:', response.status);
console.log("Login response status:", response.status);
if (!response.ok) {
const data = await response.json().catch(() => ({ error: 'Failed to parse error response' }));
console.error('Login failed:', data);
throw new Error(data.error || 'Login failed');
const data = await response
.json()
.catch(() => ({ error: "Failed to parse error response" }));
console.error("Login failed:", data);
throw new Error(data.error || "Login failed");
}
const data = await response.json();
console.log('Login successful:', data);
console.log("Login successful:", data);
sessionStorage.setItem('token', data.token);
sessionStorage.setItem('isLoggedIn', 'true');
toast.success('Successfully logged in');
navigate('/');
sessionStorage.setItem("token", data.token);
sessionStorage.setItem("isLoggedIn", "true");
toast.success("Successfully logged in");
navigate("/", { replace: true });
} catch (error) {
console.error('Login error:', error);
console.error("Login error:", error);
toast.error(
error instanceof Error
? error.message
: 'An unexpected error occurred',
error instanceof Error ? error.message : "An unexpected error occurred"
);
} finally {
setIsLoading(false);
@@ -73,23 +69,33 @@ export function Login() {
};
return (
<div className="min-h-screen bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 antialiased">
<div className="flex flex-col gap-2 p-2 bg-primary">
<div className="p-4 flex items-center gap-2 group-data-[collapsible=icon]:justify-center text-white">
<motion.div
layout
className="min-h-screen bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 antialiased"
>
<div className="flex flex-col gap-2 p-2 bg-primary">
<div className="p-4 flex items-center gap-2 group-data-[collapsible=icon]:justify-center text-white">
<Box className="h-6 w-6 shrink-0" />
<h2 className="text-lg font-semibold group-data-[collapsible=icon]:hidden">
Inventory Manager
</h2>
</div>
</div>
<div className="container relative flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
className="container relative flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center"
>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<Card className="border-none shadow-xl">
<CardHeader className="space-y-1">
<div className="flex items-center justify-center mb-2">
<Box className="h-10 w-10 text-primary" />
</div>
<CardTitle className="text-2xl text-center">Log in to continue</CardTitle>
<CardTitle className="text-2xl text-center">
Log in to continue
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
@@ -126,7 +132,7 @@ export function Login() {
</CardContent>
</Card>
</div>
</div>
</div>
</motion.div>
</motion.div>
);
}

View File

@@ -33,7 +33,7 @@ import { ArrowUpDown, Search } from "lucide-react";
import debounce from 'lodash/debounce';
import config from '../config';
import { DateRange } from 'react-day-picker';
import { motion } from 'motion/react';
interface Order {
order_number: string;
customer: string;
@@ -130,7 +130,7 @@ export function Orders() {
);
return (
<div className="p-8 space-y-8">
<motion.div layout className="p-8 space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Orders</h1>
<div className="text-sm text-muted-foreground">
@@ -317,6 +317,6 @@ export function Orders() {
</PaginationContent>
</Pagination>
)}
</div>
</motion.div>
);
}

View File

@@ -23,7 +23,7 @@ import {
import { Button } from "@/components/ui/button";
import { Settings2 } from "lucide-react";
import config from '../config';
import { motion } from 'motion/react';
// Enhanced Product interface with all possible fields
interface Product {
// Basic product info (from products table)
@@ -309,10 +309,11 @@ export function Products() {
);
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
aria-disabled={page === 1 || isFetching}
className={page === 1 || isFetching ? 'pointer-events-none opacity-50' : ''}
onClick={() => handlePageChange(Math.max(1, page - 1))}
@@ -371,11 +372,12 @@ export function Products() {
</PaginationItem>
</PaginationContent>
</Pagination>
);
};
return (
<div className="p-8 space-y-8">
<motion.div layout className="p-8 space-y-8">
<h1 className="text-2xl font-bold">Products</h1>
<div>
@@ -451,6 +453,6 @@ export function Products() {
productId={selectedProductId}
onClose={() => setSelectedProductId(null)}
/>
</div>
</motion.div>
);
}

View File

@@ -19,6 +19,7 @@ import {
PaginationNext,
PaginationPrevious,
} from '../components/ui/pagination';
import { motion } from 'motion/react';
interface PurchaseOrder {
id: number;
@@ -205,7 +206,7 @@ export default function PurchaseOrders() {
}
return (
<div className="container mx-auto py-6">
<motion.div layout className="container mx-auto py-6">
<h1 className="mb-6 text-3xl font-bold">Purchase Orders</h1>
{/* Metrics Overview */}
@@ -453,6 +454,6 @@ export default function PurchaseOrders() {
</Table>
</CardContent>
</Card>
</div>
</motion.div>
);
}

View File

@@ -3,10 +3,11 @@ import { DataManagement } from "@/components/settings/DataManagement";
import { StockManagement } from "@/components/settings/StockManagement";
import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
import { CalculationSettings } from "@/components/settings/CalculationSettings";
import { motion } from 'motion/react';
export function Settings() {
return (
<div className="container mx-auto py-6">
<motion.div layout className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-3xl font-bold">Settings</h1>
</div>
@@ -39,6 +40,6 @@ export function Settings() {
<CalculationSettings />
</TabsContent>
</Tabs>
</div>
</motion.div>
);
}