sachnun commited on
Commit
fb70774
·
1 Parent(s): 72a843b

Update styling, utilities, routes, and add UI components

Browse files
.mcp.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "shadcn": {
4
+ "command": "npx",
5
+ "args": [
6
+ "shadcn@latest",
7
+ "mcp"
8
+ ]
9
+ }
10
+ }
11
+ }
app/app.css CHANGED
@@ -1,4 +1,7 @@
1
  @import "tailwindcss";
 
 
 
2
 
3
  @theme {
4
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
@@ -13,3 +16,119 @@ body {
13
  color-scheme: dark;
14
  }
15
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
 
6
  @theme {
7
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
 
16
  color-scheme: dark;
17
  }
18
  }
19
+
20
+ @theme inline {
21
+ --radius-sm: calc(var(--radius) - 4px);
22
+ --radius-md: calc(var(--radius) - 2px);
23
+ --radius-lg: var(--radius);
24
+ --radius-xl: calc(var(--radius) + 4px);
25
+ --color-background: var(--background);
26
+ --color-foreground: var(--foreground);
27
+ --color-card: var(--card);
28
+ --color-card-foreground: var(--card-foreground);
29
+ --color-popover: var(--popover);
30
+ --color-popover-foreground: var(--popover-foreground);
31
+ --color-primary: var(--primary);
32
+ --color-primary-foreground: var(--primary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-secondary-foreground: var(--secondary-foreground);
35
+ --color-muted: var(--muted);
36
+ --color-muted-foreground: var(--muted-foreground);
37
+ --color-accent: var(--accent);
38
+ --color-accent-foreground: var(--accent-foreground);
39
+ --color-destructive: var(--destructive);
40
+ --color-border: var(--border);
41
+ --color-input: var(--input);
42
+ --color-ring: var(--ring);
43
+ --color-chart-1: var(--chart-1);
44
+ --color-chart-2: var(--chart-2);
45
+ --color-chart-3: var(--chart-3);
46
+ --color-chart-4: var(--chart-4);
47
+ --color-chart-5: var(--chart-5);
48
+ --color-sidebar: var(--sidebar);
49
+ --color-sidebar-foreground: var(--sidebar-foreground);
50
+ --color-sidebar-primary: var(--sidebar-primary);
51
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
52
+ --color-sidebar-accent: var(--sidebar-accent);
53
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
54
+ --color-sidebar-border: var(--sidebar-border);
55
+ --color-sidebar-ring: var(--sidebar-ring);
56
+ }
57
+
58
+ :root {
59
+ --radius: 0.625rem;
60
+ --background: oklch(1 0 0);
61
+ --foreground: oklch(0.141 0.005 285.823);
62
+ --card: oklch(1 0 0);
63
+ --card-foreground: oklch(0.141 0.005 285.823);
64
+ --popover: oklch(1 0 0);
65
+ --popover-foreground: oklch(0.141 0.005 285.823);
66
+ --primary: oklch(0.21 0.006 285.885);
67
+ --primary-foreground: oklch(0.985 0 0);
68
+ --secondary: oklch(0.967 0.001 286.375);
69
+ --secondary-foreground: oklch(0.21 0.006 285.885);
70
+ --muted: oklch(0.967 0.001 286.375);
71
+ --muted-foreground: oklch(0.552 0.016 285.938);
72
+ --accent: oklch(0.967 0.001 286.375);
73
+ --accent-foreground: oklch(0.21 0.006 285.885);
74
+ --destructive: oklch(0.577 0.245 27.325);
75
+ --border: oklch(0.92 0.004 286.32);
76
+ --input: oklch(0.92 0.004 286.32);
77
+ --ring: oklch(0.705 0.015 286.067);
78
+ --chart-1: oklch(0.646 0.222 41.116);
79
+ --chart-2: oklch(0.6 0.118 184.704);
80
+ --chart-3: oklch(0.398 0.07 227.392);
81
+ --chart-4: oklch(0.828 0.189 84.429);
82
+ --chart-5: oklch(0.769 0.188 70.08);
83
+ --sidebar: oklch(0.985 0 0);
84
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
85
+ --sidebar-primary: oklch(0.21 0.006 285.885);
86
+ --sidebar-primary-foreground: oklch(0.985 0 0);
87
+ --sidebar-accent: oklch(0.967 0.001 286.375);
88
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
89
+ --sidebar-border: oklch(0.92 0.004 286.32);
90
+ --sidebar-ring: oklch(0.705 0.015 286.067);
91
+ }
92
+
93
+ .dark {
94
+ --background: oklch(0.141 0.005 285.823);
95
+ --foreground: oklch(0.985 0 0);
96
+ --card: oklch(0.21 0.006 285.885);
97
+ --card-foreground: oklch(0.985 0 0);
98
+ --popover: oklch(0.21 0.006 285.885);
99
+ --popover-foreground: oklch(0.985 0 0);
100
+ --primary: oklch(0.92 0.004 286.32);
101
+ --primary-foreground: oklch(0.21 0.006 285.885);
102
+ --secondary: oklch(0.274 0.006 286.033);
103
+ --secondary-foreground: oklch(0.985 0 0);
104
+ --muted: oklch(0.274 0.006 286.033);
105
+ --muted-foreground: oklch(0.705 0.015 286.067);
106
+ --accent: oklch(0.274 0.006 286.033);
107
+ --accent-foreground: oklch(0.985 0 0);
108
+ --destructive: oklch(0.704 0.191 22.216);
109
+ --border: oklch(1 0 0 / 10%);
110
+ --input: oklch(1 0 0 / 15%);
111
+ --ring: oklch(0.552 0.016 285.938);
112
+ --chart-1: oklch(0.488 0.243 264.376);
113
+ --chart-2: oklch(0.696 0.17 162.48);
114
+ --chart-3: oklch(0.769 0.188 70.08);
115
+ --chart-4: oklch(0.627 0.265 303.9);
116
+ --chart-5: oklch(0.645 0.246 16.439);
117
+ --sidebar: oklch(0.21 0.006 285.885);
118
+ --sidebar-foreground: oklch(0.985 0 0);
119
+ --sidebar-primary: oklch(0.488 0.243 264.376);
120
+ --sidebar-primary-foreground: oklch(0.985 0 0);
121
+ --sidebar-accent: oklch(0.274 0.006 286.033);
122
+ --sidebar-accent-foreground: oklch(0.985 0 0);
123
+ --sidebar-border: oklch(1 0 0 / 10%);
124
+ --sidebar-ring: oklch(0.552 0.016 285.938);
125
+ }
126
+
127
+ @layer base {
128
+ * {
129
+ @apply border-border outline-ring/50;
130
+ }
131
+ body {
132
+ @apply bg-background text-foreground;
133
+ }
134
+ }
app/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "~/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
app/components/ui/avatar.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
3
+
4
+ import { cn } from "~/lib/utils"
5
+
6
+ function Avatar({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
10
+ return (
11
+ <AvatarPrimitive.Root
12
+ data-slot="avatar"
13
+ className={cn(
14
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function AvatarImage({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
26
+ return (
27
+ <AvatarPrimitive.Image
28
+ data-slot="avatar-image"
29
+ className={cn("aspect-square size-full", className)}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function AvatarFallback({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
39
+ return (
40
+ <AvatarPrimitive.Fallback
41
+ data-slot="avatar-fallback"
42
+ className={cn(
43
+ "bg-muted flex size-full items-center justify-center rounded-full",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ export { Avatar, AvatarImage, AvatarFallback }
app/components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "~/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
app/components/ui/dialog.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { XIcon } from "lucide-react"
6
+
7
+ import { cn } from "~/lib/utils"
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ showCloseButton = true,
53
+ ...props
54
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
55
+ showCloseButton?: boolean
56
+ }) {
57
+ return (
58
+ <DialogPortal data-slot="dialog-portal">
59
+ <DialogOverlay />
60
+ <DialogPrimitive.Content
61
+ data-slot="dialog-content"
62
+ className={cn(
63
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {showCloseButton && (
70
+ <DialogPrimitive.Close
71
+ data-slot="dialog-close"
72
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
73
+ >
74
+ <XIcon />
75
+ <span className="sr-only">Close</span>
76
+ </DialogPrimitive.Close>
77
+ )}
78
+ </DialogPrimitive.Content>
79
+ </DialogPortal>
80
+ )
81
+ }
82
+
83
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="dialog-header"
87
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94
+ return (
95
+ <div
96
+ data-slot="dialog-footer"
97
+ className={cn(
98
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
99
+ className
100
+ )}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ function DialogTitle({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
110
+ return (
111
+ <DialogPrimitive.Title
112
+ data-slot="dialog-title"
113
+ className={cn("text-lg leading-none font-semibold", className)}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function DialogDescription({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
123
+ return (
124
+ <DialogPrimitive.Description
125
+ data-slot="dialog-description"
126
+ className={cn("text-muted-foreground text-sm", className)}
127
+ {...props}
128
+ />
129
+ )
130
+ }
131
+
132
+ export {
133
+ Dialog,
134
+ DialogClose,
135
+ DialogContent,
136
+ DialogDescription,
137
+ DialogFooter,
138
+ DialogHeader,
139
+ DialogOverlay,
140
+ DialogPortal,
141
+ DialogTitle,
142
+ DialogTrigger,
143
+ }
app/components/ui/separator.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
3
+
4
+ import { cn } from "~/lib/utils"
5
+
6
+ function Separator({
7
+ className,
8
+ orientation = "horizontal",
9
+ decorative = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12
+ return (
13
+ <SeparatorPrimitive.Root
14
+ data-slot="separator"
15
+ decorative={decorative}
16
+ orientation={orientation}
17
+ className={cn(
18
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ export { Separator }
app/lib/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ClassValue, clsx } from "clsx"
2
  import { twMerge } from "tailwind-merge"
3
 
4
  export function cn(...inputs: ClassValue[]) {
 
1
+ import { clsx, type ClassValue } from "clsx"
2
  import { twMerge } from "tailwind-merge"
3
 
4
  export function cn(...inputs: ClassValue[]) {
app/root.tsx CHANGED
@@ -9,6 +9,9 @@ import {
9
 
10
  import type { Route } from "./+types/root";
11
  import "./app.css";
 
 
 
12
 
13
  export const links: Route.LinksFunction = () => [
14
  { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
@@ -63,14 +66,25 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
63
  }
64
 
65
  return (
66
- <main className="pt-16 p-4 container mx-auto">
67
- <h1>{message}</h1>
68
- <p>{details}</p>
69
- {stack && (
70
- <pre className="w-full p-4 overflow-x-auto">
71
- <code>{stack}</code>
72
- </pre>
73
- )}
74
- </main>
 
 
 
 
 
 
 
 
 
 
 
75
  );
76
  }
 
9
 
10
  import type { Route } from "./+types/root";
11
  import "./app.css";
12
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
13
+ import { Alert, AlertDescription } from "~/components/ui/alert";
14
+ import { AlertCircle } from "lucide-react";
15
 
16
  export const links: Route.LinksFunction = () => [
17
  { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
 
66
  }
67
 
68
  return (
69
+ <div className="min-h-screen bg-background flex items-center justify-center p-4">
70
+ <Card className="max-w-lg w-full">
71
+ <CardHeader>
72
+ <CardTitle className="text-2xl">{message}</CardTitle>
73
+ <CardDescription>{details}</CardDescription>
74
+ </CardHeader>
75
+ {stack && (
76
+ <CardContent>
77
+ <Alert>
78
+ <AlertCircle className="h-4 w-4" />
79
+ <AlertDescription>
80
+ <pre className="text-xs overflow-x-auto mt-2 p-2 bg-muted rounded">
81
+ <code>{stack}</code>
82
+ </pre>
83
+ </AlertDescription>
84
+ </Alert>
85
+ </CardContent>
86
+ )}
87
+ </Card>
88
+ </div>
89
  );
90
  }
app/routes/home.tsx CHANGED
@@ -1,6 +1,9 @@
1
  import type { Route } from "./+types/home";
2
  import { Link } from "react-router";
3
  import { useState, useEffect } from "react";
 
 
 
4
 
5
  export function meta({}: Route.MetaArgs) {
6
  return [
@@ -22,84 +25,97 @@ export default function Home() {
22
  // Show "Open in New Tab" message if in iframe
23
  if (isInIframe) {
24
  return (
25
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center">
26
- <div className="max-w-md mx-auto p-8 bg-white dark:bg-gray-800 rounded-lg shadow-2xl text-center">
27
- <div className="text-6xl mb-6">🔗</div>
28
- <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
29
- Open in New Tab
30
- </h2>
31
- <p className="text-gray-600 dark:text-gray-300 mb-8">
32
- For the best experience, please open this application in a new tab.
33
- </p>
34
- <a
35
- href={window.location.href}
36
- target="_blank"
37
- rel="noopener noreferrer"
38
- className="inline-flex items-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg shadow-lg transition-all transform hover:scale-105"
39
- >
40
- Open in New Tab
41
- <svg className="ml-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
43
- </svg>
44
- </a>
45
- </div>
 
 
 
46
  </div>
47
  );
48
  }
49
 
50
  return (
51
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
52
- <div className="container mx-auto px-4 py-16">
53
- <div className="max-w-4xl mx-auto text-center">
54
- <h1 className="text-6xl font-bold text-gray-900 dark:text-white mb-6">
55
- Always Uptime
56
- </h1>
57
- <p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
58
- Monitor your services 24/7 with real-time uptime checks every 30 seconds
59
- </p>
60
-
61
- <div className="flex justify-center gap-4 mb-16">
62
- <Link
63
- to="/login"
64
- className="inline-flex items-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg shadow-lg transition-all transform hover:scale-105"
65
- >
66
- Get Started
67
- <svg className="ml-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
68
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
69
- </svg>
70
- </Link>
71
  </div>
72
 
73
- <div className="grid md:grid-cols-3 gap-8 mt-16">
74
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
75
- <div className="text-4xl mb-4">⚡</div>
76
- <h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
77
- Real-time Monitoring
78
- </h3>
79
- <p className="text-gray-600 dark:text-gray-400">
80
- Automated checks every 30 seconds to ensure your services are always available
81
- </p>
82
- </div>
 
 
 
 
 
83
 
84
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
85
- <div className="text-4xl mb-4">📊</div>
86
- <h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
87
- Detailed Analytics
88
- </h3>
89
- <p className="text-gray-600 dark:text-gray-400">
90
- Track uptime percentage, response times, and historical data
91
- </p>
92
- </div>
 
 
 
 
93
 
94
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
95
- <div className="text-4xl mb-4">🚀</div>
96
- <h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
97
- Simple & Fast
98
- </h3>
99
- <p className="text-gray-600 dark:text-gray-400">
100
- Clean dashboard with instant insights into your service health
101
- </p>
102
- </div>
 
 
 
 
103
  </div>
104
  </div>
105
  </div>
 
1
  import type { Route } from "./+types/home";
2
  import { Link } from "react-router";
3
  import { useState, useEffect } from "react";
4
+ import { Button } from "~/components/ui/button";
5
+ import { Card, CardContent } from "~/components/ui/card";
6
+ import { Activity, BarChart3, Zap, ArrowRight, ExternalLink } from "lucide-react";
7
 
8
  export function meta({}: Route.MetaArgs) {
9
  return [
 
25
  // Show "Open in New Tab" message if in iframe
26
  if (isInIframe) {
27
  return (
28
+ <div className="min-h-screen bg-background flex items-center justify-center p-4">
29
+ <Card className="max-w-md w-full">
30
+ <CardContent className="pt-6 text-center">
31
+ <div className="mb-6 flex justify-center">
32
+ <ExternalLink className="h-16 w-16 text-muted-foreground" />
33
+ </div>
34
+ <h2 className="text-2xl font-semibold mb-3">
35
+ Open in New Tab
36
+ </h2>
37
+ <p className="text-muted-foreground mb-6">
38
+ For the best experience, please open this application in a new tab.
39
+ </p>
40
+ <Button asChild className="w-full">
41
+ <a
42
+ href={window.location.href}
43
+ target="_blank"
44
+ rel="noopener noreferrer"
45
+ >
46
+ Open in New Tab
47
+ <ExternalLink className="ml-2 h-4 w-4" />
48
+ </a>
49
+ </Button>
50
+ </CardContent>
51
+ </Card>
52
  </div>
53
  );
54
  }
55
 
56
  return (
57
+ <div className="min-h-screen bg-background">
58
+ <div className="container mx-auto px-4 py-20 sm:py-32">
59
+ <div className="max-w-4xl mx-auto">
60
+ {/* Hero Section */}
61
+ <div className="text-center mb-20">
62
+ <h1 className="text-5xl sm:text-6xl font-bold tracking-tight mb-6">
63
+ Always Uptime
64
+ </h1>
65
+ <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
66
+ Monitor your services 24/7 with real-time uptime checks every 30 seconds
67
+ </p>
68
+ <Button asChild size="lg" className="group">
69
+ <Link to="/login">
70
+ Get Started
71
+ <ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
72
+ </Link>
73
+ </Button>
 
 
 
74
  </div>
75
 
76
+ {/* Features */}
77
+ <div className="grid sm:grid-cols-3 gap-6">
78
+ <Card className="border-border/50">
79
+ <CardContent className="pt-6">
80
+ <div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
81
+ <Activity className="h-6 w-6 text-primary" />
82
+ </div>
83
+ <h3 className="text-lg font-semibold mb-2">
84
+ Real-time Monitoring
85
+ </h3>
86
+ <p className="text-sm text-muted-foreground">
87
+ Automated checks every 30 seconds to ensure your services are always available
88
+ </p>
89
+ </CardContent>
90
+ </Card>
91
 
92
+ <Card className="border-border/50">
93
+ <CardContent className="pt-6">
94
+ <div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
95
+ <BarChart3 className="h-6 w-6 text-primary" />
96
+ </div>
97
+ <h3 className="text-lg font-semibold mb-2">
98
+ Detailed Analytics
99
+ </h3>
100
+ <p className="text-sm text-muted-foreground">
101
+ Track uptime percentage, response times, and historical data
102
+ </p>
103
+ </CardContent>
104
+ </Card>
105
 
106
+ <Card className="border-border/50">
107
+ <CardContent className="pt-6">
108
+ <div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
109
+ <Zap className="h-6 w-6 text-primary" />
110
+ </div>
111
+ <h3 className="text-lg font-semibold mb-2">
112
+ Simple & Fast
113
+ </h3>
114
+ <p className="text-sm text-muted-foreground">
115
+ Clean dashboard with instant insights into your service health
116
+ </p>
117
+ </CardContent>
118
+ </Card>
119
  </div>
120
  </div>
121
  </div>
app/routes/uptime.tsx CHANGED
@@ -4,6 +4,15 @@ import { useRevalidator, redirect, Form } from "react-router";
4
  import { useEffect, useState } from "react";
5
  import { lucia } from "~/lib/auth.server";
6
  import { Button } from "~/components/ui/button";
 
 
 
 
 
 
 
 
 
7
 
8
  export async function loader({ request }: Route.LoaderArgs) {
9
  // Check authentication
@@ -157,353 +166,337 @@ export default function Uptime({ loaderData, actionData }: Route.ComponentProps)
157
  }, [actionData, revalidator]);
158
 
159
  return (
160
- <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
161
- <div className="container mx-auto px-4 py-8">
 
162
  <header className="mb-8 flex items-center justify-between">
163
  <div>
164
- <h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
165
  Always Uptime
166
  </h1>
167
- <p className="text-gray-600 dark:text-gray-400">
168
  Monitoring your services every 30 seconds
169
  </p>
170
  </div>
171
- <div className="flex items-center gap-4">
172
- <span className="text-sm text-gray-600 dark:text-gray-400">{user.username}</span>
 
 
 
 
 
173
  <Form method="post" action="/logout">
174
  <Button variant="outline" size="sm" type="submit">
175
- Logout
 
176
  </Button>
177
  </Form>
178
  </div>
179
  </header>
180
 
 
 
181
  {/* Add Monitor Button */}
182
  <div className="mb-6">
183
- <Button
184
- onClick={() => setShowAddForm(!showAddForm)}
185
- className="bg-indigo-600 hover:bg-indigo-700 text-white"
186
- >
187
- {showAddForm ? "Cancel" : "+ Add New Monitor"}
188
  </Button>
189
  </div>
190
 
191
  {/* Add Monitor Form */}
192
  {showAddForm && (
193
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-6">
194
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
195
- Add New Monitor
196
- </h2>
197
- {actionData?.error && (
198
- <div className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-3 rounded mb-4">
199
- {actionData.error}
200
- </div>
201
- )}
202
- <Form method="post" className="space-y-4">
203
- <input type="hidden" name="intent" value="create" />
204
-
205
- <div>
206
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
207
- URL to Monitor
208
- </label>
209
- <input
210
- type="url"
211
- name="url"
212
- required
213
- placeholder="https://example.com"
214
- className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
215
- />
216
- </div>
217
-
218
- <div>
219
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
220
- Check Interval (seconds)
221
- </label>
222
- <input
223
- type="number"
224
- name="interval"
225
- required
226
- min="10"
227
- defaultValue="30"
228
- placeholder="30"
229
- className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
230
- />
231
- <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
232
- Minimum 10 seconds
233
- </p>
234
- </div>
235
-
236
- <div className="flex gap-3">
237
- <Button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white">
238
- Create Monitor
239
- </Button>
240
- <Button
241
- type="button"
242
- variant="outline"
243
- onClick={() => setShowAddForm(false)}
244
- >
245
- Cancel
246
- </Button>
247
- </div>
248
- </Form>
249
- </div>
250
  )}
251
 
252
  {stats.length === 0 ? (
253
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
254
- <p className="text-gray-600 dark:text-gray-400">
255
- No uptime data available yet. Checks will appear here shortly...
256
- </p>
257
- </div>
 
 
 
258
  ) : (
259
  <div className="space-y-6">
260
  {stats.map((stat: UptimeStats) => (
261
- <div
262
- key={stat.url}
263
- className="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden"
264
- >
265
- <div className="p-6">
266
- <div className="flex items-center justify-between mb-4">
267
  <div className="flex-1">
268
- <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-1">
269
- {stat.name}
270
- </h2>
271
- <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
272
- {stat.url}
273
- </p>
274
- <div className="flex items-center gap-2">
275
- <span
276
- className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
277
- stat.lastCheck?.status === "up"
278
- ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
279
- : "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
280
- }`}
281
- >
282
- {stat.lastCheck?.status === "up" ? "🟢 Online" : "🔴 Offline"}
283
- </span>
284
- {stat.lastCheck?.statusCode && (
285
- <span className="text-sm text-gray-500 dark:text-gray-400">
286
- Status: {stat.lastCheck.statusCode}
287
- </span>
288
- )}
289
  </div>
 
 
 
 
 
 
290
  </div>
291
- <div className="text-right">
292
- <div className="text-3xl font-bold text-gray-900 dark:text-white">
293
- {stat.uptime.toFixed(2)}%
294
- </div>
295
- <div className="text-sm text-gray-500 dark:text-gray-400 mb-3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  Uptime
297
  </div>
298
- {/* Action Buttons */}
299
- <div className="flex gap-2 justify-end">
300
- <Button
301
- variant="outline"
302
- size="sm"
303
- onClick={() => setEditingMonitor(stat)}
304
- >
305
- Edit
306
- </Button>
307
- <Button
308
- variant="outline"
309
- size="sm"
310
- onClick={() => setDeletingMonitor(stat.id)}
311
- className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900"
312
- >
313
- Delete
314
- </Button>
315
  </div>
316
  </div>
317
- </div>
318
-
319
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
320
- <div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
321
- <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
322
  Total Checks
323
  </div>
324
- <div className="text-2xl font-semibold text-gray-900 dark:text-white">
325
  {stat.totalChecks}
326
  </div>
327
  </div>
328
- <div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
329
- <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
330
  Successful
331
  </div>
332
- <div className="text-2xl font-semibold text-green-600 dark:text-green-400">
333
  {stat.successfulChecks}
334
  </div>
335
  </div>
336
- <div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
337
- <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
338
- Failed
339
- </div>
340
- <div className="text-2xl font-semibold text-red-600 dark:text-red-400">
341
- {stat.failedChecks}
342
- </div>
343
- </div>
344
- <div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
345
- <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
346
  Avg Response
347
  </div>
348
- <div className="text-2xl font-semibold text-gray-900 dark:text-white">
349
  {Math.round(stat.averageResponseTime)}ms
350
  </div>
351
  </div>
352
  </div>
353
 
354
  {stat.lastCheck && (
355
- <div className="border-t border-gray-200 dark:border-gray-700 pt-4">
356
- <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
357
- Last Check
358
- </h3>
359
- <div className="text-sm text-gray-600 dark:text-gray-400">
360
- <p>
361
- Time: {new Date(stat.lastCheck.timestamp).toLocaleString()}
362
- </p>
363
- {stat.lastCheck.responseTime && (
364
- <p>Response Time: {stat.lastCheck.responseTime}ms</p>
365
- )}
366
- {stat.lastCheck.error && (
367
- <p className="text-red-600 dark:text-red-400">
368
- Error: {stat.lastCheck.error}
369
- </p>
370
- )}
371
  </div>
372
- </div>
373
  )}
374
 
375
  {stat.checks.length > 0 && (
376
- <div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
377
- <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
378
- Recent Checks (Last {Math.min(stat.checks.length, 20)})
379
- </h3>
380
- <div className="flex gap-1 overflow-x-auto pb-2">
381
- {stat.checks
382
- .slice(-20)
383
- .reverse()
384
- .map((check, idx: number) => (
385
- <div
386
- key={idx}
387
- className={`w-3 h-16 rounded ${
388
- check.status === "up"
389
- ? "bg-green-500"
390
- : "bg-red-500"
391
- }`}
392
- title={`${new Date(check.timestamp).toLocaleString()} - ${
393
- check.status
394
- } (${check.responseTime}ms)`}
395
- />
396
- ))}
 
 
 
397
  </div>
398
- </div>
399
  )}
400
- </div>
401
- </div>
402
  ))}
403
  </div>
404
  )}
405
 
406
  {/* Edit Monitor Modal */}
407
- {editingMonitor && (
408
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
409
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-md w-full">
410
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
411
- Edit Monitor
412
- </h2>
413
- {actionData?.error && (
414
- <div className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-3 rounded mb-4">
415
- {actionData.error}
416
- </div>
417
- )}
 
 
 
 
418
  <Form method="post" className="space-y-4">
419
  <input type="hidden" name="intent" value="update" />
420
  <input type="hidden" name="monitorId" value={editingMonitor.id} />
421
 
422
- <div>
423
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
424
- URL to Monitor
425
- </label>
426
- <input
427
  type="url"
428
  name="url"
429
  defaultValue={editingMonitor.url}
430
  required
431
- className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
432
  />
433
  </div>
434
 
435
- <div>
436
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
437
- Check Interval (seconds)
438
- </label>
439
- <input
440
  type="number"
441
  name="interval"
442
  defaultValue={editingMonitor.interval}
443
  required
444
  min="10"
445
- className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
446
  />
447
- <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
448
  Minimum 10 seconds
449
  </p>
450
  </div>
451
 
452
- <div className="flex gap-3">
453
- <Button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white">
454
- Update Monitor
455
- </Button>
456
- <Button
457
- type="button"
458
- variant="outline"
459
- onClick={() => setEditingMonitor(null)}
460
- >
461
  Cancel
462
  </Button>
463
- </div>
 
464
  </Form>
465
- </div>
466
- </div>
467
- )}
468
 
469
  {/* Delete Confirmation Modal */}
470
- {deletingMonitor && (
471
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
472
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-md w-full">
473
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
474
- Delete Monitor
475
- </h2>
476
- <p className="text-gray-600 dark:text-gray-400 mb-6">
477
  Are you sure you want to delete this monitor? This action cannot be undone.
478
- </p>
479
- {actionData?.error && (
480
- <div className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-3 rounded mb-4">
481
- {actionData.error}
482
- </div>
483
- )}
484
- <Form method="post" className="flex gap-3">
485
- <input type="hidden" name="intent" value="delete" />
486
- <input type="hidden" name="monitorId" value={deletingMonitor} />
487
- <Button
488
- type="submit"
489
- className="bg-red-600 hover:bg-red-700 text-white"
490
- >
491
- Delete
492
- </Button>
493
- <Button
494
- type="button"
495
- variant="outline"
496
- onClick={() => setDeletingMonitor(null)}
497
- >
498
  Cancel
499
  </Button>
500
- </Form>
501
- </div>
502
- </div>
503
- )}
 
 
 
504
 
505
- <footer className="mt-8 text-center text-sm text-gray-500 dark:text-gray-400">
506
- Auto-refreshing every 30 seconds
 
 
507
  </footer>
508
  </div>
509
  </div>
 
4
  import { useEffect, useState } from "react";
5
  import { lucia } from "~/lib/auth.server";
6
  import { Button } from "~/components/ui/button";
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
8
+ import { Input } from "~/components/ui/input";
9
+ import { Label } from "~/components/ui/label";
10
+ import { Badge } from "~/components/ui/badge";
11
+ import { Separator } from "~/components/ui/separator";
12
+ import { Alert, AlertDescription } from "~/components/ui/alert";
13
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog";
14
+ import { Avatar, AvatarFallback } from "~/components/ui/avatar";
15
+ import { Plus, LogOut, Pencil, Trash2, Activity, Clock, TrendingUp, AlertCircle } from "lucide-react";
16
 
17
  export async function loader({ request }: Route.LoaderArgs) {
18
  // Check authentication
 
166
  }, [actionData, revalidator]);
167
 
168
  return (
169
+ <div className="min-h-screen bg-background">
170
+ <div className="container mx-auto px-4 py-8 max-w-7xl">
171
+ {/* Header */}
172
  <header className="mb-8 flex items-center justify-between">
173
  <div>
174
+ <h1 className="text-3xl font-bold tracking-tight mb-1">
175
  Always Uptime
176
  </h1>
177
+ <p className="text-sm text-muted-foreground">
178
  Monitoring your services every 30 seconds
179
  </p>
180
  </div>
181
+ <div className="flex items-center gap-3">
182
+ <Avatar className="h-9 w-9">
183
+ <AvatarFallback className="text-xs">
184
+ {user.username.slice(0, 2).toUpperCase()}
185
+ </AvatarFallback>
186
+ </Avatar>
187
+ <span className="text-sm font-medium hidden sm:inline">{user.username}</span>
188
  <Form method="post" action="/logout">
189
  <Button variant="outline" size="sm" type="submit">
190
+ <LogOut className="h-4 w-4 sm:mr-2" />
191
+ <span className="hidden sm:inline">Logout</span>
192
  </Button>
193
  </Form>
194
  </div>
195
  </header>
196
 
197
+ <Separator className="mb-8" />
198
+
199
  {/* Add Monitor Button */}
200
  <div className="mb-6">
201
+ <Button onClick={() => setShowAddForm(!showAddForm)}>
202
+ <Plus className="h-4 w-4 mr-2" />
203
+ {showAddForm ? "Cancel" : "Add New Monitor"}
 
 
204
  </Button>
205
  </div>
206
 
207
  {/* Add Monitor Form */}
208
  {showAddForm && (
209
+ <Card className="mb-6">
210
+ <CardHeader>
211
+ <CardTitle>Add New Monitor</CardTitle>
212
+ <CardDescription>
213
+ Start monitoring a new service endpoint
214
+ </CardDescription>
215
+ </CardHeader>
216
+ <CardContent>
217
+ {actionData?.error && (
218
+ <Alert variant="destructive" className="mb-4">
219
+ <AlertCircle className="h-4 w-4" />
220
+ <AlertDescription>{actionData.error}</AlertDescription>
221
+ </Alert>
222
+ )}
223
+ <Form method="post" className="space-y-4">
224
+ <input type="hidden" name="intent" value="create" />
225
+
226
+ <div className="space-y-2">
227
+ <Label htmlFor="url">URL to Monitor</Label>
228
+ <Input
229
+ id="url"
230
+ type="url"
231
+ name="url"
232
+ required
233
+ placeholder="https://example.com"
234
+ />
235
+ </div>
236
+
237
+ <div className="space-y-2">
238
+ <Label htmlFor="interval">Check Interval (seconds)</Label>
239
+ <Input
240
+ id="interval"
241
+ type="number"
242
+ name="interval"
243
+ required
244
+ min="10"
245
+ defaultValue="30"
246
+ placeholder="30"
247
+ />
248
+ <p className="text-xs text-muted-foreground">
249
+ Minimum 10 seconds
250
+ </p>
251
+ </div>
252
+
253
+ <div className="flex gap-3">
254
+ <Button type="submit">Create Monitor</Button>
255
+ <Button
256
+ type="button"
257
+ variant="outline"
258
+ onClick={() => setShowAddForm(false)}
259
+ >
260
+ Cancel
261
+ </Button>
262
+ </div>
263
+ </Form>
264
+ </CardContent>
265
+ </Card>
266
  )}
267
 
268
  {stats.length === 0 ? (
269
+ <Card>
270
+ <CardContent className="pt-6 text-center py-12">
271
+ <Activity className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
272
+ <p className="text-muted-foreground">
273
+ No monitors yet. Add your first monitor to start tracking uptime.
274
+ </p>
275
+ </CardContent>
276
+ </Card>
277
  ) : (
278
  <div className="space-y-6">
279
  {stats.map((stat: UptimeStats) => (
280
+ <Card key={stat.url}>
281
+ <CardHeader>
282
+ <div className="flex items-start justify-between">
 
 
 
283
  <div className="flex-1">
284
+ <div className="flex items-center gap-2 mb-1">
285
+ <CardTitle className="text-xl">{stat.name}</CardTitle>
286
+ <Badge variant={stat.lastCheck?.status === "up" ? "default" : "destructive"}>
287
+ {stat.lastCheck?.status === "up" ? "Online" : "Offline"}
288
+ </Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  </div>
290
+ <CardDescription className="text-sm">{stat.url}</CardDescription>
291
+ {stat.lastCheck?.statusCode && (
292
+ <p className="text-xs text-muted-foreground mt-1">
293
+ Status Code: {stat.lastCheck.statusCode}
294
+ </p>
295
+ )}
296
  </div>
297
+ <div className="flex items-center gap-2">
298
+ <Button
299
+ variant="outline"
300
+ size="sm"
301
+ onClick={() => setEditingMonitor(stat)}
302
+ >
303
+ <Pencil className="h-4 w-4" />
304
+ </Button>
305
+ <Button
306
+ variant="outline"
307
+ size="sm"
308
+ onClick={() => setDeletingMonitor(stat.id)}
309
+ >
310
+ <Trash2 className="h-4 w-4 text-destructive" />
311
+ </Button>
312
+ </div>
313
+ </div>
314
+ </CardHeader>
315
+ <CardContent>
316
+ {/* Stats Grid */}
317
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
318
+ <div className="space-y-1">
319
+ <div className="flex items-center text-sm text-muted-foreground">
320
+ <TrendingUp className="h-4 w-4 mr-1" />
321
  Uptime
322
  </div>
323
+ <div className="text-2xl font-bold">
324
+ {stat.uptime.toFixed(2)}%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  </div>
326
  </div>
327
+ <div className="space-y-1">
328
+ <div className="flex items-center text-sm text-muted-foreground">
329
+ <Activity className="h-4 w-4 mr-1" />
 
 
330
  Total Checks
331
  </div>
332
+ <div className="text-2xl font-bold">
333
  {stat.totalChecks}
334
  </div>
335
  </div>
336
+ <div className="space-y-1">
337
+ <div className="text-sm text-muted-foreground">
338
  Successful
339
  </div>
340
+ <div className="text-2xl font-bold text-green-600 dark:text-green-400">
341
  {stat.successfulChecks}
342
  </div>
343
  </div>
344
+ <div className="space-y-1">
345
+ <div className="flex items-center text-sm text-muted-foreground">
346
+ <Clock className="h-4 w-4 mr-1" />
 
 
 
 
 
 
 
347
  Avg Response
348
  </div>
349
+ <div className="text-2xl font-bold">
350
  {Math.round(stat.averageResponseTime)}ms
351
  </div>
352
  </div>
353
  </div>
354
 
355
  {stat.lastCheck && (
356
+ <>
357
+ <Separator className="mb-4" />
358
+ <div className="space-y-1 mb-4">
359
+ <h4 className="text-sm font-semibold">Last Check</h4>
360
+ <div className="text-sm text-muted-foreground space-y-0.5">
361
+ <p>Time: {new Date(stat.lastCheck.timestamp).toLocaleString()}</p>
362
+ {stat.lastCheck.responseTime && (
363
+ <p>Response Time: {stat.lastCheck.responseTime}ms</p>
364
+ )}
365
+ {stat.lastCheck.error && (
366
+ <p className="text-destructive">
367
+ Error: {stat.lastCheck.error}
368
+ </p>
369
+ )}
370
+ </div>
 
371
  </div>
372
+ </>
373
  )}
374
 
375
  {stat.checks.length > 0 && (
376
+ <>
377
+ <Separator className="mb-4" />
378
+ <div>
379
+ <h4 className="text-sm font-semibold mb-3">
380
+ Recent Checks (Last {Math.min(stat.checks.length, 20)})
381
+ </h4>
382
+ <div className="flex gap-1 overflow-x-auto pb-2">
383
+ {stat.checks
384
+ .slice(-20)
385
+ .reverse()
386
+ .map((check, idx: number) => (
387
+ <div
388
+ key={idx}
389
+ className={`w-3 h-16 rounded ${
390
+ check.status === "up"
391
+ ? "bg-green-500"
392
+ : "bg-red-500"
393
+ }`}
394
+ title={`${new Date(check.timestamp).toLocaleString()} - ${
395
+ check.status
396
+ } (${check.responseTime}ms)`}
397
+ />
398
+ ))}
399
+ </div>
400
  </div>
401
+ </>
402
  )}
403
+ </CardContent>
404
+ </Card>
405
  ))}
406
  </div>
407
  )}
408
 
409
  {/* Edit Monitor Modal */}
410
+ <Dialog open={!!editingMonitor} onOpenChange={() => setEditingMonitor(null)}>
411
+ <DialogContent>
412
+ <DialogHeader>
413
+ <DialogTitle>Edit Monitor</DialogTitle>
414
+ <DialogDescription>
415
+ Update the URL or check interval for this monitor
416
+ </DialogDescription>
417
+ </DialogHeader>
418
+ {actionData?.error && (
419
+ <Alert variant="destructive">
420
+ <AlertCircle className="h-4 w-4" />
421
+ <AlertDescription>{actionData.error}</AlertDescription>
422
+ </Alert>
423
+ )}
424
+ {editingMonitor && (
425
  <Form method="post" className="space-y-4">
426
  <input type="hidden" name="intent" value="update" />
427
  <input type="hidden" name="monitorId" value={editingMonitor.id} />
428
 
429
+ <div className="space-y-2">
430
+ <Label htmlFor="edit-url">URL to Monitor</Label>
431
+ <Input
432
+ id="edit-url"
 
433
  type="url"
434
  name="url"
435
  defaultValue={editingMonitor.url}
436
  required
 
437
  />
438
  </div>
439
 
440
+ <div className="space-y-2">
441
+ <Label htmlFor="edit-interval">Check Interval (seconds)</Label>
442
+ <Input
443
+ id="edit-interval"
 
444
  type="number"
445
  name="interval"
446
  defaultValue={editingMonitor.interval}
447
  required
448
  min="10"
 
449
  />
450
+ <p className="text-xs text-muted-foreground">
451
  Minimum 10 seconds
452
  </p>
453
  </div>
454
 
455
+ <DialogFooter>
456
+ <Button type="button" variant="outline" onClick={() => setEditingMonitor(null)}>
 
 
 
 
 
 
 
457
  Cancel
458
  </Button>
459
+ <Button type="submit">Update Monitor</Button>
460
+ </DialogFooter>
461
  </Form>
462
+ )}
463
+ </DialogContent>
464
+ </Dialog>
465
 
466
  {/* Delete Confirmation Modal */}
467
+ <Dialog open={!!deletingMonitor} onOpenChange={() => setDeletingMonitor(null)}>
468
+ <DialogContent>
469
+ <DialogHeader>
470
+ <DialogTitle>Delete Monitor</DialogTitle>
471
+ <DialogDescription>
 
 
472
  Are you sure you want to delete this monitor? This action cannot be undone.
473
+ </DialogDescription>
474
+ </DialogHeader>
475
+ {actionData?.error && (
476
+ <Alert variant="destructive">
477
+ <AlertCircle className="h-4 w-4" />
478
+ <AlertDescription>{actionData.error}</AlertDescription>
479
+ </Alert>
480
+ )}
481
+ <Form method="post">
482
+ <input type="hidden" name="intent" value="delete" />
483
+ <input type="hidden" name="monitorId" value={deletingMonitor || ""} />
484
+ <DialogFooter>
485
+ <Button type="button" variant="outline" onClick={() => setDeletingMonitor(null)}>
 
 
 
 
 
 
 
486
  Cancel
487
  </Button>
488
+ <Button type="submit" variant="destructive">
489
+ Delete
490
+ </Button>
491
+ </DialogFooter>
492
+ </Form>
493
+ </DialogContent>
494
+ </Dialog>
495
 
496
+ <footer className="mt-12 text-center">
497
+ <p className="text-sm text-muted-foreground">
498
+ Auto-refreshing every 30 seconds
499
+ </p>
500
  </footer>
501
  </div>
502
  </div>
components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/app.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "~/components",
16
+ "utils": "~/lib/utils",
17
+ "ui": "~/components/ui",
18
+ "lib": "~/lib",
19
+ "hooks": "~/hooks"
20
+ },
21
+ "registries": {}
22
+ }
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -12,6 +12,10 @@
12
  "@libsql/client": "^0.15.15",
13
  "@lucia-auth/adapter-drizzle": "^1.1.0",
14
  "@node-rs/argon2": "^2.0.2",
 
 
 
 
15
  "@react-router/node": "^7.9.2",
16
  "@react-router/serve": "^7.9.2",
17
  "class-variance-authority": "^0.7.1",
@@ -34,7 +38,9 @@
34
  "@types/node": "^22",
35
  "@types/react": "^19.1.13",
36
  "@types/react-dom": "^19.1.9",
 
37
  "tailwindcss": "^4.1.13",
 
38
  "typescript": "^5.9.2",
39
  "vite": "^7.1.7",
40
  "vite-tsconfig-paths": "^5.1.4"
 
12
  "@libsql/client": "^0.15.15",
13
  "@lucia-auth/adapter-drizzle": "^1.1.0",
14
  "@node-rs/argon2": "^2.0.2",
15
+ "@radix-ui/react-avatar": "^1.1.10",
16
+ "@radix-ui/react-dialog": "^1.1.15",
17
+ "@radix-ui/react-separator": "^1.1.7",
18
+ "@radix-ui/react-slot": "^1.2.3",
19
  "@react-router/node": "^7.9.2",
20
  "@react-router/serve": "^7.9.2",
21
  "class-variance-authority": "^0.7.1",
 
38
  "@types/node": "^22",
39
  "@types/react": "^19.1.13",
40
  "@types/react-dom": "^19.1.9",
41
+ "shadcn": "^3.4.2",
42
  "tailwindcss": "^4.1.13",
43
+ "tw-animate-css": "^1.4.0",
44
  "typescript": "^5.9.2",
45
  "vite": "^7.1.7",
46
  "vite-tsconfig-paths": "^5.1.4"