From f9f26bb590d4458afdee08426cb1860a7b0e7e1d Mon Sep 17 00:00:00 2001 From: RevIQ Date: Sat, 10 Jan 2026 15:26:49 +0800 Subject: [PATCH 1/6] Add shadcn UI components and replace raw HTML form elements - Install 8 shadcn components: select, skeleton, checkbox, switch, avatar, dropdown-menu, tooltip, textarea - Replace raw diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/index.ts b/apps/publisher-dashboard/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..a7865f5 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Content from "./tooltip-content.svelte"; +import Portal from "./tooltip-portal.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Trigger from "./tooltip-trigger.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..118ccac --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
+ {/snippet} +
+
+
diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..9b32bbf --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..7bea75e --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..57370f8 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..90e83e6 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte b/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte index 916ca58..f98027f 100644 --- a/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte +++ b/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte @@ -1,12 +1,5 @@ diff --git a/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte b/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte index c0c0afa..a192c94 100644 --- a/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte +++ b/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte @@ -1,5 +1,9 @@ @@ -86,35 +150,10 @@ function handleNavClick() { /> - {/if} - {item.label} - - {/each} - - - - -
- {#each bottomItems as item} - {@const isActive = $page.url.pathname === item.href} - - {#if item.icon === "settings"} + {:else if item.icon === "building"} - - + + {/if} {item.label} @@ -126,14 +165,47 @@ function handleNavClick() { diff --git a/apps/publisher-dashboard/src/lib/components/layout/org-switcher.svelte b/apps/publisher-dashboard/src/lib/components/layout/org-switcher.svelte new file mode 100644 index 0000000..c3b2afb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/layout/org-switcher.svelte @@ -0,0 +1,89 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Organizations + + {#if orgsQuery.isPending} + Loading... + {:else if orgs.length === 0} + No organizations + {:else} + {#each orgs as org} + {@const isActive = currentSlug === org.slug} + handleOrgSelect(org.slug)} + class={cn(isActive && "bg-accent")} + > +
+ {#if org.logoUrl} + + {:else} +
+ {org.displayName.charAt(0).toUpperCase()} +
+ {/if} + {org.displayName} + {#if isActive} + + + + {/if} +
+
+ {/each} + {/if} + + goto("/dashboard/new")}> +
+ + + + + Create New Organization +
+
+
+
diff --git a/apps/publisher-dashboard/src/lib/components/layout/user-menu.svelte b/apps/publisher-dashboard/src/lib/components/layout/user-menu.svelte new file mode 100644 index 0000000..f5d62bd --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/layout/user-menu.svelte @@ -0,0 +1,112 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + +
+ {#if user?.avatarUrl} + + {:else} +
+ {initials} +
+ {/if} +
+ {user?.displayName ?? user?.email ?? "Loading..."} + {#if currentUserRole} + {currentUserRole} + {:else if user?.email && user?.displayName} + {user.email} + {/if} +
+
+ + goto("/account")}> + + + + + Account Settings + + + + + + + + + Sign out + +
+
diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..5fe30f7 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..49be469 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,43 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..ef29949 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..a84920a --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..9b8c8e1 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..e1278b4 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..9ab03be --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..79e897c --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..f1dced8 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..d4f7ef3 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..f7b3d5f --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..a27ab29 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..67f6303 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..cc5bc78 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..48d664b --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..51774fa --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,10 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..f1b7449 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/index.ts b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..41ef3f9 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/index.ts b/apps/publisher-dashboard/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000..2c3e67d --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/index.ts @@ -0,0 +1,19 @@ +import Root from "./popover.svelte"; +import Close from "./popover-close.svelte"; +import Content from "./popover-content.svelte"; +import Portal from "./popover-portal.svelte"; +import Trigger from "./popover-trigger.svelte"; + +export { + Root, + Content, + Trigger, + Close, + Portal, + // + Root as Popover, + Content as PopoverContent, + Trigger as PopoverTrigger, + Close as PopoverClose, + Portal as PopoverPortal, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-close.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-close.svelte new file mode 100644 index 0000000..77cef23 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-close.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000..c42998f --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-portal.svelte new file mode 100644 index 0000000..d76241e --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000..567deeb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover.svelte new file mode 100644 index 0000000..018f415 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/routes/performance/+page.svelte b/apps/publisher-dashboard/src/routes/dashboard/[slug]/performance/+page.svelte similarity index 91% rename from apps/publisher-dashboard/src/routes/performance/+page.svelte rename to apps/publisher-dashboard/src/routes/dashboard/[slug]/performance/+page.svelte index 09fa1ab..fd50f5d 100644 --- a/apps/publisher-dashboard/src/routes/performance/+page.svelte +++ b/apps/publisher-dashboard/src/routes/dashboard/[slug]/performance/+page.svelte @@ -1,10 +1,15 @@ + + + Reports - Publisher Dashboard + + + +
+
+
+ + + + +
+

Coming Soon

+

+ Advanced reporting features are currently in development. +

+
+
+
diff --git a/apps/publisher-dashboard/src/routes/performance/+layout.ts b/apps/publisher-dashboard/src/routes/performance/+layout.ts deleted file mode 100644 index 89da957..0000000 --- a/apps/publisher-dashboard/src/routes/performance/+layout.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const ssr = false; -export const prerender = true; diff --git a/bun.lock b/bun.lock index 392b2e3..b063665 100644 --- a/bun.lock +++ b/bun.lock @@ -91,7 +91,7 @@ }, "devDependencies": { "@internationalized/date": "^3.10.1", - "@lucide/svelte": "^0.562.0", + "@lucide/svelte": "^0.561.0", "@macalinao/eslint-config": "catalog:", "@macalinao/tsconfig": "catalog:", "@sveltejs/adapter-static": "^3.0.8", @@ -334,7 +334,7 @@ "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], - "@lucide/svelte": ["@lucide/svelte@0.562.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-wDMULwtTFN2Sc/TFBm6gfuVCNb4Y5P9LDrwxNnUbV52+IEU7NXZmvxwXoz+vrrpad6Xupq+Hw5eUlqIHEGhouw=="], + "@lucide/svelte": ["@lucide/svelte@0.561.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A=="], "@macalinao/biome-config": ["@macalinao/biome-config@0.1.7", "", { "peerDependencies": { "@biomejs/biome": "^2.3.10" } }, "sha512-JijaB/REJr6D3fGV36d1XGsf2WFofgnMS1WbOYcNJCQpic2XmFALV7GNL28z7rDCN3/DeSovPuW/1yImce7kPA=="], From 93abba044a647b8619512f9b6c0ff959e74bea7f Mon Sep 17 00:00:00 2001 From: RevIQ Date: Sat, 10 Jan 2026 15:56:51 +0800 Subject: [PATCH 3/6] Add root test script using Turborepo Co-Authored-By: Claude Opus 4.5 --- package.json | 1 + turbo.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index c2f32e2..740d31d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint:fix": "biome check --write --unsafe && turbo run lint -- --fix", "typecheck": "turbo typecheck", "clean": "turbo clean", + "test": "turbo test", "db:codegen": "bun run --cwd packages/db-schema generate" }, "devDependencies": { diff --git a/turbo.json b/turbo.json index 9e0a980..4e37ec2 100644 --- a/turbo.json +++ b/turbo.json @@ -29,6 +29,11 @@ "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "src/**/*.test.ts"], "cache": false + }, + "test": { + "dependsOn": ["^build"], + "inputs": ["src/**/*.ts", "src/**/*.test.ts"], + "cache": false } } } From f394b8002857d371cfaabe5cd45627e9453c3ec0 Mon Sep 17 00:00:00 2001 From: RevIQ Date: Sat, 10 Jan 2026 16:07:32 +0800 Subject: [PATCH 4/6] Add PhoneNumberInput component with libphonenumber-js formatting Uses AsYouType for real-time phone number formatting as user types. Implements digit-based cursor positioning to handle formatting changes without cursor jumping. Co-Authored-By: Claude Opus 4.5 --- .../components/ui/phone-number-input/index.ts | 7 ++ .../phone-number-input.svelte | 111 ++++++++++++++++++ .../src/routes/account/+page.svelte | 4 +- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts create mode 100644 apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte diff --git a/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts new file mode 100644 index 0000000..2de5dd2 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts @@ -0,0 +1,7 @@ +import Root from "./phone-number-input.svelte"; + +export { + Root, + // + Root as PhoneNumberInput, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte new file mode 100644 index 0000000..661d7f9 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte @@ -0,0 +1,111 @@ + + + diff --git a/apps/publisher-dashboard/src/routes/account/+page.svelte b/apps/publisher-dashboard/src/routes/account/+page.svelte index bf721f2..a4e5c89 100644 --- a/apps/publisher-dashboard/src/routes/account/+page.svelte +++ b/apps/publisher-dashboard/src/routes/account/+page.svelte @@ -16,6 +16,7 @@ import { } from "$lib/components/ui/card"; import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; +import { PhoneNumberInput } from "$lib/components/ui/phone-number-input"; import { LoadingButton } from "$lib/components/ui/loading-button"; import { Separator } from "$lib/components/ui/separator"; import { cn } from "$lib/utils"; @@ -224,9 +225,8 @@ function getInitials(name: string | null | undefined): string {
- Date: Sat, 10 Jan 2026 16:30:22 +0800 Subject: [PATCH 5/6] Replace void returns with { success: true } across all API endpoints - Add successResponseSchema to common.ts for explicit success responses - Update all auth, me, orgs, and admin procedures to return { success: true } - Update contract.ts to use successResponseSchema instead of z.void() - Add ast-grep rule to prevent future z.void() usage in contracts - Add build:packages script to root package.json - Fix test file lint errors with eslint-disable comments Co-Authored-By: Claude Opus 4.5 --- .ast-grep/rules/no-void-output.yml | 15 +++ apps/api-server/src/__tests__/e2e/me.test.ts | 14 ++- .../src/__tests__/helpers/test-db.ts | 9 +- .../procedures/admin/auth/complete-login.ts | 2 + .../src/procedures/admin/orgs/delete.ts | 2 + .../src/procedures/admin/orgs/sites.ts | 4 + .../src/procedures/admin/orgs/update.ts | 4 +- .../procedures/admin/users/confirm-email.ts | 2 + .../src/procedures/admin/users/create.ts | 2 + .../src/procedures/admin/users/update.ts | 4 +- .../src/procedures/auth/forgot-password.ts | 1 + .../procedures/auth/login-password-confirm.ts | 4 +- .../src/procedures/auth/login-password.ts | 2 +- apps/api-server/src/procedures/auth/logout.ts | 2 + .../procedures/auth/resend-verification.ts | 4 +- .../src/procedures/auth/reset-password.ts | 2 +- apps/api-server/src/procedures/auth/signup.ts | 2 + .../src/procedures/auth/verify-email.ts | 2 + apps/api-server/src/procedures/me/delete.ts | 2 + apps/api-server/src/procedures/me/devices.ts | 6 + apps/api-server/src/procedures/me/passkeys.ts | 4 + apps/api-server/src/procedures/me/sessions.ts | 4 + .../src/procedures/me/set-password.ts | 2 + .../src/procedures/me/update-profile.ts | 2 + .../api-server/src/procedures/orgs/invites.ts | 6 + .../src/procedures/orgs/management.ts | 6 + .../api-server/src/procedures/orgs/members.ts | 4 + apps/api-server/src/router.ts | 4 + package.json | 1 + packages/api-contract/src/contract.ts | 108 ++++++++++++------ packages/api-contract/src/schemas/common.ts | 6 + 31 files changed, 179 insertions(+), 53 deletions(-) create mode 100644 .ast-grep/rules/no-void-output.yml diff --git a/.ast-grep/rules/no-void-output.yml b/.ast-grep/rules/no-void-output.yml new file mode 100644 index 0000000..7a44f86 --- /dev/null +++ b/.ast-grep/rules/no-void-output.yml @@ -0,0 +1,15 @@ +id: no-void-output +language: typescript +severity: error +message: Do not use z.void() for output - use successResponseSchema instead +note: | + Endpoints should return `{ success: true }` instead of void. + This makes the API more explicit and avoids issues with TypeScript + expecting void-returning Promises. + + Replace `.output(z.void())` with `.output(successResponseSchema)` and ensure + the handler returns `{ success: true }`. +rule: + pattern: $EXPR.output(z.void()) +files: + - packages/api-contract/**/*.ts diff --git a/apps/api-server/src/__tests__/e2e/me.test.ts b/apps/api-server/src/__tests__/e2e/me.test.ts index 4caa9d3..9257cea 100644 --- a/apps/api-server/src/__tests__/e2e/me.test.ts +++ b/apps/api-server/src/__tests__/e2e/me.test.ts @@ -23,9 +23,9 @@ import { } from "bun:test"; import { call } from "@orpc/server"; import { router } from "../../router.js"; -import { hashPassword } from "../../utils/password.js"; -import { hashToken } from "../../utils/crypto.js"; import { COOKIE_NAMES } from "../../utils/cookies.js"; +import { hashToken } from "../../utils/crypto.js"; +import { hashPassword } from "../../utils/password.js"; import { TEST_RP } from "../helpers/test-constants.js"; import { createTestDb, @@ -85,7 +85,7 @@ function createAPIContext(options?: { * Create a real session in the database and return the token */ async function createSession(userId: number): Promise { - const token = "test-session-" + String(Date.now()) + String(Math.random()); + const token = `test-session-${String(Date.now())}${String(Math.random())}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -110,8 +110,7 @@ async function createSession(userId: number): Promise { async function createApiToken( userId: number, ): Promise<{ token: string; name: string }> { - const token = - "test-api-token-" + String(Date.now()) + String(Math.random()); + const token = `test-api-token-${String(Date.now())}${String(Math.random())}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS); @@ -523,6 +522,7 @@ describe("me.setPassword", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call( router.me.setPassword, @@ -544,6 +544,7 @@ describe("me.setPassword", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call( router.me.setPassword, @@ -567,6 +568,7 @@ describe("me.setPassword", () => { // Password must be at least 8 chars to pass schema validation // "password" passes length check but fails zxcvbn strength check // zxcvbn provides feedback like "This is a top-10 common password" + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call( router.me.setPassword, @@ -611,6 +613,7 @@ describe("me.delete", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call(router.me.delete, { password: "anything" }, { context }), ).rejects.toThrow("Cannot delete account without a password"); @@ -626,6 +629,7 @@ describe("me.delete", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call(router.me.delete, { password: "WrongPassword123!" }, { context }), ).rejects.toThrow("Incorrect password"); diff --git a/apps/api-server/src/__tests__/helpers/test-db.ts b/apps/api-server/src/__tests__/helpers/test-db.ts index 77bb019..82b406a 100644 --- a/apps/api-server/src/__tests__/helpers/test-db.ts +++ b/apps/api-server/src/__tests__/helpers/test-db.ts @@ -3,9 +3,9 @@ */ import type { Database } from "@reviq/db-schema"; +import type { Kysely } from "kysely"; import { join } from "node:path"; import { createDb } from "@reviq/db"; -import type { Kysely } from "kysely"; import { sql } from "kysely"; import pg from "pg"; @@ -134,14 +134,13 @@ async function ensureTestDatabaseExists(): Promise { * * @throws Error if repo root cannot be found */ -function findRepoRoot(): string { - const { existsSync } = require("node:fs"); +async function findRepoRoot(): Promise { let current = import.meta.dir; // Walk up to 10 levels to find the repo root for (let i = 0; i < 10; i++) { const migrationsPath = join(current, "db", "migrations"); - if (existsSync(migrationsPath)) { + if (await Bun.file(migrationsPath).exists()) { return current; } const parent = join(current, ".."); @@ -167,7 +166,7 @@ export async function runMigrations(): Promise { // Ensure the database exists first await ensureTestDatabaseExists(); - const repoRoot = findRepoRoot(); + const repoRoot = await findRepoRoot(); const proc = Bun.spawn(["dbmate", "up"], { env: { ...process.env, DATABASE_URL: testDbUrl }, diff --git a/apps/api-server/src/procedures/admin/auth/complete-login.ts b/apps/api-server/src/procedures/admin/auth/complete-login.ts index 74e49d7..bb25325 100644 --- a/apps/api-server/src/procedures/admin/auth/complete-login.ts +++ b/apps/api-server/src/procedures/admin/auth/complete-login.ts @@ -46,4 +46,6 @@ export const adminAuthCompleteLogin = os.admin.auth.completeLogin .set({ completed_at: new Date() }) .where("id", "=", anyRequest.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/delete.ts b/apps/api-server/src/procedures/admin/orgs/delete.ts index 8b1e609..1cfd440 100644 --- a/apps/api-server/src/procedures/admin/orgs/delete.ts +++ b/apps/api-server/src/procedures/admin/orgs/delete.ts @@ -33,4 +33,6 @@ export const adminOrgsDelete = os.admin.orgs.delete .execute(); await trx.deleteFrom("orgs").where("id", "=", org.id).execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/sites.ts b/apps/api-server/src/procedures/admin/orgs/sites.ts index 250ebe6..aa0fc7b 100644 --- a/apps/api-server/src/procedures/admin/orgs/sites.ts +++ b/apps/api-server/src/procedures/admin/orgs/sites.ts @@ -68,6 +68,8 @@ export const adminOrgsAddSite = os.admin.orgs.addSite }) .execute(); }); + + return { success: true }; }); export const adminOrgsRemoveSite = os.admin.orgs.removeSite @@ -94,4 +96,6 @@ export const adminOrgsRemoveSite = os.admin.orgs.removeSite if (!result.numDeletedRows || result.numDeletedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Site not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/update.ts b/apps/api-server/src/procedures/admin/orgs/update.ts index d2b2bb1..27f4aa3 100644 --- a/apps/api-server/src/procedures/admin/orgs/update.ts +++ b/apps/api-server/src/procedures/admin/orgs/update.ts @@ -22,7 +22,7 @@ export const adminOrgsUpdate = os.admin.orgs.update if (!org) { throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); } - return; + return { success: true }; } const updates: Partial<{ @@ -47,4 +47,6 @@ export const adminOrgsUpdate = os.admin.orgs.update if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/confirm-email.ts b/apps/api-server/src/procedures/admin/users/confirm-email.ts index a2fa6f1..782306f 100644 --- a/apps/api-server/src/procedures/admin/users/confirm-email.ts +++ b/apps/api-server/src/procedures/admin/users/confirm-email.ts @@ -21,4 +21,6 @@ export const adminUsersConfirmEmail = os.admin.users.confirmEmail if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "User not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/create.ts b/apps/api-server/src/procedures/admin/users/create.ts index 1e431a4..a23029e 100644 --- a/apps/api-server/src/procedures/admin/users/create.ts +++ b/apps/api-server/src/procedures/admin/users/create.ts @@ -60,4 +60,6 @@ export const adminUsersCreate = os.admin.users.create .execute(); } }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/update.ts b/apps/api-server/src/procedures/admin/users/update.ts index ddfc353..6e8f3e6 100644 --- a/apps/api-server/src/procedures/admin/users/update.ts +++ b/apps/api-server/src/procedures/admin/users/update.ts @@ -23,7 +23,7 @@ export const adminUsersUpdate = os.admin.users.update if (!user) { throw new ORPCError("NOT_FOUND", { message: "User not found" }); } - return; + return { success: true }; } // Prevent superuser from demoting themselves @@ -45,4 +45,6 @@ export const adminUsersUpdate = os.admin.users.update if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "User not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/forgot-password.ts b/apps/api-server/src/procedures/auth/forgot-password.ts index 55a676b..40a57c6 100644 --- a/apps/api-server/src/procedures/auth/forgot-password.ts +++ b/apps/api-server/src/procedures/auth/forgot-password.ts @@ -57,5 +57,6 @@ export const forgotPassword = os.auth.forgotPassword.handler( // Always return success (anti-enumeration) // Don't reveal whether the email exists or not + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/login-password-confirm.ts b/apps/api-server/src/procedures/auth/login-password-confirm.ts index 64f6262..655f4ae 100644 --- a/apps/api-server/src/procedures/auth/login-password-confirm.ts +++ b/apps/api-server/src/procedures/auth/login-password-confirm.ts @@ -41,7 +41,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler( // If already completed, return success (idempotent) if (loginRequest.completed_at !== null) { - return; + return { success: true }; } // Mark as completed @@ -50,5 +50,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler( .set({ completed_at: new Date() }) .where("id", "=", loginRequest.id) .execute(); + + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/login-password.ts b/apps/api-server/src/procedures/auth/login-password.ts index 456fd97..6db8663 100644 --- a/apps/api-server/src/procedures/auth/login-password.ts +++ b/apps/api-server/src/procedures/auth/login-password.ts @@ -111,6 +111,6 @@ export const loginPassword = os.auth.loginPassword.handler( await sendLoginConfirmationEmail(result.email, result.token); } - // Return void (success) + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/logout.ts b/apps/api-server/src/procedures/auth/logout.ts index 1fa5762..c964d7c 100644 --- a/apps/api-server/src/procedures/auth/logout.ts +++ b/apps/api-server/src/procedures/auth/logout.ts @@ -23,4 +23,6 @@ export const logout = os.auth.logout // Clear the session cookie deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/resend-verification.ts b/apps/api-server/src/procedures/auth/resend-verification.ts index 5f2eff7..66bade8 100644 --- a/apps/api-server/src/procedures/auth/resend-verification.ts +++ b/apps/api-server/src/procedures/auth/resend-verification.ts @@ -24,7 +24,7 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail // Check if email is already verified if (context.user.emailVerifiedAt !== null) { // Email already verified, return early - return; + return { success: true }; } // Delete any existing verification tokens for this user @@ -49,4 +49,6 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail // Send verification email (stubbed) await sendVerificationEmail(context.user.email, token); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/reset-password.ts b/apps/api-server/src/procedures/auth/reset-password.ts index af51079..f98baf9 100644 --- a/apps/api-server/src/procedures/auth/reset-password.ts +++ b/apps/api-server/src/procedures/auth/reset-password.ts @@ -84,6 +84,6 @@ export const resetPassword = os.auth.resetPassword.handler( .where("revoked_at", "is", null) .execute(); - // Return void on success + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/signup.ts b/apps/api-server/src/procedures/auth/signup.ts index 33338d5..3015c8f 100644 --- a/apps/api-server/src/procedures/auth/signup.ts +++ b/apps/api-server/src/procedures/auth/signup.ts @@ -280,4 +280,6 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => { // Send verification email (stubbed) await sendVerificationEmail(email, verificationToken); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/verify-email.ts b/apps/api-server/src/procedures/auth/verify-email.ts index bdc4139..a39c206 100644 --- a/apps/api-server/src/procedures/auth/verify-email.ts +++ b/apps/api-server/src/procedures/auth/verify-email.ts @@ -54,5 +54,7 @@ export const verifyEmail = os.auth.verifyEmail.handler( .deleteFrom("email_verifications") .where("id", "=", verification.id) .execute(); + + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/me/delete.ts b/apps/api-server/src/procedures/me/delete.ts index a58f0c4..e5cf875 100644 --- a/apps/api-server/src/procedures/me/delete.ts +++ b/apps/api-server/src/procedures/me/delete.ts @@ -47,4 +47,6 @@ export const meDelete = os.me.delete // Clear session cookie deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/devices.ts b/apps/api-server/src/procedures/me/devices.ts index 2941480..5a39c1f 100644 --- a/apps/api-server/src/procedures/me/devices.ts +++ b/apps/api-server/src/procedures/me/devices.ts @@ -64,6 +64,8 @@ export const trustDevice = os.me.trustDevice if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Device not found" }); } + + return { success: true }; }); /** @@ -113,6 +115,8 @@ export const untrustDevice = os.me.untrustDevice if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Device not found" }); } + + return { success: true }; }); /** @@ -128,4 +132,6 @@ export const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices .set({ is_trusted: false }) .where("user_id", "=", context.user.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/passkeys.ts b/apps/api-server/src/procedures/me/passkeys.ts index e6ea66d..b093580 100644 --- a/apps/api-server/src/procedures/me/passkeys.ts +++ b/apps/api-server/src/procedures/me/passkeys.ts @@ -45,6 +45,8 @@ export const renamePasskey = os.me.passkeys.rename if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Passkey not found" }); } + + return { success: true }; }); /** @@ -92,4 +94,6 @@ export const deletePasskey = os.me.passkeys.delete throw new ORPCError("NOT_FOUND", { message: "Passkey not found" }); } }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/sessions.ts b/apps/api-server/src/procedures/me/sessions.ts index f8d1fdd..d6cd434 100644 --- a/apps/api-server/src/procedures/me/sessions.ts +++ b/apps/api-server/src/procedures/me/sessions.ts @@ -65,6 +65,8 @@ export const revokeSession = os.me.revokeSession if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Session not found" }); } + + return { success: true }; }); /** @@ -83,4 +85,6 @@ export const revokeAllSessions = os.me.revokeAllSessions .where("id", "!=", context.session.id) .where("revoked_at", "is", null) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/set-password.ts b/apps/api-server/src/procedures/me/set-password.ts index ba4663d..beacc87 100644 --- a/apps/api-server/src/procedures/me/set-password.ts +++ b/apps/api-server/src/procedures/me/set-password.ts @@ -58,4 +58,6 @@ export const setPassword = os.me.setPassword .set({ password_hash: newHash, updated_at: new Date() }) .where("id", "=", context.user.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/update-profile.ts b/apps/api-server/src/procedures/me/update-profile.ts index 3d59510..b23c090 100644 --- a/apps/api-server/src/procedures/me/update-profile.ts +++ b/apps/api-server/src/procedures/me/update-profile.ts @@ -36,4 +36,6 @@ export const updateProfile = os.me.updateProfile .where("id", "=", context.user.id) .execute(); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/invites.ts b/apps/api-server/src/procedures/orgs/invites.ts index 4089ad2..842c90f 100644 --- a/apps/api-server/src/procedures/orgs/invites.ts +++ b/apps/api-server/src/procedures/orgs/invites.ts @@ -123,6 +123,8 @@ export const invitesCreate = os.orgs.invites.create // Send invitation email const inviterName = context.user.displayName ?? context.user.email; await sendOrgInviteEmail(email, token, org.displayName, inviterName, role); + + return { success: true }; }); /** @@ -149,6 +151,8 @@ export const invitesCancel = os.orgs.invites.cancel if (!result.numDeletedRows || result.numDeletedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Invitation not found" }); } + + return { success: true }; }); /** @@ -219,4 +223,6 @@ export const invitesAccept = os.orgs.invites.accept } throw error; } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/management.ts b/apps/api-server/src/procedures/orgs/management.ts index 57ff4cf..00a72ae 100644 --- a/apps/api-server/src/procedures/orgs/management.ts +++ b/apps/api-server/src/procedures/orgs/management.ts @@ -39,6 +39,8 @@ export const orgsUpdate = os.orgs.update .set(updates) .where("id", "=", org.id) .execute(); + + return { success: true }; }); /** @@ -57,6 +59,8 @@ export const orgsDelete = os.orgs.delete requireRole(membership, "owner"); await context.db.deleteFrom("orgs").where("id", "=", org.id).execute(); + + return { success: true }; }); /** @@ -92,4 +96,6 @@ export const orgsLeave = os.orgs.leave .where("user_id", "=", context.user.id) .execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/members.ts b/apps/api-server/src/procedures/orgs/members.ts index 1a39aba..8bcc681 100644 --- a/apps/api-server/src/procedures/orgs/members.ts +++ b/apps/api-server/src/procedures/orgs/members.ts @@ -95,6 +95,8 @@ export const membersUpdateRole = os.orgs.members.updateRole .where("id", "=", targetMember.id) .execute(); }); + + return { success: true }; }); /** @@ -155,4 +157,6 @@ export const membersRemove = os.orgs.members.remove .where("id", "=", targetMember.id) .execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index 08bfef9..e4de764 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -160,6 +160,8 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication .set({ completed_at: new Date() }) .where("id", "=", String(context.loginRequestId)) .execute(); + + return { success: true }; }); // Me procedures @@ -245,6 +247,8 @@ const setupProfile = os.me.setupProfile }) .where("id", "=", context.user.id) .execute(); + + return { success: true }; }); // Me procedures imported from ./procedures/me/* diff --git a/package.json b/package.json index c2f32e2..b014ed6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dev": "turbo dev", "build": "turbo build", "build:watch:packages": "turbo watch build --filter=./packages/*", + "build:packages": "turbo build --filter=./packages/*", "lint": "biome check && turbo run lint", "lint:fix": "biome check --write --unsafe && turbo run lint -- --fix", "typecheck": "turbo typecheck", diff --git a/packages/api-contract/src/contract.ts b/packages/api-contract/src/contract.ts index b2e477f..59ed053 100644 --- a/packages/api-contract/src/contract.ts +++ b/packages/api-contract/src/contract.ts @@ -22,7 +22,11 @@ import { signupInputSchema, verifyEmailInputSchema, } from "./schemas/auth.js"; -import { emailSchema, slugSchema } from "./schemas/common.js"; +import { + emailSchema, + slugSchema, + successResponseSchema, +} from "./schemas/common.js"; import { createInviteInputSchema, createOrgInputSchema, @@ -51,26 +55,32 @@ import { export const contract = oc.router({ auth: oc.router({ // Signup and verification - signup: oc.input(signupInputSchema).output(z.void()), - verifyEmail: oc.input(verifyEmailInputSchema).output(z.void()), - resendVerificationEmail: oc.output(z.void()), + signup: oc.input(signupInputSchema).output(successResponseSchema), + verifyEmail: oc.input(verifyEmailInputSchema).output(successResponseSchema), + resendVerificationEmail: oc.output(successResponseSchema), // Login flow createLoginRequest: oc .input(loginRequestInputSchema) .output(loginRequestOutputSchema), - loginPassword: oc.input(loginPasswordInputSchema).output(z.void()), + loginPassword: oc + .input(loginPasswordInputSchema) + .output(successResponseSchema), loginPasswordConfirm: oc .input(z.object({ token: z.string() })) - .output(z.void()), + .output(successResponseSchema), loginIfRequestIsCompleted: oc.output(loginStatusOutputSchema), // Password reset - forgotPassword: oc.input(forgotPasswordInputSchema).output(z.void()), - resetPassword: oc.input(resetPasswordInputSchema).output(z.void()), + forgotPassword: oc + .input(forgotPasswordInputSchema) + .output(successResponseSchema), + resetPassword: oc + .input(resetPasswordInputSchema) + .output(successResponseSchema), // Logout - logout: oc.output(z.void()), + logout: oc.output(successResponseSchema), // WebAuthn procedures webauthn: oc.router({ @@ -103,45 +113,53 @@ export const contract = oc.router({ response: z.custom(), }), ) - .output(z.void()), + .output(successResponseSchema), }), }), me: oc.router({ // Profile get: oc.output(userProfileSchema), - setupProfile: oc.input(setupProfileInputSchema).output(z.void()), - updateProfile: oc.input(updateProfileInputSchema).output(z.void()), - delete: oc.input(z.object({ password: z.string() })).output(z.void()), + setupProfile: oc + .input(setupProfileInputSchema) + .output(successResponseSchema), + updateProfile: oc + .input(updateProfileInputSchema) + .output(successResponseSchema), + delete: oc + .input(z.object({ password: z.string() })) + .output(successResponseSchema), // Auth status (for CLI and debugging) authStatus: oc.output(authStatusOutputSchema), // Authentication settings - setPassword: oc.input(setPasswordInputSchema).output(z.void()), + setPassword: oc.input(setPasswordInputSchema).output(successResponseSchema), // Passkeys passkeys: oc.router({ list: oc.output(z.array(passkeyOutputSchema)), rename: oc .input(z.object({ passkeyId: z.number(), name: z.string() })) - .output(z.void()), - delete: oc.input(z.object({ passkeyId: z.number() })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ passkeyId: z.number() })) + .output(successResponseSchema), }), // Sessions & devices listSessions: oc.output(z.array(sessionOutputSchema)), revokeSession: oc .input(z.object({ sessionId: z.number() })) - .output(z.void()), - revokeAllSessions: oc.output(z.void()), + .output(successResponseSchema), + revokeAllSessions: oc.output(successResponseSchema), getDeviceInfo: oc.output(deviceOutputSchema), - trustDevice: oc.input(trustDeviceInputSchema).output(z.void()), + trustDevice: oc.input(trustDeviceInputSchema).output(successResponseSchema), listTrustedDevices: oc.output(z.array(deviceOutputSchema)), untrustDevice: oc .input(z.object({ deviceId: z.number() })) - .output(z.void()), - revokeAllTrustedDevices: oc.output(z.void()), + .output(successResponseSchema), + revokeAllTrustedDevices: oc.output(successResponseSchema), }), orgs: oc.router({ @@ -159,19 +177,25 @@ export const contract = oc.router({ logoUrl: z.string().optional(), }), ) - .output(z.void()), - delete: oc.input(z.object({ slug: slugSchema })).output(z.void()), - leave: oc.input(z.object({ slug: slugSchema })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), + leave: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), // Members members: oc.router({ list: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgMemberOutputSchema)), - updateRole: oc.input(updateMemberRoleInputSchema).output(z.void()), + updateRole: oc + .input(updateMemberRoleInputSchema) + .output(successResponseSchema), remove: oc .input(z.object({ slug: slugSchema, userId: z.number() })) - .output(z.void()), + .output(successResponseSchema), }), // Invites @@ -179,11 +203,13 @@ export const contract = oc.router({ list: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgInviteOutputSchema)), - create: oc.input(createInviteInputSchema).output(z.void()), + create: oc.input(createInviteInputSchema).output(successResponseSchema), cancel: oc .input(z.object({ slug: slugSchema, inviteId: z.number() })) - .output(z.void()), - accept: oc.input(z.object({ token: z.string() })).output(z.void()), + .output(successResponseSchema), + accept: oc + .input(z.object({ token: z.string() })) + .output(successResponseSchema), }), // Sites @@ -210,31 +236,39 @@ export const contract = oc.router({ logoUrl: z.string().optional(), }), ) - .output(z.void()), - delete: oc.input(z.object({ slug: slugSchema })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), listSites: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgSiteOutputSchema)), - addSite: oc.input(adminAddSiteInputSchema).output(z.void()), + addSite: oc.input(adminAddSiteInputSchema).output(successResponseSchema), removeSite: oc .input(z.object({ slug: slugSchema, domain: z.string() })) - .output(z.void()), + .output(successResponseSchema), }), // Admin user management users: oc.router({ list: oc.output(z.array(userProfileSchema)), get: oc.input(z.object({ email: emailSchema })).output(userProfileSchema), - create: oc.input(adminCreateUserInputSchema).output(z.void()), - update: oc.input(adminUpdateUserInputSchema).output(z.void()), - confirmEmail: oc.input(z.object({ email: emailSchema })).output(z.void()), + create: oc + .input(adminCreateUserInputSchema) + .output(successResponseSchema), + update: oc + .input(adminUpdateUserInputSchema) + .output(successResponseSchema), + confirmEmail: oc + .input(z.object({ email: emailSchema })) + .output(successResponseSchema), }), // Admin auth management auth: oc.router({ completeLogin: oc .input(z.object({ email: emailSchema })) - .output(z.void()), + .output(successResponseSchema), }), }), }); diff --git a/packages/api-contract/src/schemas/common.ts b/packages/api-contract/src/schemas/common.ts index a4cd986..782c97c 100644 --- a/packages/api-contract/src/schemas/common.ts +++ b/packages/api-contract/src/schemas/common.ts @@ -58,3 +58,9 @@ export const phoneSchema = z .refine((val) => !val || isValidPhoneNumber(val), { message: "Invalid phone number", }); + +/** + * Success response schema for operations that don't return data + * Use instead of void to make responses more explicit + */ +export const successResponseSchema = z.object({ success: z.literal(true) }); From 41af130e85867d0ff830fdb8115da44a8ba602b9 Mon Sep 17 00:00:00 2001 From: RevIQ Date: Sat, 10 Jan 2026 16:35:44 +0800 Subject: [PATCH 6/6] Remove /settings page and redirect / to first org dashboard - Delete unused /settings route - Update root page to redirect authenticated users to their first org - Falls back to /dashboard if user has no orgs Co-Authored-By: Claude Opus 4.5 --- .../src/routes/+page.svelte | 44 +++++++++++++------ .../src/routes/account/+page.svelte | 2 +- .../src/routes/settings/+page.svelte | 14 ------ 3 files changed, 32 insertions(+), 28 deletions(-) delete mode 100644 apps/publisher-dashboard/src/routes/settings/+page.svelte diff --git a/apps/publisher-dashboard/src/routes/+page.svelte b/apps/publisher-dashboard/src/routes/+page.svelte index 06c4862..d33f9bc 100644 --- a/apps/publisher-dashboard/src/routes/+page.svelte +++ b/apps/publisher-dashboard/src/routes/+page.svelte @@ -1,20 +1,38 @@ - Publisher Dashboard + Publisher Dashboard -
-

Publisher Dashboard

-

Welcome to the Publisher Dashboard

- - +
+
diff --git a/apps/publisher-dashboard/src/routes/account/+page.svelte b/apps/publisher-dashboard/src/routes/account/+page.svelte index a4e5c89..bd46a13 100644 --- a/apps/publisher-dashboard/src/routes/account/+page.svelte +++ b/apps/publisher-dashboard/src/routes/account/+page.svelte @@ -16,8 +16,8 @@ import { } from "$lib/components/ui/card"; import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; -import { PhoneNumberInput } from "$lib/components/ui/phone-number-input"; import { LoadingButton } from "$lib/components/ui/loading-button"; +import { PhoneNumberInput } from "$lib/components/ui/phone-number-input"; import { Separator } from "$lib/components/ui/separator"; import { cn } from "$lib/utils"; import { validatePhone } from "$lib/utils/validation"; diff --git a/apps/publisher-dashboard/src/routes/settings/+page.svelte b/apps/publisher-dashboard/src/routes/settings/+page.svelte deleted file mode 100644 index 20d3d2a..0000000 --- a/apps/publisher-dashboard/src/routes/settings/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - Settings - Publisher Dashboard - - -
-

Settings

-

Configure your publisher settings here.

- - -