mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: extract grid background
This commit is contained in:
25
apps/client/app/components/grid-background.tsx
Normal file
25
apps/client/app/components/grid-background.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface GridBackgroundProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export function GridBackground({ children, className, containerClassName }: GridBackgroundProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative min-h-dvh w-full overflow-x-hidden",
|
||||
"[background-size:20px_20px] sm:[background-size:40px_40px]",
|
||||
"[background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
||||
"dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||
containerClassName,
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-card" />
|
||||
<div className={cn("relative h-screen", className)}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import { Outlet, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { appContext } from "~/context";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { authMiddleware } from "~/middleware/auth";
|
||||
import type { Route } from "./+types/layout";
|
||||
import { AppBreadcrumb } from "./app-breadcrumb";
|
||||
import { GridBackground } from "./grid-background";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export const clientMiddleware = [authMiddleware];
|
||||
@@ -31,16 +31,8 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative min-h-dvh w-full overflow-x-hidden",
|
||||
"[background-size:20px_20px] sm:[background-size:40px_40px]",
|
||||
"[background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
||||
"dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-card"></div>
|
||||
<header className="relative bg-card-header border-b border-border/50">
|
||||
<GridBackground>
|
||||
<header className="bg-card-header border-b border-border/50">
|
||||
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-4 container mx-auto">
|
||||
<AppBreadcrumb />
|
||||
{loaderData.user && (
|
||||
@@ -55,9 +47,9 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="relative flex flex-col pt-4 sm:pt-8 px-2 sm:px-4 pb-4 container mx-auto">
|
||||
<main className="flex flex-col pt-4 sm:pt-8 px-2 sm:px-4 pb-4 container mx-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</GridBackground>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"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 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",
|
||||
"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 border bg-transparent px-3 py-1 text-sm 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,
|
||||
|
||||
@@ -60,97 +60,91 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <h1 className="text-2xl sm:text-3xl font-bold mb-0 uppercase">Ironmount</h1> */}
|
||||
{/* <h2 className="text-xs sm:text-sm font-semibold mb-2 text-muted-foreground"> */}
|
||||
{/* Create, manage, monitor, and automate your volumes with ease. */}
|
||||
{/* </h2> */}
|
||||
<Card className="p-0 gap-0">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:justify-between p-4 bg-card-header py-4">
|
||||
<span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Input
|
||||
className="w-full sm:w-[180px]"
|
||||
placeholder="Search volumes…"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="All status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mounted">Mounted</SelectItem>
|
||||
<SelectItem value="unmounted">Unmounted</SelectItem>
|
||||
<SelectItem value="error">Error</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="All backends" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
<SelectItem value="nfs">NFS</SelectItem>
|
||||
<SelectItem value="smb">SMB</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(searchQuery || statusFilter || backendFilter) && (
|
||||
<Button variant="outline" size="sm" onClick={clearFilters} className="w-full sm:w-auto">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Clear filters
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="border-t">
|
||||
<TableHeader className="bg-card-header">
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
||||
<TableHead className="uppercase hidden sm:table-cell">Mountpoint</TableHead>
|
||||
<TableHead className="uppercase text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredVolumes.map((volume) => (
|
||||
<TableRow
|
||||
key={volume.name}
|
||||
className="hover:bg-accent/50 hover:cursor-pointer"
|
||||
onClick={() => navigate(`/volumes/${volume.name}`)}
|
||||
>
|
||||
<TableCell className="font-medium text-strong-accent">{volume.name}</TableCell>
|
||||
<TableCell>
|
||||
<VolumeIcon backend={volume.type} />
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
|
||||
{volume.path}
|
||||
</span>
|
||||
<Copy size={10} />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<StatusDot status={volume.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-end border-t">
|
||||
{filteredVolumes.length === 0 ? (
|
||||
"No volumes found."
|
||||
) : (
|
||||
<span>
|
||||
<span className="text-strong-accent">{filteredVolumes.length}</span> volume
|
||||
{filteredVolumes.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
<Card className="p-0 gap-0">
|
||||
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4">
|
||||
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
|
||||
<Input
|
||||
className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]"
|
||||
placeholder="Search volumes…"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]">
|
||||
<SelectValue placeholder="All status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mounted">Mounted</SelectItem>
|
||||
<SelectItem value="unmounted">Unmounted</SelectItem>
|
||||
<SelectItem value="error">Error</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
||||
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mt-[-1px]">
|
||||
<SelectValue placeholder="All backends" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
<SelectItem value="nfs">NFS</SelectItem>
|
||||
<SelectItem value="smb">SMB</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(searchQuery || statusFilter || backendFilter) && (
|
||||
<Button onClick={clearFilters} className="w-full lg:w-auto mt-2 lg:mt-0 lg:ml-2">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Clear filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
</span>
|
||||
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="border-t">
|
||||
<TableHeader className="bg-card-header">
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
||||
<TableHead className="uppercase hidden sm:table-cell">Mountpoint</TableHead>
|
||||
<TableHead className="uppercase text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredVolumes.map((volume) => (
|
||||
<TableRow
|
||||
key={volume.name}
|
||||
className="hover:bg-accent/50 hover:cursor-pointer"
|
||||
onClick={() => navigate(`/volumes/${volume.name}`)}
|
||||
>
|
||||
<TableCell className="font-medium text-strong-accent">{volume.name}</TableCell>
|
||||
<TableCell>
|
||||
<VolumeIcon backend={volume.type} />
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
|
||||
{volume.path}
|
||||
</span>
|
||||
<Copy size={10} />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<StatusDot status={volume.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-end border-t">
|
||||
{filteredVolumes.length === 0 ? (
|
||||
"No volumes found."
|
||||
) : (
|
||||
<span>
|
||||
<span className="text-strong-accent">{filteredVolumes.length}</span> volume
|
||||
{filteredVolumes.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { GridBackground } from "~/components/grid-background";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
@@ -49,9 +50,8 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen flex items-center justify-center p-4 [background-size:20px_20px] sm:[background-size:40px_40px] [background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)] dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]">
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black" />
|
||||
<Card className="relative w-full max-w-md">
|
||||
<GridBackground className="flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
|
||||
<CardDescription>Sign in to your account</CardDescription>
|
||||
@@ -98,6 +98,6 @@ export default function LoginPage() {
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</GridBackground>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { GridBackground } from "~/components/grid-background";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
@@ -60,9 +61,8 @@ export default function OnboardingPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen flex items-center justify-center p-4 [background-size:20px_20px] sm:[background-size:40px_40px] [background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)] dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]">
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black" />
|
||||
<Card className="relative w-full max-w-md">
|
||||
<GridBackground className="flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle>
|
||||
<CardDescription>Create the admin user to get started</CardDescription>
|
||||
@@ -79,7 +79,7 @@ export default function OnboardingPage() {
|
||||
<FormControl>
|
||||
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
|
||||
</FormControl>
|
||||
<FormDescription>Choose a username for the admin account (2-50 characters).</FormDescription>
|
||||
<FormDescription>Choose a username for the admin account</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -128,6 +128,6 @@ export default function OnboardingPage() {
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</GridBackground>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user