From 1e0be3f86e56cbf08fc3434df8dc0457f8877f8c Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 28 Mar 2025 19:26:52 -0400 Subject: [PATCH] Ensure data management page grabs progress of any running scripts on load, clean up unneeded console logs, restyle login page --- inventory/index.html | 7 +- inventory/public/box.svg | 1 - inventory/public/cherrybottom.ico | Bin 0 -> 30851 bytes inventory/public/cherrybottom.png | Bin 0 -> 21475 bytes inventory/src/App.css | 43 +- .../src/components/layout/AppSidebar.tsx | 21 +- .../components/settings/DataManagement.tsx | 703 ++++++++++++------ inventory/src/index.css | 6 +- inventory/src/main.tsx | 1 + inventory/src/pages/Login.tsx | 150 ++-- inventory/tailwind.config.js | 3 + 11 files changed, 599 insertions(+), 336 deletions(-) delete mode 100644 inventory/public/box.svg create mode 100644 inventory/public/cherrybottom.ico create mode 100644 inventory/public/cherrybottom.png diff --git a/inventory/index.html b/inventory/index.html index c69b332..5ba2347 100644 --- a/inventory/index.html +++ b/inventory/index.html @@ -2,9 +2,12 @@ - + + + + - Inventory Manager + A Cherry On Bottom
diff --git a/inventory/public/box.svg b/inventory/public/box.svg deleted file mode 100644 index bf13e8b..0000000 --- a/inventory/public/box.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/inventory/public/cherrybottom.ico b/inventory/public/cherrybottom.ico new file mode 100644 index 0000000000000000000000000000000000000000..318a12909a4bbee353940e560ad8dcfd28dae773 GIT binary patch literal 30851 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

#%B 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 literal 0 HcmV?d00001 diff --git a/inventory/src/App.css b/inventory/src/App.css index 8da90e7..b8c8a47 100644 --- a/inventory/src/App.css +++ b/inventory/src/App.css @@ -1,42 +1,3 @@ #root { - max-width: 1800px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} + font-family: 'Inter', sans-serif; +} \ No newline at end of file diff --git a/inventory/src/components/layout/AppSidebar.tsx b/inventory/src/components/layout/AppSidebar.tsx index 95fe654..a53783a 100644 --- a/inventory/src/components/layout/AppSidebar.tsx +++ b/inventory/src/components/layout/AppSidebar.tsx @@ -3,7 +3,6 @@ import { Package, BarChart2, Settings, - Box, ClipboardList, LogOut, Users, @@ -22,6 +21,7 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarSeparator, + useSidebar } from "@/components/ui/sidebar"; import { useLocation, useNavigate, Link } from "react-router-dom"; import { Protected } from "@/components/auth/Protected"; @@ -80,6 +80,7 @@ const items = [ export function AppSidebar() { const location = useLocation(); const navigate = useNavigate(); + useSidebar(); const handleLogout = () => { localStorage.removeItem('token'); @@ -90,11 +91,19 @@ export function AppSidebar() { return ( -

- -

- Inventory Manager -

+
+
+
+ Cherry Bottom +
+
+ A Cherry On Bottom +
+
diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index 5c5c5bd..07797c3 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -28,6 +28,7 @@ import { Loader2, X, RefreshCw, AlertTriangle, RefreshCcw, Hourglass } from "luc import config from "../../config"; import { toast } from "sonner"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; +import { Skeleton } from "@/components/ui/skeleton"; interface HistoryRecord { @@ -78,6 +79,75 @@ interface GroupedTableCounts { config: TableCount[]; } +// TableSkeleton component for consistent loading states +interface TableSkeletonProps { + rows?: number; + columns?: number; + useAccordion?: boolean; +} + +const TableSkeleton = ({ rows = 5, columns = 4, useAccordion = false }: TableSkeletonProps) => { + return ( + + + {Array.from({ length: rows }).map((_, rowIndex) => ( + + + {useAccordion ? ( + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ ) : ( +
+ + +
+ )} +
+
+ ))} +
+
+ ); +}; + +// Empty state skeleton that maintains consistent sizing +const EmptyStateSkeleton = ({ message = "No data available" }: { message?: string }) => { + return ( + + +
+

