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 { toast } from "sonner";
|
||||||
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import { appContext } from "~/context";
|
import { appContext } from "~/context";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { authMiddleware } from "~/middleware/auth";
|
import { authMiddleware } from "~/middleware/auth";
|
||||||
import type { Route } from "./+types/layout";
|
import type { Route } from "./+types/layout";
|
||||||
import { AppBreadcrumb } from "./app-breadcrumb";
|
import { AppBreadcrumb } from "./app-breadcrumb";
|
||||||
|
import { GridBackground } from "./grid-background";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export const clientMiddleware = [authMiddleware];
|
export const clientMiddleware = [authMiddleware];
|
||||||
@@ -31,16 +31,8 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<GridBackground>
|
||||||
className={cn(
|
<header className="bg-card-header border-b border-border/50">
|
||||||
"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">
|
|
||||||
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-4 container mx-auto">
|
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-4 container mx-auto">
|
||||||
<AppBreadcrumb />
|
<AppBreadcrumb />
|
||||||
{loaderData.user && (
|
{loaderData.user && (
|
||||||
@@ -55,9 +47,9 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</GridBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
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]",
|
"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",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -60,97 +60,91 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Card className="p-0 gap-0">
|
||||||
{/* <h1 className="text-2xl sm:text-3xl font-bold mb-0 uppercase">Ironmount</h1> */}
|
<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">
|
||||||
{/* <h2 className="text-xs sm:text-sm font-semibold mb-2 text-muted-foreground"> */}
|
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
|
||||||
{/* Create, manage, monitor, and automate your volumes with ease. */}
|
<Input
|
||||||
{/* </h2> */}
|
className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]"
|
||||||
<Card className="p-0 gap-0">
|
placeholder="Search volumes…"
|
||||||
<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">
|
value={searchQuery}
|
||||||
<span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<Input
|
/>
|
||||||
className="w-full sm:w-[180px]"
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
placeholder="Search volumes…"
|
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]">
|
||||||
value={searchQuery}
|
<SelectValue placeholder="All status" />
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
</SelectTrigger>
|
||||||
/>
|
<SelectContent>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<SelectItem value="mounted">Mounted</SelectItem>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectItem value="unmounted">Unmounted</SelectItem>
|
||||||
<SelectValue placeholder="All status" />
|
<SelectItem value="error">Error</SelectItem>
|
||||||
</SelectTrigger>
|
</SelectContent>
|
||||||
<SelectContent>
|
</Select>
|
||||||
<SelectItem value="mounted">Mounted</SelectItem>
|
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
||||||
<SelectItem value="unmounted">Unmounted</SelectItem>
|
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mt-[-1px]">
|
||||||
<SelectItem value="error">Error</SelectItem>
|
<SelectValue placeholder="All backends" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
<SelectItem value="directory">Directory</SelectItem>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectItem value="nfs">NFS</SelectItem>
|
||||||
<SelectValue placeholder="All backends" />
|
<SelectItem value="smb">SMB</SelectItem>
|
||||||
</SelectTrigger>
|
</SelectContent>
|
||||||
<SelectContent>
|
</Select>
|
||||||
<SelectItem value="directory">Directory</SelectItem>
|
{(searchQuery || statusFilter || backendFilter) && (
|
||||||
<SelectItem value="nfs">NFS</SelectItem>
|
<Button onClick={clearFilters} className="w-full lg:w-auto mt-2 lg:mt-0 lg:ml-2">
|
||||||
<SelectItem value="smb">SMB</SelectItem>
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
</SelectContent>
|
Clear filters
|
||||||
</Select>
|
</Button>
|
||||||
{(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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</span>
|
||||||
</Card>
|
<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 { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
|
import { GridBackground } from "~/components/grid-background";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
@@ -49,9 +50,8 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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)]">
|
<GridBackground className="flex items-center justify-center p-4">
|
||||||
<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="w-full max-w-md">
|
||||||
<Card className="relative w-full max-w-md">
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
|
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
|
||||||
<CardDescription>Sign in to your account</CardDescription>
|
<CardDescription>Sign in to your account</CardDescription>
|
||||||
@@ -98,6 +98,6 @@ export default function LoginPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</GridBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
|
import { GridBackground } from "~/components/grid-background";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
@@ -60,9 +61,8 @@ export default function OnboardingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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)]">
|
<GridBackground className="flex items-center justify-center p-4">
|
||||||
<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="w-full max-w-md">
|
||||||
<Card className="relative w-full max-w-md">
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle>
|
<CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle>
|
||||||
<CardDescription>Create the admin user to get started</CardDescription>
|
<CardDescription>Create the admin user to get started</CardDescription>
|
||||||
@@ -79,7 +79,7 @@ export default function OnboardingPage() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
|
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Choose a username for the admin account (2-50 characters).</FormDescription>
|
<FormDescription>Choose a username for the admin account</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -128,6 +128,6 @@ export default function OnboardingPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</GridBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user