create-aron-app 0.1.0 → 0.1.1

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 (156) hide show
  1. package/package.json +5 -2
  2. package/templates/_base/.cursor/agents/skills/clerk/SKILL.md +89 -0
  3. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/SKILL.md +142 -0
  4. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +30 -0
  5. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +88 -0
  6. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +165 -0
  7. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +208 -0
  8. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +14 -0
  9. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/SKILL.md +157 -0
  10. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +224 -0
  11. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +190 -0
  12. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +314 -0
  13. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +259 -0
  14. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +125 -0
  15. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +94 -0
  16. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +50 -0
  17. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +56 -0
  18. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +68 -0
  19. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +56 -0
  20. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +104 -0
  21. package/templates/_base/.cursor/agents/skills/clerk/clerk-webhooks/SKILL.md +131 -0
  22. package/templates/_base/.cursor/agents/skills/shadcn/SKILL.md +241 -0
  23. package/templates/_base/.cursor/agents/skills/shadcn/agents/openai.yml +5 -0
  24. package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn-small.png +0 -0
  25. package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn.png +0 -0
  26. package/templates/_base/.cursor/agents/skills/shadcn/cli.md +257 -0
  27. package/templates/_base/.cursor/agents/skills/shadcn/customization.md +202 -0
  28. package/templates/_base/.cursor/agents/skills/shadcn/evals/evals.json +47 -0
  29. package/templates/_base/.cursor/agents/skills/shadcn/mcp.md +94 -0
  30. package/templates/_base/.cursor/agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  31. package/templates/_base/.cursor/agents/skills/shadcn/rules/composition.md +195 -0
  32. package/templates/_base/.cursor/agents/skills/shadcn/rules/forms.md +192 -0
  33. package/templates/_base/.cursor/agents/skills/shadcn/rules/icons.md +101 -0
  34. package/templates/_base/.cursor/agents/skills/shadcn/rules/styling.md +162 -0
  35. package/templates/_base/.cursor/commands/builder.md +0 -0
  36. package/templates/_base/.cursor/commands/pr.md +7 -0
  37. package/templates/_base/.cursor/rules/api_architecture.mdc +268 -0
  38. package/templates/_base/.cursor/rules/coding_standards.mdc +64 -0
  39. package/templates/_base/.cursor/rules/convex_rules.mdc +675 -0
  40. package/templates/_base/.cursor/rules/frontend_rules.mdc +268 -0
  41. package/templates/_base/.env.convex.example +3 -0
  42. package/templates/_base/.github/workflows/ci.yml +29 -0
  43. package/templates/_base/.nvmrc +1 -0
  44. package/templates/_base/.vscode/settings.json +9 -0
  45. package/templates/_base/apps/api/auth.config.ts +18 -0
  46. package/templates/_base/apps/api/functions.ts +99 -0
  47. package/templates/_base/apps/api/project.json +22 -0
  48. package/templates/_base/apps/api/schema.ts +11 -0
  49. package/templates/_base/apps/api/todos/crud.ts +81 -0
  50. package/templates/_base/apps/api/todos/schema.ts +11 -0
  51. package/templates/_base/apps/api/todos/types.ts +22 -0
  52. package/templates/_base/apps/api/tsconfig.json +23 -0
  53. package/templates/_base/apps/api/types.ts +16 -0
  54. package/templates/_base/biome.json +114 -0
  55. package/templates/_base/convex.json +4 -0
  56. package/templates/_base/emails/project.json +16 -0
  57. package/templates/_base/emails/tsconfig.json +5 -0
  58. package/templates/_base/emails/welcome_email.tsx +53 -0
  59. package/templates/_base/nx.json +29 -0
  60. package/templates/_base/package.json +73 -0
  61. package/templates/_base/scripts/sync_convex_env.ts +63 -0
  62. package/templates/_base/shared/assets/image.d.ts +4 -0
  63. package/templates/_base/shared/assets/src/styles/global.css +73 -0
  64. package/templates/_base/shared/assets/tsconfig.json +5 -0
  65. package/templates/_base/shared/ui/src/base/alert_dialog.tsx +139 -0
  66. package/templates/_base/shared/ui/src/base/badge.tsx +33 -0
  67. package/templates/_base/shared/ui/src/base/basic_data_table.tsx +61 -0
  68. package/templates/_base/shared/ui/src/base/button.tsx +69 -0
  69. package/templates/_base/shared/ui/src/base/button_group.tsx +82 -0
  70. package/templates/_base/shared/ui/src/base/card.tsx +79 -0
  71. package/templates/_base/shared/ui/src/base/checkbox.tsx +26 -0
  72. package/templates/_base/shared/ui/src/base/command.tsx +165 -0
  73. package/templates/_base/shared/ui/src/base/dialog.tsx +129 -0
  74. package/templates/_base/shared/ui/src/base/dropdown_menu.tsx +232 -0
  75. package/templates/_base/shared/ui/src/base/form.tsx +161 -0
  76. package/templates/_base/shared/ui/src/base/input.tsx +129 -0
  77. package/templates/_base/shared/ui/src/base/label.tsx +19 -0
  78. package/templates/_base/shared/ui/src/base/popover.tsx +46 -0
  79. package/templates/_base/shared/ui/src/base/radio_group.tsx +49 -0
  80. package/templates/_base/shared/ui/src/base/resizable.tsx +55 -0
  81. package/templates/_base/shared/ui/src/base/scroll_area.tsx +44 -0
  82. package/templates/_base/shared/ui/src/base/select.tsx +151 -0
  83. package/templates/_base/shared/ui/src/base/separator.tsx +32 -0
  84. package/templates/_base/shared/ui/src/base/sheet.tsx +130 -0
  85. package/templates/_base/shared/ui/src/base/side_bar.tsx +688 -0
  86. package/templates/_base/shared/ui/src/base/skeleton.tsx +7 -0
  87. package/templates/_base/shared/ui/src/base/spinner.tsx +20 -0
  88. package/templates/_base/shared/ui/src/base/switch.tsx +27 -0
  89. package/templates/_base/shared/ui/src/base/table.tsx +91 -0
  90. package/templates/_base/shared/ui/src/base/text_area.tsx +21 -0
  91. package/templates/_base/shared/ui/src/base/tooltip.tsx +31 -0
  92. package/templates/_base/shared/ui/src/base/utils.ts +17 -0
  93. package/templates/_base/shared/ui/src/hooks/use_keyboard_press.tsx +48 -0
  94. package/templates/_base/shared/ui/src/hooks/use_keyboard_release.tsx +48 -0
  95. package/templates/_base/shared/ui/src/hooks/use_mobile.tsx +25 -0
  96. package/templates/_base/shared/ui/src/hooks/use_mouse_click.tsx +44 -0
  97. package/templates/_base/shared/ui/src/hooks/use_mouse_location.tsx +55 -0
  98. package/templates/_base/shared/ui/src/hooks/use_outside_click.tsx +29 -0
  99. package/templates/_base/shared/ui/src/hooks/use_query_params.tsx +33 -0
  100. package/templates/_base/shared/ui/tsconfig.json +8 -0
  101. package/templates/_base/shared/utils/src/convex.ts +3 -0
  102. package/templates/_base/shared/utils/src/time.ts +12 -0
  103. package/templates/_base/shared/utils/tsconfig.json +5 -0
  104. package/templates/_base/skills-lock.json +35 -0
  105. package/templates/_base/tsconfig.base.json +34 -0
  106. package/templates/nextjs/.env.example +8 -0
  107. package/templates/nextjs/index.d.ts +6 -0
  108. package/templates/nextjs/next-env.d.ts +5 -0
  109. package/templates/nextjs/next.config.js +22 -0
  110. package/templates/nextjs/postcss.config.js +17 -0
  111. package/templates/nextjs/project.json +22 -0
  112. package/templates/nextjs/src/app/(auth)/layout.tsx +21 -0
  113. package/templates/nextjs/src/app/(auth)/not-allowed/page.tsx +22 -0
  114. package/templates/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +15 -0
  115. package/templates/nextjs/src/app/(dashboard)/layout.tsx +27 -0
  116. package/templates/nextjs/src/app/(dashboard)/page.tsx +5 -0
  117. package/templates/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +23 -0
  118. package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +16 -0
  119. package/templates/nextjs/src/app/app.css +3 -0
  120. package/templates/nextjs/src/app/layout.tsx +26 -0
  121. package/templates/nextjs/src/convex.ts +11 -0
  122. package/templates/nextjs/src/middleware.ts +18 -0
  123. package/templates/nextjs/src/providers/convex_provider.tsx +44 -0
  124. package/templates/nextjs/src/surfaces/home_surface.tsx +22 -0
  125. package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +97 -0
  126. package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +107 -0
  127. package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +90 -0
  128. package/templates/nextjs/src/ui/sidebar/nav_link.tsx +36 -0
  129. package/templates/nextjs/src/ui/sidebar/sidebar.tsx +125 -0
  130. package/templates/nextjs/src/utils/font.ts +9 -0
  131. package/templates/nextjs/tsconfig.json +42 -0
  132. package/templates/react-router/.env.example +8 -0
  133. package/templates/react-router/postcss.config.js +15 -0
  134. package/templates/react-router/project.json +23 -0
  135. package/templates/react-router/public/favicon.ico +0 -0
  136. package/templates/react-router/react-router.config.ts +9 -0
  137. package/templates/react-router/src/app.css +3 -0
  138. package/templates/react-router/src/components/error_boundary.tsx +33 -0
  139. package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +76 -0
  140. package/templates/react-router/src/layouts/sidebar/sidebar_aside/user_menu.tsx +36 -0
  141. package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +22 -0
  142. package/templates/react-router/src/providers/api_auth_provider.tsx +38 -0
  143. package/templates/react-router/src/root.tsx +37 -0
  144. package/templates/react-router/src/routes/auth/layout.tsx +13 -0
  145. package/templates/react-router/src/routes/auth/sign-in.tsx +13 -0
  146. package/templates/react-router/src/routes/index.tsx +9 -0
  147. package/templates/react-router/src/routes/layout.tsx +26 -0
  148. package/templates/react-router/src/routes/todos/[id].tsx +22 -0
  149. package/templates/react-router/src/routes/todos/index.tsx +13 -0
  150. package/templates/react-router/src/routes.ts +12 -0
  151. package/templates/react-router/src/surfaces/home_surface.tsx +20 -0
  152. package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +87 -0
  153. package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +102 -0
  154. package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +81 -0
  155. package/templates/react-router/tsconfig.json +20 -0
  156. package/templates/react-router/vite.config.ts +40 -0
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import { Loader2Icon } from "lucide-react";
6
+
7
+ import { cn } from "@/ui/base/utils";
8
+
9
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
10
+ return (
11
+ <Loader2Icon
12
+ role="status"
13
+ aria-label="Loading"
14
+ className={cn("size-4 animate-spin", className)}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ export { Spinner };
@@ -0,0 +1,27 @@
1
+ import * as SwitchPrimitives from "@radix-ui/react-switch";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "@/ui/base/utils";
5
+
6
+ const Switch = React.forwardRef<
7
+ React.ElementRef<typeof SwitchPrimitives.Root>,
8
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <SwitchPrimitives.Root
11
+ className={cn(
12
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-color_tool.tsx focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
13
+ className,
14
+ )}
15
+ {...props}
16
+ ref={ref}
17
+ >
18
+ <SwitchPrimitives.Thumb
19
+ className={cn(
20
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
21
+ )}
22
+ />
23
+ </SwitchPrimitives.Root>
24
+ ));
25
+ Switch.displayName = SwitchPrimitives.Root.displayName;
26
+
27
+ export { Switch };
@@ -0,0 +1,91 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/ui/base/utils";
4
+
5
+ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div className="relative w-full overflow-auto">
8
+ <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
9
+ </div>
10
+ ),
11
+ );
12
+ Table.displayName = "Table";
13
+
14
+ const TableHeader = React.forwardRef<
15
+ HTMLTableSectionElement,
16
+ React.HTMLAttributes<HTMLTableSectionElement>
17
+ >(({ className, ...props }, ref) => (
18
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
19
+ ));
20
+ TableHeader.displayName = "TableHeader";
21
+
22
+ const TableBody = React.forwardRef<
23
+ HTMLTableSectionElement,
24
+ React.HTMLAttributes<HTMLTableSectionElement>
25
+ >(({ className, ...props }, ref) => (
26
+ <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
27
+ ));
28
+ TableBody.displayName = "TableBody";
29
+
30
+ const TableFooter = React.forwardRef<
31
+ HTMLTableSectionElement,
32
+ React.HTMLAttributes<HTMLTableSectionElement>
33
+ >(({ className, ...props }, ref) => (
34
+ <tfoot
35
+ ref={ref}
36
+ className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
37
+ {...props}
38
+ />
39
+ ));
40
+ TableFooter.displayName = "TableFooter";
41
+
42
+ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
43
+ ({ className, ...props }, ref) => (
44
+ <tr
45
+ ref={ref}
46
+ className={cn(
47
+ "border-b transition-color_tool.tsx hover:bg-muted/50 data-[design_doc=selected]:bg-muted",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ ),
53
+ );
54
+ TableRow.displayName = "TableRow";
55
+
56
+ const TableHead = React.forwardRef<
57
+ HTMLTableCellElement,
58
+ React.ThHTMLAttributes<HTMLTableCellElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <th
61
+ ref={ref}
62
+ className={cn(
63
+ "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ ));
69
+ TableHead.displayName = "TableHead";
70
+
71
+ const TableCell = React.forwardRef<
72
+ HTMLTableCellElement,
73
+ React.TdHTMLAttributes<HTMLTableCellElement>
74
+ >(({ className, ...props }, ref) => (
75
+ <td
76
+ ref={ref}
77
+ className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
78
+ {...props}
79
+ />
80
+ ));
81
+ TableCell.displayName = "TableCell";
82
+
83
+ const TableCaption = React.forwardRef<
84
+ HTMLTableCaptionElement,
85
+ React.HTMLAttributes<HTMLTableCaptionElement>
86
+ >(({ className, ...props }, ref) => (
87
+ <caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
88
+ ));
89
+ TableCaption.displayName = "TableCaption";
90
+
91
+ export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/ui/base/utils";
4
+
5
+ const TextArea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
6
+ ({ className, ...props }, ref) => {
7
+ return (
8
+ <textarea
9
+ className={cn(
10
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ className,
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ );
17
+ },
18
+ );
19
+ TextArea.displayName = "TextArea";
20
+
21
+ export { TextArea };
@@ -0,0 +1,31 @@
1
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "@/ui/base/utils";
5
+
6
+ const TooltipProvider = TooltipPrimitive.Provider;
7
+
8
+ const Tooltip = TooltipPrimitive.Root;
9
+
10
+ const TooltipTrigger = TooltipPrimitive.Trigger;
11
+
12
+ const TooltipContent = React.forwardRef<
13
+ React.ElementRef<typeof TooltipPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
15
+ >(({ className, sideOffset = 4, children, ...props }, ref) => (
16
+ <TooltipPrimitive.Content
17
+ ref={ref}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[design_doc=closed]:animate-out data-[design_doc=closed]:fade-out-0 data-[design_doc=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",
21
+ className,
22
+ )}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <TooltipPrimitive.Arrow className="bg-white" width={11} height={5} />
27
+ </TooltipPrimitive.Content>
28
+ ));
29
+
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
@@ -0,0 +1,17 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function formatList(items: string[], locale = "en"): string {
9
+ return new Intl.ListFormat(locale, {
10
+ style: "long",
11
+ type: "conjunction",
12
+ }).format(items);
13
+ }
14
+
15
+ export function capitaliseFirst(s: string): string {
16
+ return s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
17
+ }
@@ -0,0 +1,48 @@
1
+ import { Store } from "@tanstack/react-store";
2
+ import { useEffect } from "react";
3
+
4
+ export type KeyboardSubscriberCallback = (
5
+ key: string,
6
+ shift: boolean,
7
+ alt: boolean,
8
+ target: EventTarget | null,
9
+ ) => void;
10
+
11
+ export const keyboardSubscribers = new Store(
12
+ new Set<KeyboardSubscriberCallback>(),
13
+ );
14
+
15
+ /**
16
+ * Only use once in a project, likely at the surface entrypoint
17
+ */
18
+ export const useKeyboardPressObserver = () => {
19
+ useEffect(() => {
20
+ const listener = (e: KeyboardEvent) => {
21
+ for (const subscriber of keyboardSubscribers.state) {
22
+ subscriber(e.key, e.shiftKey, e.altKey, e.target);
23
+ }
24
+ };
25
+
26
+ document.addEventListener("keydown", listener);
27
+
28
+ return () => {
29
+ document.removeEventListener("keydown", listener);
30
+ keyboardSubscribers.state.clear();
31
+ };
32
+ }, []);
33
+ };
34
+
35
+ export const useKeyboardPressSubscriber = (
36
+ cb: KeyboardSubscriberCallback,
37
+ deps: any[],
38
+ ) => {
39
+ useEffect(() => {
40
+ keyboardSubscribers.state.add(cb);
41
+
42
+ return () => {
43
+ keyboardSubscribers.state.delete(cb);
44
+ };
45
+ }, deps);
46
+
47
+ return () => keyboardSubscribers.state.delete(cb);
48
+ };
@@ -0,0 +1,48 @@
1
+ import { Store } from "@tanstack/react-store";
2
+ import { useEffect } from "react";
3
+
4
+ export type KeyboardSubscriberCallback = (
5
+ key: string,
6
+ shift: boolean,
7
+ alt: boolean,
8
+ target: EventTarget | null,
9
+ ) => void;
10
+
11
+ export const keyboardSubscribers = new Store(
12
+ new Set<KeyboardSubscriberCallback>(),
13
+ );
14
+
15
+ /**
16
+ * Only use once in a project, likely at the surface entrypoint
17
+ */
18
+ export const useKeyboardReleaseObserver = () => {
19
+ useEffect(() => {
20
+ const listener = (e: KeyboardEvent) => {
21
+ for (const subscriber of keyboardSubscribers.state) {
22
+ subscriber(e.key, e.shiftKey, e.altKey, e.target);
23
+ }
24
+ };
25
+
26
+ document.addEventListener("keyup", listener);
27
+
28
+ return () => {
29
+ document.removeEventListener("keyup", listener);
30
+ keyboardSubscribers.state.clear();
31
+ };
32
+ }, []);
33
+ };
34
+
35
+ export const useKeyboardReleaseSubscriber = (
36
+ cb: KeyboardSubscriberCallback,
37
+ deps: any[],
38
+ ) => {
39
+ useEffect(() => {
40
+ keyboardSubscribers.state.add(cb);
41
+
42
+ return () => {
43
+ keyboardSubscribers.state.delete(cb);
44
+ };
45
+ }, deps);
46
+
47
+ return () => keyboardSubscribers.state.delete(cb);
48
+ };
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ "use client";
6
+
7
+ import { useEffect, useState } from "react";
8
+
9
+ const MOBILE_BREAKPOINT = 768;
10
+
11
+ export const useMobile = () => {
12
+ const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
13
+
14
+ useEffect(() => {
15
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
16
+ const onChange = () => {
17
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
18
+ };
19
+ mql.addEventListener("change", onChange);
20
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
21
+ return () => mql.removeEventListener("change", onChange);
22
+ }, []);
23
+
24
+ return !!isMobile;
25
+ };
@@ -0,0 +1,44 @@
1
+ import { Store } from "@tanstack/react-store";
2
+ import { useEffect } from "react";
3
+
4
+ export type MouseSubscriberCallback = (
5
+ position: [number, number],
6
+ target: EventTarget | null,
7
+ ) => void;
8
+
9
+ export const mouseSubscribers = new Store(new Set<MouseSubscriberCallback>());
10
+
11
+ /**
12
+ * Only use once in a project, likely at the surface entrypoint
13
+ */
14
+ export const useMouseClickObserver = () => {
15
+ useEffect(() => {
16
+ const listener = (e: MouseEvent) => {
17
+ for (const subscriber of mouseSubscribers.state) {
18
+ subscriber([e.clientX, e.clientY], e.target);
19
+ }
20
+ };
21
+
22
+ document.addEventListener("mousedown", listener);
23
+
24
+ return () => {
25
+ document.removeEventListener("mousedown", listener);
26
+ mouseSubscribers.state.clear();
27
+ };
28
+ }, []);
29
+ };
30
+
31
+ export const useMouseClickSubscriber = (
32
+ cb: MouseSubscriberCallback,
33
+ deps: any[],
34
+ ) => {
35
+ useEffect(() => {
36
+ mouseSubscribers.state.add(cb);
37
+
38
+ return () => {
39
+ mouseSubscribers.state.delete(cb);
40
+ };
41
+ }, deps);
42
+
43
+ return () => mouseSubscribers.state.delete(cb);
44
+ };
@@ -0,0 +1,55 @@
1
+ import { Store } from "@tanstack/react-store";
2
+ import { useEffect } from "react";
3
+
4
+ export type MouseLocationState = {
5
+ x: number;
6
+ y: number;
7
+ };
8
+
9
+ export const initialMouseLocationState: MouseLocationState = {
10
+ x: 0,
11
+ y: 0,
12
+ };
13
+
14
+ export const mouseLocationState = new Store<MouseLocationState>(
15
+ initialMouseLocationState,
16
+ );
17
+
18
+ export const mouseLocationSubscribers = new Store(
19
+ new Set<(state: MouseLocationState) => void>(),
20
+ );
21
+
22
+ export const setMouse = (x: number, y: number) =>
23
+ mouseLocationState.setState(() => ({ x, y }));
24
+
25
+ /**
26
+ * Only use once in a project, likely at the surface entrypoint
27
+ */
28
+ export const useMouseLocationObserver = () => {
29
+ useEffect(() => {
30
+ const listener = (e: MouseEvent) => {
31
+ setMouse(e.clientX, e.clientY);
32
+
33
+ for (const subscriber of mouseLocationSubscribers.state) {
34
+ subscriber(mouseLocationState.state);
35
+ }
36
+ };
37
+
38
+ document.addEventListener("mousemove", listener);
39
+
40
+ return () => {
41
+ document.removeEventListener("mousemove", listener);
42
+ mouseLocationSubscribers.state.clear();
43
+ };
44
+ }, []);
45
+ };
46
+
47
+ export const useMouseLocationSubscriber = (
48
+ cb: (state: MouseLocationState) => void,
49
+ ) => {
50
+ useEffect(() => {
51
+ mouseLocationSubscribers.state.add(cb);
52
+ }, []);
53
+
54
+ return () => mouseLocationSubscribers.state.delete(cb);
55
+ };
@@ -0,0 +1,29 @@
1
+ import type { RefObject } from "react";
2
+ import { useEffect } from "react";
3
+
4
+ /**
5
+ * Hook that alerts clicks outside the passed ref
6
+ */
7
+ export const useOutsideClick = (
8
+ ref: RefObject<HTMLDivElement>,
9
+ cb: () => void,
10
+ ) => {
11
+ useEffect(() => {
12
+ /**
13
+ * Alert if clicked on outside of element
14
+ */
15
+ function handleClickOutside(event: any) {
16
+ if (ref.current && !ref.current.contains(event.target)) {
17
+ cb();
18
+ }
19
+ }
20
+
21
+ // Bind the event listener
22
+ document.addEventListener("mousedown", handleClickOutside);
23
+
24
+ return () => {
25
+ // Unbind the event listener on clean up
26
+ document.removeEventListener("mousedown", handleClickOutside);
27
+ };
28
+ }, [ref]);
29
+ };
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import { useCallback } from "react";
6
+ import { usePathname, useSearchParams } from "next/navigation";
7
+ import { useRouter } from "next/navigation";
8
+
9
+ export const useQueryParams = () => {
10
+ const router = useRouter();
11
+ const pathname = usePathname();
12
+ const searchParams = useSearchParams();
13
+
14
+ return useCallback(
15
+ (paths: Record<string, string | null>) => {
16
+ const params = new URLSearchParams(searchParams.toString());
17
+
18
+ for (const [key, value] of Object.entries(paths)) {
19
+ if (value === null) {
20
+ params.delete(key);
21
+ continue;
22
+ }
23
+
24
+ params.set(key, value);
25
+ }
26
+
27
+ const path = pathname + "?" + params.toString();
28
+
29
+ return router.push(path);
30
+ },
31
+ [searchParams],
32
+ );
33
+ };
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "moduleResolution": "Node"
5
+ },
6
+ "include": ["src/base/**/*"],
7
+ "exclude": ["node_modules"]
8
+ }
@@ -0,0 +1,3 @@
1
+ import { ConvexClient } from "convex/browser";
2
+
3
+ export const convex = new ConvexClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
@@ -0,0 +1,12 @@
1
+ import dayjs from "dayjs";
2
+ import relativeTime from "dayjs/plugin/relativeTime";
3
+
4
+ dayjs.extend(relativeTime);
5
+
6
+ export const formatTime = (date: Date | number) => {
7
+ return dayjs(date).format("DD/MM/YYYY hh:mma");
8
+ };
9
+
10
+ export const formatDistanceToNow = (timestamp: number) => {
11
+ return dayjs(timestamp).fromNow();
12
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules"]
5
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "shadcn": {
5
+ "source": "shadcn/ui",
6
+ "sourceType": "github",
7
+ "computedHash": "1ec42c9b918b315d8e5c6c856b78c3c4725fc93dea30b9b7e9975c69848fcce6"
8
+ },
9
+ "clerk": {
10
+ "source": "clerk/skills",
11
+ "sourceType": "github",
12
+ "computedHash": "912205692b6c1db07d6933f0584af27c6582484e5d6fac1c13fec4b6a963a528"
13
+ },
14
+ "clerk-backend-api": {
15
+ "source": "clerk/skills",
16
+ "sourceType": "github",
17
+ "computedHash": "4f51a8aac3bb33c5dd6ec06740e13db9c45ad40bb372c918155ea8076f80367b"
18
+ },
19
+ "clerk-custom-ui": {
20
+ "source": "clerk/skills",
21
+ "sourceType": "github",
22
+ "computedHash": "bf28bf1908604c50876a302c8bebd90cf31b04fcf0482ccfc542b22778677bcb"
23
+ },
24
+ "clerk-nextjs-patterns": {
25
+ "source": "clerk/skills",
26
+ "sourceType": "github",
27
+ "computedHash": "c55aeca9c8ca0602af4a2bdc0370e3c349e53752177cb1187edf213f51d34636"
28
+ },
29
+ "clerk-webhooks": {
30
+ "source": "clerk/skills",
31
+ "sourceType": "github",
32
+ "computedHash": "6a13eea450dda0b57edc358970f53c55ad7a2eb237d31decbc38e312555ddac5"
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "baseUrl": ".",
6
+ "moduleResolution": "Bundler",
7
+ "module": "ESNext",
8
+ "target": "ESNext",
9
+ "jsx": "react-jsx",
10
+ "allowJs": true,
11
+ "lib": ["ESNext", "DOM"],
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "strictNullChecks": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "isolatedModules": true,
17
+ "resolveJsonModule": true,
18
+ "paths": {
19
+ "@/api/*": ["./apps/api/*"],
20
+ "@/assets/*": ["./shared/assets/src/*"],
21
+ "@/ui/*": ["./shared/ui/src/*"],
22
+ "@/utils/*": ["./shared/utils/src/*"],
23
+ "@/emails/*": ["./emails/*"],
24
+ "@/web/*": ["./apps/web/src/*"]
25
+ }
26
+ },
27
+ "exclude": ["node_modules", "dist", "build", "public"],
28
+ "include": [
29
+ "./apps/api/src/**/*",
30
+ "./apps/web/src/**/*",
31
+ "./shared/**/*",
32
+ "./tsconfig.json"
33
+ ]
34
+ }
@@ -0,0 +1,8 @@
1
+ # Convex
2
+ CONVEX_DEPLOYMENT=
3
+ CONVEX_DEPLOYMENT_URL=
4
+ NEXT_PUBLIC_CONVEX_URL=
5
+
6
+ # Authentication
7
+ CLERK_SECRET_KEY=
8
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
@@ -0,0 +1,6 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ declare module '*.svg' {
3
+ const content: any;
4
+ export const ReactComponent: any;
5
+ export default content;
6
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,22 @@
1
+ //@ts-check
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
4
+ const { composePlugins, withNx } = require("@nx/next");
5
+
6
+ /**
7
+ * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
8
+ **/
9
+ const nextConfig = {
10
+ nx: {
11
+ // Set this to true if you would like to use SVGR
12
+ // See: https://github.com/gregberge/svgr
13
+ svgr: false,
14
+ },
15
+ };
16
+
17
+ const plugins = [
18
+ // Add more Next.js plugins to this list if needed.
19
+ withNx,
20
+ ];
21
+
22
+ module.exports = composePlugins(...plugins)(nextConfig);
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ const { join } = require("node:path");
6
+
7
+ // Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
8
+ // option from your application's configuration (i.e. project.json).
9
+ //
10
+ // See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries
11
+
12
+ module.exports = {
13
+ plugins: {
14
+ "@tailwindcss/postcss": {},
15
+ autoprefixer: {},
16
+ },
17
+ };