{message}

+
+
+
+ ); +}; + +// Create a component for the empty state of status cards +const StatusEmptyState = ({ message }: { message: React.ReactNode }) => ( +
+

{message}

+
+); + export function DataManagement() { const [isUpdating, setIsUpdating] = useState(false); const [isResetting, setIsResetting] = useState(false); @@ -94,6 +164,149 @@ export function DataManagement() { // Add useRef for scroll handling const terminalRef = useRef(null); + // Check if there's an active script running on component mount + const checkActiveProcess = async () => { + try { + console.log("Checking for active processes..."); + const response = await fetch(`${config.apiUrl}/csv/status`, { + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Failed to check active process status"); + } + + const data = await response.json(); + console.log("Active process check response:", data); + + if (data.active && data.progress) { + console.log("Active process detected:", data.progress); + + // Determine if it's a reset or update based on the progress data + const isReset = + data.progress.operation?.includes("reset") || + data.progress.operation?.includes("Reset"); + + // Set the appropriate state + if (isReset) { + console.log("Reconnecting to reset process..."); + setIsResetting(true); + // Connect to reset SSE endpoint + connectToEventSource("reset"); + } else { + console.log("Reconnecting to update process..."); + setIsUpdating(true); + // Connect to update SSE endpoint + connectToEventSource("update"); + } + + // If we have progress data, initialize the output + if (data.progress) { + setScriptOutput([JSON.stringify(data.progress)]); + } + } else { + console.log("No active processes detected"); + } + } catch (error) { + console.error("Error checking for active processes:", error); + } + }; + + // Function to connect to the appropriate SSE endpoint + const connectToEventSource = (type: "update" | "reset") => { + if (eventSource) { + eventSource.close(); + } + + // The correct URL structure is /api/csv/{type}/progress + const sseUrl = `${config.apiUrl}/csv/${type}/progress`; + console.log(`Connecting to event source: ${sseUrl}`); + + // Create the event source with the correct URL pattern + const source = new EventSource( + sseUrl, + { withCredentials: true } + ); + + source.onopen = () => { + console.log(`SSE connection opened for ${type} progress`); + // Add a message indicating reconnection, but don't clear existing output + setScriptOutput(prev => [...prev, `[Connected to ${type} progress stream]`]); + }; + + source.onmessage = (event) => { + console.log(`SSE message received:`, event.data); + setScriptOutput((prev) => [...prev, event.data]); + + try { + const data = JSON.parse(event.data); + // Handle completion events + if (data.status === 'complete' || data.status === 'error' || data.status === 'cancelled') { + if (data.operation === 'Full update complete' || + data.operation === 'Full reset complete' || + data.status === 'error' || + data.status === 'cancelled') { + source.close(); + setEventSource(null); + setIsUpdating(false); + setIsResetting(false); + fetchHistory(); // Refresh at end + + if (data.status === 'complete') { + toast.success(`${type === 'update' ? 'Update' : 'Reset'} completed successfully`); + } else if (data.status === 'error') { + toast.error(`${type === 'update' ? 'Update' : 'Reset'} failed: ${data.error || 'Unknown error'}`); + } else { + toast.warning(`${type === 'update' ? 'Update' : 'Reset'} cancelled`); + } + } + } + + // Also check progress objects that might be nested + if (data.progress && (data.progress.status === 'complete' || data.progress.status === 'error' || data.progress.status === 'cancelled')) { + if (data.progress.operation === 'Full update complete' || + data.progress.operation === 'Full reset complete' || + data.progress.status === 'error' || + data.progress.status === 'cancelled') { + source.close(); + setEventSource(null); + setIsUpdating(false); + setIsResetting(false); + fetchHistory(); // Refresh at end + + if (data.progress.status === 'complete') { + toast.success(`${type === 'update' ? 'Update' : 'Reset'} completed successfully`); + } else if (data.progress.status === 'error') { + toast.error(`${type === 'update' ? 'Update' : 'Reset'} failed: ${data.progress.error || 'Unknown error'}`); + } else { + toast.warning(`${type === 'update' ? 'Update' : 'Reset'} cancelled`); + } + } + } + } catch (error) { + // Not JSON, just continue + } + }; + + source.onerror = (error) => { + console.error(`SSE connection error for ${type}:`, error); + + // Attempt to reconnect with exponential backoff + if (source.readyState === EventSource.CLOSED) { + source.close(); + setEventSource(null); + + // Only attempt to reconnect if we're still updating/resetting + if ((type === "update" && isUpdating) || (type === "reset" && isResetting)) { + console.log("Connection closed, will attempt to reconnect..."); + setTimeout(() => connectToEventSource(type), 3000); + } + } + }; + + setEventSource(source); + }; + // Helper to format date const formatDate = (date: string) => { return new Date(date).toLocaleString(); @@ -190,52 +403,9 @@ export function DataManagement() { fetchHistory(); // Refresh at start try { - const source = new EventSource(`${config.apiUrl}/csv/update/progress`, { - withCredentials: true, - }); - - source.onmessage = (event) => { - setScriptOutput((prev) => [...prev, event.data]); - - // Try to parse for status updates, but don't affect display - try { - const data = JSON.parse(event.data); - if (data.status === 'complete' || data.status === 'error' || data.status === 'cancelled') { - // Only close and reset state if this is the final completion message - if (data.operation === 'Full update complete' || - data.status === 'error' || - data.status === 'cancelled') { - source.close(); - setEventSource(null); - setIsUpdating(false); - fetchHistory(); // Refresh at end - - if (data.status === 'complete') { - toast.success("Update completed successfully"); - } else if (data.status === 'error') { - toast.error(`Update failed: ${data.error || 'Unknown error'}`); - } else { - toast.warning("Update cancelled"); - } - } - // For intermediate completions, just show a toast - else if (data.status === 'complete') { - toast.success(data.message || "Step completed"); - } - } - } catch (error) { - // Not JSON, just display as is - } - }; - - source.onerror = (error) => { - setScriptOutput((prev) => [...prev, `[Error] ${error.type}`]); - source.close(); - setEventSource(null); - setIsUpdating(false); - }; - - setEventSource(source); + console.log("Starting full update..."); + // Connect to the update SSE endpoint + connectToEventSource("update"); const response = await fetch(`${config.apiUrl}/csv/full-update`, { method: "POST", @@ -246,12 +416,18 @@ export function DataManagement() { const data = await response.json(); throw new Error(data.error || "Failed to start update"); } + + console.log("Full update request successful"); } catch (error) { + console.error("Error starting full update:", error); if (error instanceof Error) { toast.error(`Update failed: ${error.message}`); } setIsUpdating(false); - setEventSource(null); + if (eventSource) { + eventSource.close(); + setEventSource(null); + } } }; @@ -261,52 +437,9 @@ export function DataManagement() { fetchHistory(); // Refresh at start try { - const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, { - withCredentials: true, - }); - - source.onmessage = (event) => { - setScriptOutput((prev) => [...prev, event.data]); - - // Try to parse for status updates, but don't affect display - try { - const data = JSON.parse(event.data); - if (data.status === 'complete' || data.status === 'error' || data.status === 'cancelled') { - // Only close and reset state if this is the final completion message - if (data.operation === 'Full reset complete' || - data.status === 'error' || - data.status === 'cancelled') { - source.close(); - setEventSource(null); - setIsResetting(false); - fetchHistory(); // Refresh at end - - if (data.status === 'complete') { - toast.success("Reset completed successfully"); - } else if (data.status === 'error') { - toast.error(`Reset failed: ${data.error || 'Unknown error'}`); - } else { - toast.warning("Reset cancelled"); - } - } - // For intermediate completions, just show a toast - else if (data.status === 'complete') { - toast.success(data.message || "Step completed"); - } - } - } catch (error) { - // Not JSON, just display as is - } - }; - - source.onerror = (error) => { - setScriptOutput((prev) => [...prev, `[Error] ${error.type}`]); - source.close(); - setEventSource(null); - setIsResetting(false); - }; - - setEventSource(source); + console.log("Starting full reset..."); + // Connect to the reset SSE endpoint + connectToEventSource("reset"); const response = await fetch(`${config.apiUrl}/csv/full-reset`, { method: "POST", @@ -317,12 +450,18 @@ export function DataManagement() { const data = await response.json(); throw new Error(data.error || "Failed to start reset"); } + + console.log("Full reset request successful"); } catch (error) { + console.error("Error starting full reset:", error); if (error instanceof Error) { toast.error(`Reset failed: ${error.message}`); } setIsResetting(false); - setEventSource(null); + if (eventSource) { + eventSource.close(); + setEventSource(null); + } } }; @@ -351,6 +490,9 @@ export function DataManagement() { setIsResetting(false); toast.info("Operation cancelled"); + + // Refresh history data + fetchHistory(); } catch (error) { toast.error( `Failed to cancel operation: ${ @@ -443,14 +585,141 @@ export function DataManagement() { }; useEffect(() => { - // Fetch data immediately on component mount - fetchHistory(); + // Set up async function to run process checks and initial data load + const initComponent = async () => { + try { + console.log("Initializing DataManagement component..."); + setIsLoading(true); + + // First check for any active operations + await checkActiveProcess(); + + // Then fetch data (but only if we're not already in an active operation) + if (!isUpdating && !isResetting) { + await fetchHistory(); + } + + console.log("Component initialization complete"); + } catch (error) { + console.error("Error during component initialization:", error); + // Ensure we fetch data even if checkActiveProcess fails + await fetchHistory(); + } finally { + setIsLoading(false); + } + }; + + // Run the initialization + initComponent(); // Set up periodic refresh every minute const refreshInterval = setInterval(fetchHistory, 60000); + + // Clean up interval and event sources on component unmount + return () => { + console.log("Cleaning up DataManagement component..."); + clearInterval(refreshInterval); + + // Close any open event sources + if (eventSource) { + console.log("Closing event source on unmount"); + eventSource.close(); + } + }; + }, []); - // Clean up interval on component unmount - return () => clearInterval(refreshInterval); + // Add a dedicated useEffect for direct SSE monitoring that doesn't rely on server status + useEffect(() => { + // Listen for SSE messages directly, even if the server reports no active processes + const setupDirectSSEMonitoring = () => { + console.log("Setting up direct SSE monitoring..."); + + // Try both update and reset endpoints + const setupListenerForType = (type: "update" | "reset") => { + const sseUrl = `${config.apiUrl}/csv/${type}/progress`; + console.log(`Setting up SSE listener for ${type} at ${sseUrl}`); + + try { + const source = new EventSource(sseUrl, { withCredentials: true }); + + source.onopen = () => { + console.log(`Direct SSE connection opened for ${type}`); + }; + + source.onmessage = (event) => { + console.log(`Direct SSE message from ${type}:`, event.data); + + try { + const data = JSON.parse(event.data); + + // If we get a real progress message, update our state accordingly + if (data.progress && data.progress.status === 'running') { + console.log(`Detected running ${type} process from direct SSE`); + + // Update the appropriate state flag + if (type === 'update') { + setIsUpdating(true); + setIsResetting(false); + } else { + setIsUpdating(false); + setIsResetting(true); + } + + // Add the message to our output + setScriptOutput(prev => { + // Add as first message if no messages, otherwise append + if (prev.length === 0) { + return [`[Connected to running ${type} process]`, event.data]; + } else { + return [...prev, event.data]; + } + }); + } + + // Handle completion events + if (data.status === 'complete' || data.status === 'error' || data.status === 'cancelled' || + (data.progress && (data.progress.status === 'complete' || data.progress.status === 'error' || data.progress.status === 'cancelled'))) { + console.log(`Process ${type} completed or failed`); + source.close(); + setIsUpdating(false); + setIsResetting(false); + fetchHistory(); + } + } catch (err) { + // Not JSON or invalid, just continue + } + }; + + source.onerror = (error) => { + console.log(`Direct SSE error for ${type}:`, error); + source.close(); + }; + + // Return the source for cleanup + return source; + } catch (err) { + console.error(`Error setting up direct SSE for ${type}:`, err); + return null; + } + }; + + // Set up listeners for both types + const updateSource = setupListenerForType('update'); + const resetSource = setupListenerForType('reset'); + + // Return cleanup function + return () => { + console.log("Cleaning up direct SSE monitoring"); + if (updateSource) updateSource.close(); + if (resetSource) resetSource.close(); + }; + }; + + // Set up monitoring + const cleanup = setupDirectSSEMonitoring(); + + // Cleanup on unmount + return cleanup; }, []); // Add useEffect to handle auto-scrolling @@ -460,11 +729,13 @@ export function DataManagement() { } }, [scriptOutput]); - // Replace renderTerminal with new version + // Replace renderTerminal with new version - simple direct output display const renderTerminal = () => { - if (!scriptOutput.length) return null; + if (!isUpdating && !isResetting && scriptOutput.length === 0) { + return null; + } - return ( + return ( Script Output @@ -496,10 +767,19 @@ export function DataManagement() { return new Intl.NumberFormat().format(num); }; - // Update renderTableCountsSection to match other cards' styling + // Update renderTableCountsSection to use skeletons const renderTableCountsSection = () => { - if (!tableCounts) return null; - + const renderTableCountsSkeleton = () => ( +
+ {Array.from({ length: 18 }).map((_, i) => ( +
+ + +
+ ))} +
+ ); + const renderTableGroup = (_title: string, tables: TableCount[]) => (
@@ -526,14 +806,21 @@ export function DataManagement() { Table Record Counts - {isLoading && !tableCounts.core.length ? ( -
- + {isLoading ? ( +
+ {renderTableCountsSkeleton()}
+ ) : !tableCounts ? ( + ) : (
-
{renderTableGroup('Core Tables', tableCounts.core)}
-
{renderTableGroup('Metrics Tables', tableCounts.metrics)}
+
{renderTableGroup('Core Tables', tableCounts?.core || [])}
+
{renderTableGroup('Metrics Tables', tableCounts?.metrics || [])}
)} @@ -545,91 +832,91 @@ export function DataManagement() {
{/* Full Update Card */} - - + + Full Update Import latest data and recalculate all metrics - - -
- + + )} + {isUpdating && ( - )} -
-
-
+ + + )} +
+ + {/* Full Reset Card */} - - + + Full Reset Reset database, reimport all data, and recalculate metrics - +
- - - - - - + )} + + + + Are you absolutely sure? - + This will completely reset the database, delete all data, and reimport everything from scratch. This action cannot be undone. - - - - Cancel + + + + Cancel Continue - - - + + + {isResetting && (
- {/* Terminal Output */} - {(isUpdating || isResetting) && renderTerminal()} + {/* Terminal Output - Always show if there are operations running */} + {renderTerminal()} {/* History Section */}
@@ -673,8 +960,13 @@ export function DataManagement() {
{isLoading && !tableStatus.length ? ( -
- +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))}
) : tableStatus.length > 0 ? ( tableStatus.map((table) => ( @@ -689,13 +981,14 @@ export function DataManagement() {
)) ) : ( -
- {hasError ? ( - "Failed to load data. Please try refreshing." - ) : ( - <>No imports have been performed yet.
Run a full update or reset to import data. - )} -
+ No imports have been performed yet.
Run a full update or reset to import data. + ) + } + /> )}
@@ -709,8 +1002,13 @@ export function DataManagement() {
{isLoading && !moduleStatus.length ? ( -
- +
+ {Array.from({ length: 7 }).map((_, i) => ( +
+ + +
+ ))}
) : moduleStatus.length > 0 ? ( moduleStatus.map((module) => ( @@ -725,13 +1023,14 @@ export function DataManagement() {
)) ) : ( -
- {hasError ? ( - "Failed to load data. Please try refreshing." - ) : ( - <>No metrics have been calculated yet.
Run a full update or reset to calculate metrics. - )} -
+ No metrics have been calculated yet.
Run a full update or reset to calculate metrics. + ) + } + /> )}
@@ -751,14 +1050,7 @@ export function DataManagement() { {isLoading && !importHistory.length ? ( - - -
- - Loading import history... -
-
-
+ ) : importHistory.length > 0 ? ( importHistory.slice(0, 20).map((record) => ( @@ -838,15 +1130,12 @@ export function DataManagement() { )) ) : ( - - - {hasError ? ( - "Failed to load import history. Please try refreshing." - ) : ( - "No import history available" - )} - - + )}
@@ -862,14 +1151,7 @@ export function DataManagement() { {isLoading && !calculateHistory.length ? ( - - -
- - Loading calculation history... -
-
-
+ ) : calculateHistory.length > 0 ? ( calculateHistory.slice(0, 20).map((record) => ( @@ -954,15 +1236,12 @@ export function DataManagement() { )) ) : ( - - - {hasError ? ( - "Failed to load calculation history. Please try refreshing." - ) : ( - "No calculation history available" - )} - - + )}
diff --git a/inventory/src/index.css b/inventory/src/index.css index a2fa817..150883b 100644 --- a/inventory/src/index.css +++ b/inventory/src/index.css @@ -96,17 +96,15 @@ @apply border-border; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-sans; } } - - @layer base { * { @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-sans; } } diff --git a/inventory/src/main.tsx b/inventory/src/main.tsx index 69ddf4f..b7df181 100644 --- a/inventory/src/main.tsx +++ b/inventory/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' +import './App.css' import App from './App.tsx' import { BrowserRouter as Router } from 'react-router-dom' diff --git a/inventory/src/pages/Login.tsx b/inventory/src/pages/Login.tsx index 665037f..d69296f 100644 --- a/inventory/src/pages/Login.tsx +++ b/inventory/src/pages/Login.tsx @@ -1,28 +1,31 @@ import { useState, useContext } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { AuthContext } from "@/contexts/AuthContext"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; 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 { Loader2, Box } from "lucide-react"; -import { motion } from "framer-motion"; -import { AuthContext } from "@/contexts/AuthContext"; +import { Label } from "@/components/ui/label"; +import { motion } from "motion/react"; export function Login() { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { login } = useContext(AuthContext); - const handleLogin = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); + const formData = new FormData(e.currentTarget); + const username = formData.get("username") as string; + const password = formData.get("password") as string; + try { await login(username, password); - + // Login successful, redirect to the requested page or home const redirectTo = searchParams.get("redirect") || "/"; navigate(redirectTo); @@ -36,70 +39,77 @@ export function Login() { }; return ( - -
-
- -

- Inventory Manager -

+ +
+
+
+
+
+ Cherry Bottom +
+ A Cherry On Bottom +
+

+ supporting the cherry on top +

- -
- - -
- -
- - Log in to continue - -
- -
-
-
- setUsername(e.target.value)} - disabled={isLoading} - className="w-full" - /> -
-
- setPassword(e.target.value)} - disabled={isLoading} - className="w-full" - /> -
- -
-
-
-
-
-
+
+ +
+ +
+ ); +} + +interface LoginFormProps { + className?: string; + isLoading?: boolean; + onSubmit: (e: React.FormEvent) => void; +} + +function LoginForm({ className, isLoading, onSubmit, ...props }: LoginFormProps) { + return ( + + + + Log in to your account + + +
+
+ + +
+ +
+ + +
+ + +
+ +
+
); } diff --git a/inventory/tailwind.config.js b/inventory/tailwind.config.js index c04f2f5..0e14d50 100644 --- a/inventory/tailwind.config.js +++ b/inventory/tailwind.config.js @@ -14,6 +14,9 @@ export default { } }, extend: { + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))',