mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d87814aee | ||
|
|
56a4afdc92 | ||
|
|
728cfebeb7 | ||
|
|
472f7799a4 | ||
|
|
e134d0e1d1 | ||
|
|
689f14dff7 | ||
|
|
8d46074bb1 |
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply bg-white dark:bg-[#0D0D0D];
|
@apply bg-white dark:bg-[#131313];
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -30,6 +30,7 @@ body {
|
|||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card-header: var(--card-header);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
@@ -57,6 +58,7 @@ body {
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-strong-accent: var(--strong-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -65,6 +67,7 @@ body {
|
|||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--card-header: oklch(0.922 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.205 0 0);
|
||||||
@@ -92,12 +95,14 @@ body {
|
|||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
--strong-accent: #ff543a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: #131313;
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.1448 0 0);
|
--card: #131313;
|
||||||
|
--card-header: #1b1b1b;
|
||||||
/* --card: oklch(0.205 0 0); ORIGINAL */
|
/* --card: oklch(0.205 0 0); ORIGINAL */
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.205 0 0);
|
||||||
@@ -111,7 +116,7 @@ body {
|
|||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: #ff543a;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
@@ -128,6 +133,7 @@ body {
|
|||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
--strong-accent: #ff543a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@@ -14,14 +14,20 @@ export function AppBreadcrumb() {
|
|||||||
const breadcrumbs = useBreadcrumbs();
|
const breadcrumbs = useBreadcrumbs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumb className={cn("mb-2", { invisible: breadcrumbs.length <= 1 })}>
|
<Breadcrumb>
|
||||||
|
<BreadcrumbLink asChild></BreadcrumbLink>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
|
{breadcrumbs.length === 1 && (
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<Link to="/">Ironmount</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
)}
|
||||||
{breadcrumbs.map((breadcrumb, index) => {
|
{breadcrumbs.map((breadcrumb, index) => {
|
||||||
const isLast = index === breadcrumbs.length - 1;
|
const isLast = index === breadcrumbs.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${breadcrumb.label}-${index}`} className="contents">
|
<div key={`${breadcrumb.label}-${index}`} className="contents">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem className={cn({ invisible: breadcrumbs.length <= 1 })}>
|
||||||
{isLast || breadcrumb.isCurrentPage ? (
|
{isLast || breadcrumb.isCurrentPage ? (
|
||||||
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
||||||
) : breadcrumb.href ? (
|
) : breadcrumb.href ? (
|
||||||
|
|||||||
@@ -6,15 +6,7 @@ import { createVolumeMutation } from "~/api-client/@tanstack/react-query.gen";
|
|||||||
import { parseError } from "~/lib/errors";
|
import { parseError } from "~/lib/errors";
|
||||||
import { CreateVolumeForm } from "./create-volume-form";
|
import { CreateVolumeForm } from "./create-volume-form";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "./ui/dialog";
|
|
||||||
import { ScrollArea } from "./ui/scroll-area";
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -41,7 +33,7 @@ export const CreateVolumeDialog = ({ open, setOpen }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-blue-900 hover:bg-blue-800">
|
<Button>
|
||||||
<Plus size={16} className="mr-2" />
|
<Plus size={16} className="mr-2" />
|
||||||
Create volume
|
Create volume
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -53,22 +53,21 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] });
|
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] });
|
||||||
}, [watchedBackend, watchedName, form.reset]);
|
}, [watchedBackend, watchedName, form.reset]);
|
||||||
|
|
||||||
const [testMessage, setTestMessage] = useState<string>("");
|
const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
const testBackendConnection = useMutation({
|
const testBackendConnection = useMutation({
|
||||||
...testConnectionMutation(),
|
...testConnectionMutation(),
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setTestMessage("");
|
setTestMessage(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error) => {
|
||||||
setTestMessage("Failed to test connection. Please try again.");
|
setTestMessage({
|
||||||
|
success: false,
|
||||||
|
message: error?.message || "Failed to test connection. Please try again.",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data?.success) {
|
setTestMessage(data);
|
||||||
setTestMessage(data.message);
|
|
||||||
} else {
|
|
||||||
setTestMessage(data?.message || "Connection test failed");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -424,112 +423,45 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{watchedBackend === "smb" && (
|
<div className="space-y-3">
|
||||||
<div className="space-y-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
variant="outline"
|
||||||
variant="outline"
|
onClick={handleTestConnection}
|
||||||
onClick={handleTestConnection}
|
disabled={testBackendConnection.isPending}
|
||||||
disabled={testBackendConnection.isPending}
|
className="flex-1"
|
||||||
className="flex-1"
|
>
|
||||||
>
|
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{!testBackendConnection.isPending && testMessage?.success && (
|
||||||
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
|
||||||
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
)}
|
||||||
{testBackendConnection.isIdle && "Test Connection"}
|
{!testBackendConnection.isPending && testMessage && !testMessage.success && (
|
||||||
{testBackendConnection.isPending && "Testing..."}
|
<XCircle className="mr-2 h-4 w-4 text-red-500" />
|
||||||
{testBackendConnection.isSuccess && "Connection Successful"}
|
)}
|
||||||
{testBackendConnection.isError && "Test Failed"}
|
{testBackendConnection.isPending
|
||||||
</Button>
|
? "Testing..."
|
||||||
</div>
|
: testMessage
|
||||||
{testMessage && (
|
? testMessage.success
|
||||||
<div
|
? "Connection Successful"
|
||||||
className={`text-sm p-2 rounded-md ${
|
: "Test Failed"
|
||||||
testBackendConnection.isSuccess
|
: "Test Connection"}
|
||||||
? "bg-green-50 text-green-700 border border-green-200"
|
</Button>
|
||||||
: testBackendConnection.isError
|
|
||||||
? "bg-red-50 text-red-700 border border-red-200"
|
|
||||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{testMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{testMessage && (
|
||||||
|
<div
|
||||||
{watchedBackend === "nfs" && (
|
className={`text-xs p-2 rounded-md ${
|
||||||
<div className="space-y-3">
|
testMessage.success
|
||||||
<div className="flex items-center gap-2">
|
? "bg-green-50 text-green-700 border border-green-200"
|
||||||
<Button
|
: "bg-red-50 text-red-700 border border-red-200"
|
||||||
type="button"
|
}`}
|
||||||
variant="outline"
|
>
|
||||||
onClick={handleTestConnection}
|
{testMessage.message}
|
||||||
disabled={testBackendConnection.isPending}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
|
||||||
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
|
||||||
{testBackendConnection.isIdle && "Test Connection"}
|
|
||||||
{testBackendConnection.isPending && "Testing..."}
|
|
||||||
{testBackendConnection.isSuccess && "Connection Successful"}
|
|
||||||
{testBackendConnection.isError && "Test Failed"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{testMessage && (
|
)}
|
||||||
<div
|
</div>
|
||||||
className={`text-sm p-2 rounded-md ${
|
|
||||||
testBackendConnection.isSuccess
|
|
||||||
? "bg-green-50 text-green-700 border border-green-200"
|
|
||||||
: testBackendConnection.isError
|
|
||||||
? "bg-red-50 text-red-700 border border-red-200"
|
|
||||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{testMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedBackend === "webdav" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleTestConnection}
|
|
||||||
disabled={testBackendConnection.isPending}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
|
||||||
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
|
||||||
{testBackendConnection.isIdle && "Test Connection"}
|
|
||||||
{testBackendConnection.isPending && "Testing..."}
|
|
||||||
{testBackendConnection.isSuccess && "Connection Successful"}
|
|
||||||
{testBackendConnection.isError && "Test Failed"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{testMessage && (
|
|
||||||
<div
|
|
||||||
className={`text-sm p-2 rounded-md ${
|
|
||||||
testBackendConnection.isSuccess
|
|
||||||
? "bg-green-50 text-green-700 border border-green-200"
|
|
||||||
: testBackendConnection.isError
|
|
||||||
? "bg-red-50 text-red-700 border border-red-200"
|
|
||||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{testMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{mode === "update" && (
|
{mode === "update" && (
|
||||||
<Button type="submit" className="w-full mt-4" loading={loading}>
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
56
apps/client/app/components/empty-state.tsx
Normal file
56
apps/client/app/components/empty-state.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Database, HardDrive, HeartPulse, Plus } from "lucide-react";
|
||||||
|
import { CreateVolumeDialog } from "./create-volume-dialog";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function EmptyState() {
|
||||||
|
const [createVolumeOpen, setCreateVolumeOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||||
|
<div className="relative mb-8">
|
||||||
|
<div className="absolute inset-0 animate-pulse">
|
||||||
|
<div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
|
||||||
|
<Database className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-md space-y-3 mb-8">
|
||||||
|
<h3 className="text-2xl font-semibold text-foreground">No volumes yet</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Get started by creating your first volume. Manage and monitor all your storage backends in one place with
|
||||||
|
advanced features like automatic mounting and health checks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||||
|
|
||||||
|
<div className="mt-12 grid grid-cols-1 md:grid-cols-3 gap-0 max-w-3xl">
|
||||||
|
<div className="flex flex-col items-center gap-2 p-4 border bg-card-header">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<Database className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium text-sm">Multiple Backends</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">Support for local, NFS, and SMB storage</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2 p-4 border border-r-0 border-l-0 bg-card-header">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<HardDrive className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium text-sm">Auto Mounting</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">Automatic lifecycle management</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2 p-4 border bg-card-header">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<HeartPulse className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium text-sm">Real-time Monitoring</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">Live status and health checks</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Outlet, useNavigate } from "react-router";
|
import { Outlet, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { AppBreadcrumb } from "./app-breadcrumb";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import type { Route } from "./+types/layout";
|
|
||||||
import { appContext } from "~/context";
|
import { appContext } from "~/context";
|
||||||
import { authMiddleware } from "~/middleware/auth";
|
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];
|
export const clientMiddleware = [authMiddleware];
|
||||||
|
|
||||||
@@ -31,29 +31,25 @@ 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",
|
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-4 container mx-auto">
|
||||||
"[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"></div>
|
|
||||||
<main className="relative flex flex-col pt-4 sm:pt-8 px-2 sm:px-4 pb-4 container mx-auto">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<AppBreadcrumb />
|
<AppBreadcrumb />
|
||||||
{loaderData.user && (
|
{loaderData.user && (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-muted-foreground">Welcome, {loaderData.user?.username}</span>
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Welcome, <span className="text-strong-accent">{loaderData.user?.username}</span>
|
||||||
|
</span>
|
||||||
<Button variant="outline" size="sm" onClick={() => logout.mutate({})} loading={logout.isPending}>
|
<Button variant="outline" size="sm" onClick={() => logout.mutate({})} loading={logout.isPending}>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ import type * as React from "react";
|
|||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex cursor-pointer uppercase border items-center justify-center dark:border-white dark:bg-secondary dark:text-white gap-2 whitespace-nowrap 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 cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide 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:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
default: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80",
|
||||||
|
primary: "bg-strong-accent text-white hover:bg-strong-accent/90 focus-visible:ring-strong-accent/50",
|
||||||
destructive:
|
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",
|
"border border-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50 text-destructive hover:text-white",
|
||||||
outline:
|
outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
secondary: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80",
|
||||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: "h-9 px-5 py-2 has-[>svg]:px-4",
|
||||||
sm: "rounded-xs h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
|
sm: "h-8 px-3 py-1.5 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 px-6 has-[>svg]:px-4",
|
lg: "h-10 px-6 py-2.5 has-[>svg]:px-5",
|
||||||
icon: "size-9",
|
icon: "size-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({ className, children, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn("bg-card text-card-foreground relative flex flex-col gap-6 border-2 py-6 shadow-sm", className)}
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 border-2 py-6 shadow-sm",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
<span aria-hidden="true" className="pointer-events-none absolute inset-0 z-10 select-none">
|
||||||
|
<span className="absolute left-[-2px] top-[-2px] h-0.5 w-4 bg-white/80" />
|
||||||
|
<span className="absolute left-[-2px] top-[-2px] h-4 w-0.5 bg-white/80" />
|
||||||
|
<span className="absolute right-[-2px] top-[-2px] h-0.5 w-4 bg-white/80" />
|
||||||
|
<span className="absolute right-[-2px] top-[-2px] h-4 w-0.5 bg-white/80" />
|
||||||
|
<span className="absolute left-[-2px] bottom-[-2px] h-0.5 w-4 bg-white/80" />
|
||||||
|
<span className="absolute left-[-2px] bottom-[-2px] h-4 w-0.5 bg-white/80" />
|
||||||
|
<span className="absolute right-[-2px] bottom-[-2px] h-0.5 w-4 bg-white/80" />
|
||||||
|
<span className="absolute right-[-2px] bottom-[-2px] h-4 w-0.5 bg-white/80" />
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,64 +38,31 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn("leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn(
|
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
|
||||||
<div
|
|
||||||
data-slot="card-content"
|
|
||||||
className={cn("px-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import type React from "react";
|
||||||
import Prism from "prismjs";
|
|
||||||
import "prismjs/themes/prism-twilight.css";
|
|
||||||
import "prismjs/components/prism-yaml";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { copyToClipboard } from "~/utils/clipboard";
|
import { copyToClipboard } from "~/utils/clipboard";
|
||||||
|
|
||||||
@@ -11,35 +8,31 @@ interface CodeBlockProps {
|
|||||||
filename?: string;
|
filename?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = "jsx", filename }) => {
|
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, filename }) => {
|
||||||
useEffect(() => {
|
|
||||||
Prism.highlightAll();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
await copyToClipboard(code);
|
await copyToClipboard(code);
|
||||||
toast.success("Code copied to clipboard");
|
toast.success("Code copied to clipboard");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-sm bg-slate-900 ring-1 ring-white/10">
|
<div className="overflow-hidden rounded-sm bg-card-header ring-1 ring-white/10">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs text-slate-400">
|
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
|
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
|
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
||||||
{filename && <span className="ml-3 font-medium text-slate-300">{filename}</span>}
|
{filename && <span className="ml-3 font-medium">{filename}</span>}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleCopy()}
|
onClick={() => handleCopy()}
|
||||||
className="cursor-pointer rounded-md bg-white/5 px-2 py-1 text-[11px] font-medium text-slate-300 ring-1 ring-inset ring-white/10 transition hover:bg-white/10 active:translate-y-px"
|
className="cursor-pointer rounded-md bg-white/5 px-2 py-1 text-[11px] font-medium ring-1 ring-inset ring-white/10 transition hover:bg-white/10 active:translate-y-px"
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="overflow-x-auto leading-6 text-xs m-0" style={{ marginTop: 0, marginBottom: 0 }}>
|
<pre className="text-xs m-0 px-4 py-2 bg-card-header">
|
||||||
<code className={`language-${language}`}>{code}</code>
|
<code className="text-white/80">{code}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,64 +1,58 @@
|
|||||||
import * as React from "react"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
className,
|
return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
className,
|
return (
|
||||||
...props
|
<TabsPrimitive.List
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
data-slot="tabs-list"
|
||||||
return (
|
className={cn("inline-flex h-7 items-center gap-4 text-xs text-muted-foreground", className)}
|
||||||
<TabsPrimitive.List
|
{...props}
|
||||||
data-slot="tabs-list"
|
/>
|
||||||
className={cn(
|
);
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
className,
|
return (
|
||||||
...props
|
<TabsPrimitive.Trigger
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
data-slot="tabs-trigger"
|
||||||
return (
|
className={cn(
|
||||||
<TabsPrimitive.Trigger
|
"cursor-pointer group relative inline-flex h-7 items-center whitespace-nowrap text-xs font-medium transition-colors",
|
||||||
data-slot="tabs-trigger"
|
"text-muted-foreground data-[state=active]:text-foreground disabled:pointer-events-none disabled:opacity-50",
|
||||||
className={cn(
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
className
|
// Padding: 20px horizontal (8px for bracket tick + 12px gap to text)
|
||||||
)}
|
"px-5",
|
||||||
{...props}
|
// Transparent orange background for active state
|
||||||
/>
|
"data-[state=active]:bg-[#FF453A]/10",
|
||||||
)
|
// Left bracket - vertical line
|
||||||
|
"before:absolute before:left-0 before:top-0 before:h-7 before:w-0.5 before:bg-[#5D6570] before:transition-colors data-[state=active]:before:bg-[#FF453A]",
|
||||||
|
// Left bracket - top tick
|
||||||
|
"after:absolute after:left-0 after:top-[-1px] after:w-2 after:h-0.5 after:bg-[#5D6570] after:transition-colors data-[state=active]:after:bg-[#FF453A]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="relative z-10">{props.children}</span>
|
||||||
|
{/* Left bracket - bottom tick */}
|
||||||
|
<span className="absolute left-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
{/* Right bracket - top tick */}
|
||||||
|
<span className="absolute right-0 top-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
{/* Right bracket - vertical line */}
|
||||||
|
<span className="absolute right-0 top-0 h-7 w-0.5 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
{/* Right bracket - bottom tick */}
|
||||||
|
<span className="absolute right-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
</TabsPrimitive.Trigger>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
className,
|
return <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} />;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ const getIconAndColor = (backend: BackendType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const VolumeIcon = ({ backend, size = 10 }: VolumeIconProps) => {
|
export const VolumeIcon = ({ backend, size = 10 }: VolumeIconProps) => {
|
||||||
const { icon: Icon, color, label } = getIconAndColor(backend);
|
const { icon: Icon, label } = getIconAndColor(backend);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`flex items-center gap-2 ${color} rounded-md px-2 py-1`}>
|
<span className={`flex items-center gap-2 rounded-md px-2 py-1`}>
|
||||||
<Icon size={size} />
|
<Icon size={size} />
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function StorageChart({ statfs }: Props) {
|
|||||||
{
|
{
|
||||||
name: "Used",
|
name: "Used",
|
||||||
value: statfs.used,
|
value: statfs.used,
|
||||||
fill: "#2B7EFF",
|
fill: "#ff543a",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Free",
|
name: "Free",
|
||||||
@@ -63,7 +63,7 @@ export function StorageChart({ statfs }: Props) {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 pb-0">
|
<CardContent className="flex-1 pb-0">
|
||||||
<div className="">
|
<div>
|
||||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
@@ -105,9 +105,9 @@ export function StorageChart({ statfs }: Props) {
|
|||||||
<ByteSize bytes={statfs.total} className="font-mono text-sm" />
|
<ByteSize bytes={statfs.total} className="font-mono text-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-3 rounded-lg bg-blue-500/10">
|
<div className="flex items-center justify-between p-3 rounded-lg bg-strong-accent/10">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-4 w-4 rounded-full bg-blue-500" />
|
<div className="h-4 w-4 rounded-full bg-strong-accent" />
|
||||||
<span className="font-medium">Used Space</span>
|
<span className="font-medium">Used Space</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
|
||||||
onClick={() => mountVol.mutate({ path: { name } })}
|
onClick={() => mountVol.mutate({ path: { name } })}
|
||||||
loading={mountVol.isPending}
|
loading={mountVol.isPending}
|
||||||
className={cn({ hidden: volume.status === "mounted" })}
|
className={cn({ hidden: volume.status === "mounted" })}
|
||||||
@@ -131,8 +130,8 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="info" className="mt-0">
|
<Tabs defaultValue="info" className="mt-4">
|
||||||
<TabsList>
|
<TabsList className="mb-2">
|
||||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { useNavigate } from "react-router";
|
|||||||
import { listVolumes } from "~/api-client";
|
import { listVolumes } from "~/api-client";
|
||||||
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
||||||
|
import { EmptyState } from "~/components/empty-state";
|
||||||
import { StatusDot } from "~/components/status-dot";
|
import { StatusDot } from "~/components/status-dot";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card } from "~/components/ui/card";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||||
import { VolumeIcon } from "~/components/volume-icon";
|
import { VolumeIcon } from "~/components/volume-icon";
|
||||||
import type { Route } from "./+types/home";
|
import type { Route } from "./+types/home";
|
||||||
|
|
||||||
@@ -58,22 +60,29 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
return matchesSearch && matchesStatus && matchesBackend;
|
return matchesSearch && matchesStatus && matchesBackend;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
const hasNoVolumes = data?.volumes.length === 0;
|
||||||
|
const hasNoFilteredVolumes = filteredVolumes.length === 0 && !hasNoVolumes;
|
||||||
|
|
||||||
|
if (hasNoVolumes) {
|
||||||
|
return (
|
||||||
|
<Card className="p-0 gap-0">
|
||||||
|
<EmptyState />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mt-4 sm:justify-between">
|
|
||||||
<span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
|
||||||
<Input
|
<Input
|
||||||
className="w-full sm:w-[180px]"
|
className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]"
|
||||||
placeholder="Search volumes…"
|
placeholder="Search volumes…"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]">
|
||||||
<SelectValue placeholder="All status" />
|
<SelectValue placeholder="All status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -83,7 +92,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mt-[-1px]">
|
||||||
<SelectValue placeholder="All backends" />
|
<SelectValue placeholder="All backends" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -93,7 +102,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{(searchQuery || statusFilter || backendFilter) && (
|
{(searchQuery || statusFilter || backendFilter) && (
|
||||||
<Button variant="outline" size="sm" onClick={clearFilters} className="w-full sm:w-auto">
|
<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" />
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
Clear filters
|
Clear filters
|
||||||
</Button>
|
</Button>
|
||||||
@@ -101,10 +110,9 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
</span>
|
</span>
|
||||||
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table className="border bg-white dark:bg-secondary">
|
<Table className="border-t">
|
||||||
<TableCaption>A list of your managed volumes.</TableCaption>
|
<TableHeader className="bg-card-header">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
||||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
<TableHead className="uppercase text-left">Backend</TableHead>
|
||||||
@@ -113,32 +121,56 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredVolumes.map((volume) => (
|
{hasNoFilteredVolumes ? (
|
||||||
<TableRow
|
<TableRow>
|
||||||
key={volume.name}
|
<TableCell colSpan={4} className="text-center py-12">
|
||||||
className="hover:bg-accent/50 hover:cursor-pointer"
|
<div className="flex flex-col items-center gap-3">
|
||||||
onClick={() => navigate(`/volumes/${volume.name}`)}
|
<p className="text-muted-foreground">No volumes match your filters.</p>
|
||||||
>
|
<Button onClick={clearFilters} variant="outline" size="sm">
|
||||||
<TableCell className="font-medium">{volume.name}</TableCell>
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
<TableCell>
|
Clear filters
|
||||||
<VolumeIcon backend={volume.type} />
|
</Button>
|
||||||
</TableCell>
|
</div>
|
||||||
<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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
) : (
|
||||||
|
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>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-end border-t">
|
||||||
|
{hasNoFilteredVolumes ? (
|
||||||
|
"No volumes match filters."
|
||||||
|
) : (
|
||||||
|
<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,7 +50,7 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4">
|
<GridBackground className="flex items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="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>
|
||||||
@@ -97,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,7 +61,7 @@ export default function OnboardingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4">
|
<GridBackground className="flex items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="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>
|
||||||
@@ -78,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>
|
||||||
)}
|
)}
|
||||||
@@ -127,6 +128,6 @@ export default function OnboardingPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</GridBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"prismjs": "^1.30.0",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.63.0",
|
"react-hook-form": "^7.63.0",
|
||||||
@@ -46,11 +45,11 @@
|
|||||||
"@react-router/dev": "^7.9.3",
|
"@react-router/dev": "^7.9.3",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/node": "^24.6.2",
|
"@types/node": "^24.6.2",
|
||||||
"@types/prismjs": "^1.26.5",
|
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"lightningcss": "^1.30.2",
|
"lightningcss": "^1.30.2",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.9",
|
"vite": "^7.1.9",
|
||||||
|
|||||||
7
bun.lock
7
bun.lock
@@ -34,7 +34,6 @@
|
|||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"prismjs": "^1.30.0",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.63.0",
|
"react-hook-form": "^7.63.0",
|
||||||
@@ -48,11 +47,11 @@
|
|||||||
"@react-router/dev": "^7.9.3",
|
"@react-router/dev": "^7.9.3",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/node": "^24.6.2",
|
"@types/node": "^24.6.2",
|
||||||
"@types/prismjs": "^1.26.5",
|
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"lightningcss": "^1.30.2",
|
"lightningcss": "^1.30.2",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.9",
|
"vite": "^7.1.9",
|
||||||
@@ -543,8 +542,6 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
"@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
||||||
|
|
||||||
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
|
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="],
|
"@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="],
|
||||||
@@ -1033,8 +1030,6 @@
|
|||||||
|
|
||||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
|
||||||
|
|
||||||
"proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="],
|
"proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="],
|
||||||
|
|
||||||
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
|
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user