@getjack/jack 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/package.json +1 -1
  2. package/src/commands/down.ts +11 -1
  3. package/src/commands/init.ts +19 -6
  4. package/src/lib/agents.ts +3 -1
  5. package/src/lib/auth/ensure-auth.test.ts +3 -3
  6. package/src/lib/control-plane.ts +15 -1
  7. package/src/lib/deploy-upload.ts +26 -1
  8. package/src/lib/hooks.ts +232 -1
  9. package/src/lib/managed-deploy.ts +13 -6
  10. package/src/lib/progress.ts +76 -5
  11. package/src/lib/project-list.ts +6 -1
  12. package/src/lib/project-operations.ts +14 -0
  13. package/src/lib/project-resolver.ts +1 -1
  14. package/src/lib/zip-packager.ts +36 -7
  15. package/src/templates/index.ts +1 -1
  16. package/src/templates/types.ts +16 -0
  17. package/templates/CLAUDE.md +103 -0
  18. package/templates/miniapp/.jack.json +1 -3
  19. package/templates/saas/.jack.json +154 -0
  20. package/templates/saas/AGENTS.md +333 -0
  21. package/templates/saas/bun.lock +925 -0
  22. package/templates/saas/components.json +21 -0
  23. package/templates/saas/index.html +12 -0
  24. package/templates/saas/package.json +75 -0
  25. package/templates/saas/public/icon.png +0 -0
  26. package/templates/saas/public/og.png +0 -0
  27. package/templates/saas/schema.sql +73 -0
  28. package/templates/saas/src/auth.ts +77 -0
  29. package/templates/saas/src/client/App.tsx +63 -0
  30. package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
  31. package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
  32. package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
  33. package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
  34. package/templates/saas/src/client/components/ui/alert.tsx +60 -0
  35. package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
  36. package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
  37. package/templates/saas/src/client/components/ui/badge.tsx +39 -0
  38. package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
  39. package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
  40. package/templates/saas/src/client/components/ui/button.tsx +60 -0
  41. package/templates/saas/src/client/components/ui/card.tsx +75 -0
  42. package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
  43. package/templates/saas/src/client/components/ui/chart.tsx +326 -0
  44. package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
  45. package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
  46. package/templates/saas/src/client/components/ui/command.tsx +159 -0
  47. package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
  48. package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
  49. package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
  50. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
  51. package/templates/saas/src/client/components/ui/empty.tsx +94 -0
  52. package/templates/saas/src/client/components/ui/field.tsx +232 -0
  53. package/templates/saas/src/client/components/ui/form.tsx +152 -0
  54. package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
  55. package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
  56. package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
  57. package/templates/saas/src/client/components/ui/input.tsx +21 -0
  58. package/templates/saas/src/client/components/ui/item.tsx +172 -0
  59. package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
  60. package/templates/saas/src/client/components/ui/label.tsx +21 -0
  61. package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
  62. package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
  63. package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
  64. package/templates/saas/src/client/components/ui/popover.tsx +42 -0
  65. package/templates/saas/src/client/components/ui/progress.tsx +26 -0
  66. package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
  67. package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
  68. package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
  69. package/templates/saas/src/client/components/ui/select.tsx +173 -0
  70. package/templates/saas/src/client/components/ui/separator.tsx +28 -0
  71. package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
  72. package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
  73. package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
  74. package/templates/saas/src/client/components/ui/slider.tsx +58 -0
  75. package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
  76. package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
  77. package/templates/saas/src/client/components/ui/switch.tsx +28 -0
  78. package/templates/saas/src/client/components/ui/table.tsx +90 -0
  79. package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
  80. package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
  81. package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
  82. package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
  83. package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
  84. package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
  85. package/templates/saas/src/client/hooks/useAuth.ts +14 -0
  86. package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
  87. package/templates/saas/src/client/index.css +165 -0
  88. package/templates/saas/src/client/lib/auth-client.ts +7 -0
  89. package/templates/saas/src/client/lib/plans.ts +82 -0
  90. package/templates/saas/src/client/lib/utils.ts +6 -0
  91. package/templates/saas/src/client/main.tsx +15 -0
  92. package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
  93. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
  94. package/templates/saas/src/client/pages/HomePage.tsx +285 -0
  95. package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
  96. package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
  97. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
  98. package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
  99. package/templates/saas/src/index.ts +208 -0
  100. package/templates/saas/tsconfig.json +18 -0
  101. package/templates/saas/vite.config.ts +14 -0
  102. package/templates/saas/wrangler.jsonc +20 -0
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SliderPrimitive from "@radix-ui/react-slider";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Slider({
9
+ className,
10
+ defaultValue,
11
+ value,
12
+ min = 0,
13
+ max = 100,
14
+ ...props
15
+ }: React.ComponentProps<typeof SliderPrimitive.Root>) {
16
+ const _values = React.useMemo(
17
+ () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
18
+ [value, defaultValue, min, max],
19
+ );
20
+
21
+ return (
22
+ <SliderPrimitive.Root
23
+ data-slot="slider"
24
+ defaultValue={defaultValue}
25
+ value={value}
26
+ min={min}
27
+ max={max}
28
+ className={cn(
29
+ "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ <SliderPrimitive.Track
35
+ data-slot="slider-track"
36
+ className={cn(
37
+ "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
38
+ )}
39
+ >
40
+ <SliderPrimitive.Range
41
+ data-slot="slider-range"
42
+ className={cn(
43
+ "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
44
+ )}
45
+ />
46
+ </SliderPrimitive.Track>
47
+ {Array.from({ length: _values.length }, (_, index) => (
48
+ <SliderPrimitive.Thumb
49
+ data-slot="slider-thumb"
50
+ key={index}
51
+ className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
52
+ />
53
+ ))}
54
+ </SliderPrimitive.Root>
55
+ );
56
+ }
57
+
58
+ export { Slider };
@@ -0,0 +1,38 @@
1
+ import {
2
+ CircleCheckIcon,
3
+ InfoIcon,
4
+ Loader2Icon,
5
+ OctagonXIcon,
6
+ TriangleAlertIcon,
7
+ } from "lucide-react";
8
+ import { useTheme } from "next-themes";
9
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
10
+
11
+ const Toaster = ({ ...props }: ToasterProps) => {
12
+ const { theme = "system" } = useTheme();
13
+
14
+ return (
15
+ <Sonner
16
+ theme={theme as ToasterProps["theme"]}
17
+ className="toaster group"
18
+ icons={{
19
+ success: <CircleCheckIcon className="size-4" />,
20
+ info: <InfoIcon className="size-4" />,
21
+ warning: <TriangleAlertIcon className="size-4" />,
22
+ error: <OctagonXIcon className="size-4" />,
23
+ loading: <Loader2Icon className="size-4 animate-spin" />,
24
+ }}
25
+ style={
26
+ {
27
+ "--normal-bg": "var(--popover)",
28
+ "--normal-text": "var(--popover-foreground)",
29
+ "--normal-border": "var(--border)",
30
+ "--border-radius": "var(--radius)",
31
+ } as React.CSSProperties
32
+ }
33
+ {...props}
34
+ />
35
+ );
36
+ };
37
+
38
+ export { Toaster };
@@ -0,0 +1,16 @@
1
+ import { Loader2Icon } from "lucide-react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
6
+ return (
7
+ <Loader2Icon
8
+ role="status"
9
+ aria-label="Loading"
10
+ className={cn("size-4 animate-spin", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ export { Spinner };
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
9
+ return (
10
+ <SwitchPrimitive.Root
11
+ data-slot="switch"
12
+ className={cn(
13
+ "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
14
+ className,
15
+ )}
16
+ {...props}
17
+ >
18
+ <SwitchPrimitive.Thumb
19
+ data-slot="switch-thumb"
20
+ className={cn(
21
+ "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
22
+ )}
23
+ />
24
+ </SwitchPrimitive.Root>
25
+ );
26
+ }
27
+
28
+ export { Switch };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
6
+ return (
7
+ <div data-slot="table-container" className="relative w-full overflow-x-auto">
8
+ <table
9
+ data-slot="table"
10
+ className={cn("w-full caption-bottom text-sm", className)}
11
+ {...props}
12
+ />
13
+ </div>
14
+ );
15
+ }
16
+
17
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
18
+ return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
19
+ }
20
+
21
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
22
+ return (
23
+ <tbody
24
+ data-slot="table-body"
25
+ className={cn("[&_tr:last-child]:border-0", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
32
+ return (
33
+ <tfoot
34
+ data-slot="table-footer"
35
+ className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
42
+ return (
43
+ <tr
44
+ data-slot="table-row"
45
+ className={cn(
46
+ "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
55
+ return (
56
+ <th
57
+ data-slot="table-head"
58
+ className={cn(
59
+ "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
60
+ className,
61
+ )}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
68
+ return (
69
+ <td
70
+ data-slot="table-cell"
71
+ className={cn(
72
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ );
78
+ }
79
+
80
+ function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
81
+ return (
82
+ <caption
83
+ data-slot="table-caption"
84
+ className={cn("text-muted-foreground mt-4 text-sm", className)}
85
+ {...props}
86
+ />
87
+ );
88
+ }
89
+
90
+ export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
9
+ return (
10
+ <TabsPrimitive.Root
11
+ data-slot="tabs"
12
+ className={cn("flex flex-col gap-2", className)}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
19
+ return (
20
+ <TabsPrimitive.List
21
+ data-slot="tabs-list"
22
+ className={cn(
23
+ "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
32
+ return (
33
+ <TabsPrimitive.Trigger
34
+ data-slot="tabs-trigger"
35
+ className={cn(
36
+ "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",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
45
+ return (
46
+ <TabsPrimitive.Content
47
+ data-slot="tabs-content"
48
+ className={cn("flex-1 outline-none", className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export { Textarea };
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
3
+ import { type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { toggleVariants } from "@/components/ui/toggle";
7
+
8
+ const ToggleGroupContext = React.createContext<
9
+ VariantProps<typeof toggleVariants> & {
10
+ spacing?: number;
11
+ }
12
+ >({
13
+ size: "default",
14
+ variant: "default",
15
+ spacing: 0,
16
+ });
17
+
18
+ function ToggleGroup({
19
+ className,
20
+ variant,
21
+ size,
22
+ spacing = 0,
23
+ children,
24
+ ...props
25
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
26
+ VariantProps<typeof toggleVariants> & {
27
+ spacing?: number;
28
+ }) {
29
+ return (
30
+ <ToggleGroupPrimitive.Root
31
+ data-slot="toggle-group"
32
+ data-variant={variant}
33
+ data-size={size}
34
+ data-spacing={spacing}
35
+ style={{ "--gap": spacing } as React.CSSProperties}
36
+ className={cn(
37
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
38
+ className,
39
+ )}
40
+ {...props}
41
+ >
42
+ <ToggleGroupContext.Provider value={{ variant, size, spacing }}>
43
+ {children}
44
+ </ToggleGroupContext.Provider>
45
+ </ToggleGroupPrimitive.Root>
46
+ );
47
+ }
48
+
49
+ function ToggleGroupItem({
50
+ className,
51
+ children,
52
+ variant,
53
+ size,
54
+ ...props
55
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>) {
56
+ const context = React.useContext(ToggleGroupContext);
57
+
58
+ return (
59
+ <ToggleGroupPrimitive.Item
60
+ data-slot="toggle-group-item"
61
+ data-variant={context.variant || variant}
62
+ data-size={context.size || size}
63
+ data-spacing={context.spacing}
64
+ className={cn(
65
+ toggleVariants({
66
+ variant: context.variant || variant,
67
+ size: context.size || size,
68
+ }),
69
+ "w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
70
+ "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
71
+ className,
72
+ )}
73
+ {...props}
74
+ >
75
+ {children}
76
+ </ToggleGroupPrimitive.Item>
77
+ );
78
+ }
79
+
80
+ export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,44 @@
1
+ import * as React from "react";
2
+ import * as TogglePrimitive from "@radix-ui/react-toggle";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const toggleVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-transparent",
13
+ outline:
14
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
15
+ },
16
+ size: {
17
+ default: "h-9 px-2 min-w-9",
18
+ sm: "h-8 px-1.5 min-w-8",
19
+ lg: "h-10 px-2.5 min-w-10",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ },
27
+ );
28
+
29
+ function Toggle({
30
+ className,
31
+ variant,
32
+ size,
33
+ ...props
34
+ }: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) {
35
+ return (
36
+ <TogglePrimitive.Root
37
+ data-slot="toggle"
38
+ className={cn(toggleVariants({ variant, size, className }))}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ export { Toggle, toggleVariants };
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function TooltipProvider({
9
+ delayDuration = 0,
10
+ ...props
11
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
22
+ return (
23
+ <TooltipProvider>
24
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25
+ </TooltipProvider>
26
+ );
27
+ }
28
+
29
+ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
30
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
31
+ }
32
+
33
+ function TooltipContent({
34
+ className,
35
+ sideOffset = 0,
36
+ children,
37
+ ...props
38
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Content
42
+ data-slot="tooltip-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
46
+ className,
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
52
+ </TooltipPrimitive.Content>
53
+ </TooltipPrimitive.Portal>
54
+ );
55
+ }
56
+
57
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+
3
+ const MOBILE_BREAKPOINT = 768;
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12
+ };
13
+ mql.addEventListener("change", onChange);
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15
+ return () => mql.removeEventListener("change", onChange);
16
+ }, []);
17
+
18
+ return !!isMobile;
19
+ }
@@ -0,0 +1,14 @@
1
+ import { authClient } from "../lib/auth-client";
2
+
3
+ export function useAuth() {
4
+ const { data: session, isPending, error } = authClient.useSession();
5
+
6
+ return {
7
+ user: session?.user ?? null,
8
+ session: session?.session ?? null,
9
+ isLoading: isPending,
10
+ isAuthenticated: !!session?.user,
11
+ error,
12
+ signOut: () => authClient.signOut(),
13
+ };
14
+ }
@@ -0,0 +1,86 @@
1
+ import { useState, useEffect } from "react";
2
+ import { authClient } from "../lib/auth-client";
3
+
4
+ type Subscription = {
5
+ id: string;
6
+ plan: string;
7
+ status: string;
8
+ stripeCustomerId?: string;
9
+ stripeSubscriptionId?: string;
10
+ cancelAtPeriodEnd?: boolean;
11
+ cancelAt?: Date | string | null; // Better Auth uses this
12
+ periodEnd?: Date | string | null;
13
+ };
14
+
15
+ type StripeStatus = {
16
+ id: string;
17
+ status: string;
18
+ cancelAtPeriodEnd: boolean;
19
+ periodEnd: string;
20
+ plan: string;
21
+ } | null;
22
+
23
+ export function useSubscription() {
24
+ const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
25
+ const [stripeStatus, setStripeStatus] = useState<StripeStatus>(null);
26
+ const [isLoading, setIsLoading] = useState(true);
27
+ const [error, setError] = useState<Error | null>(null);
28
+
29
+ useEffect(() => {
30
+ const fetchData = async () => {
31
+ try {
32
+ // Fetch from Better Auth
33
+ const result = await authClient.subscription.list();
34
+ console.log("[useSubscription] Better Auth subscriptions:", result);
35
+ if ("data" in result && result.data) {
36
+ setSubscriptions(result.data as Subscription[]);
37
+ }
38
+
39
+ // Fetch real-time status from Stripe
40
+ const stripeRes = await fetch("/api/subscription-status");
41
+ const stripeData = await stripeRes.json();
42
+ console.log("[useSubscription] Stripe status response:", stripeRes.status, stripeData);
43
+ if (stripeRes.ok && stripeData.subscription) {
44
+ setStripeStatus(stripeData.subscription);
45
+ }
46
+ } catch (err) {
47
+ setError(err as Error);
48
+ }
49
+ setIsLoading(false);
50
+ };
51
+
52
+ fetchData();
53
+ }, []);
54
+
55
+ const activeSubscription = subscriptions.find(
56
+ (s) => s.status === "active" || s.status === "trialing",
57
+ );
58
+
59
+ // Check if cancelling: Stripe uses cancelAtPeriodEnd, Better Auth uses cancelAt (date)
60
+ const isCancelling =
61
+ stripeStatus?.cancelAtPeriodEnd ||
62
+ activeSubscription?.cancelAtPeriodEnd ||
63
+ !!activeSubscription?.cancelAt || // Better Auth sets cancelAt date when cancelling
64
+ false;
65
+
66
+ // Get period end from either source
67
+ const periodEnd = stripeStatus?.periodEnd ||
68
+ (activeSubscription?.periodEnd ? String(activeSubscription.periodEnd) : null);
69
+
70
+ return {
71
+ subscriptions,
72
+ activeSubscription: activeSubscription ?? null,
73
+ plan: activeSubscription?.plan ?? "free",
74
+ isSubscribed: !!activeSubscription,
75
+ isCancelling,
76
+ periodEnd,
77
+ isLoading,
78
+ error,
79
+ upgrade: (plan: "pro" | "enterprise") =>
80
+ authClient.subscription.upgrade({
81
+ plan,
82
+ successUrl: `${window.location.origin}/#/dashboard?upgraded=true`,
83
+ cancelUrl: `${window.location.origin}/#/pricing`,
84
+ }),
85
+ };
86
+ }