Skip to content

Commit 2c5ef5b

Browse files
committed
feat: add admin links management and refactor shared components
- Add complete admin links management section with nested routes - Extract shared components (LinksTable, LinkDetailOverview, LinkDetailEvents, LinkDetailEdit) - Remove "Personal" option from organization selectors - Migrate from Basis to Vite build system - Add navigation entry for admin links section - Simplify page components to use shared components - Update type definitions and auth utilities - Optimize organization-centric architecture BREAKING CHANGE: All links now belong to organizations. Personal organization is automatically managed.
1 parent a5f2fac commit 2c5ef5b

110 files changed

Lines changed: 4414 additions & 3430 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"vite-plus": {
4+
"command": "npx",
5+
"args": ["vp", "mcp"]
6+
}
7+
}
8+
}

CLAUDE.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!--VITE PLUS START-->
2+
3+
# Using Vite+, the Unified Toolchain for the Web
4+
5+
This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`.
6+
7+
## Vite+ Workflow
8+
9+
`vp` is a global binary that handles the full development lifecycle. Run `vp help` to print a list of commands and `vp <command> --help` for information about a specific command.
10+
11+
### Start
12+
13+
- create - Create a new project from a template
14+
- migrate - Migrate an existing project to Vite+
15+
- config - Configure hooks and agent integration
16+
- staged - Run linters on staged files
17+
- install (`i`) - Install dependencies
18+
- env - Manage Node.js versions
19+
20+
### Develop
21+
22+
- dev - Run the development server
23+
- check - Run format, lint, and TypeScript type checks
24+
- lint - Lint code
25+
- fmt - Format code
26+
- test - Run tests
27+
28+
### Execute
29+
30+
- run - Run monorepo tasks
31+
- exec - Execute a command from local `node_modules/.bin`
32+
- dlx - Execute a package binary without installing it as a dependency
33+
- cache - Manage the task cache
34+
35+
### Build
36+
37+
- build - Build for production
38+
- pack - Build libraries
39+
- preview - Preview production build
40+
41+
### Manage Dependencies
42+
43+
Vite+ automatically detects and wraps the underlying package manager such as pnpm, npm, or Yarn through the `packageManager` field in `package.json` or package manager-specific lockfiles.
44+
45+
- add - Add packages to dependencies
46+
- remove (`rm`, `un`, `uninstall`) - Remove packages from dependencies
47+
- update (`up`) - Update packages to latest versions
48+
- dedupe - Deduplicate dependencies
49+
- outdated - Check for outdated packages
50+
- list (`ls`) - List installed packages
51+
- why (`explain`) - Show why a package is installed
52+
- info (`view`, `show`) - View package information from the registry
53+
- link (`ln`) / unlink - Manage local package links
54+
- pm - Forward a command to the package manager
55+
56+
### Maintain
57+
58+
- upgrade - Update `vp` itself to the latest version
59+
60+
These commands map to their corresponding tools. For example, `vp dev --port 3000` runs Vite's dev server and works the same as Vite. `vp test` runs JavaScript tests through the bundled Vitest. The version of all tools can be checked using `vp --version`. This is useful when researching documentation, features, and bugs.
61+
62+
## Common Pitfalls
63+
64+
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations.
65+
- **Always use Vite commands to run tools:** Don't attempt to run `vp vitest` or `vp oxlint`. They do not exist. Use `vp test` and `vp lint` instead.
66+
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a `test` script defined in `scripts` that conflicts with the built-in `vp test` command, run it using `vp run test`.
67+
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
68+
- **Use Vite+ wrappers for one-off binaries:** Use `vp dlx` instead of package-manager-specific `dlx`/`npx` commands.
69+
- **Import JavaScript modules from `vite-plus`:** Instead of importing from `vite` or `vitest`, all modules should be imported from the project's `vite-plus` dependency. For example, `import { defineConfig } from 'vite-plus';` or `import { expect, test, vi } from 'vite-plus/test';`. You must not install `vitest` to import test utilities.
70+
- **Type-Aware Linting:** There is no need to install `oxlint-tsgolint`, `vp lint --type-aware` works out of the box.
71+
72+
## Review Checklist for Agents
73+
74+
- [ ] Run `vp install` after pulling remote changes and before getting started.
75+
- [ ] Run `vp check` and `vp test` to validate changes.
76+
<!--VITE PLUS END-->

app/components/OrgsMenu.vue

Lines changed: 52 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
2-
import type { Organization } from "better-auth/plugins";
32
import type { DropdownMenuItem } from "@nuxt/ui";
3+
import type { Organization } from "better-auth/plugins";
4+
45
import { authClient } from "~/utils/auth";
56
67
defineProps<{
@@ -37,69 +38,51 @@ const switching = ref(false);
3738
const currentEntity = computed(() => {
3839
const activeOrg = activeOrgResult.value.data;
3940
if (activeOrg) {
41+
const isPersonal = activeOrg.slug?.startsWith("user_");
4042
return {
4143
label: activeOrg.name,
42-
icon: "i-lucide-building",
43-
type: "organization",
44+
icon: isPersonal ? "i-lucide-user" : "i-lucide-building",
45+
type: isPersonal ? "personal" : "organization",
4446
};
4547
}
4648
return {
47-
label: "Personal",
48-
icon: "i-lucide-user",
49-
type: "personal",
49+
label: "Select Organization",
50+
icon: "i-lucide-users",
51+
type: "none",
5052
};
5153
});
5254
53-
// Switch between personal and organization
55+
// Switch between organizations
5456
async function switchContext(entityId: string) {
5557
if (switching.value) return;
5658
5759
switching.value = true;
5860
try {
59-
if (entityId === "personal") {
60-
// Switch to personal mode
61-
const { error } = await authClient.organization.setActive({
62-
organizationId: null,
63-
});
64-
65-
if (error) {
66-
toast.add({
67-
title: "Switch Failed",
68-
description: error.message || "Failed to switch to personal account",
69-
color: "error",
70-
});
71-
return;
72-
}
73-
74-
toast.add({
75-
title: "Switched to Personal",
76-
description: "You are now using your personal account",
77-
color: "success",
78-
});
79-
} else {
80-
// Switch to organization mode
81-
const { error } = await authClient.organization.setActive({
82-
organizationId: entityId,
83-
});
61+
// Switch to organization mode
62+
const { error } = await authClient.organization.setActive({
63+
organizationId: entityId,
64+
});
8465
85-
if (error) {
86-
toast.add({
87-
title: "Switch Failed",
88-
description: error.message || "Failed to switch to organization",
89-
color: "error",
90-
});
91-
return;
92-
}
93-
94-
const orgList = orgs.value;
95-
const org = orgList?.find((o: Organization) => o.id === entityId);
66+
if (error) {
9667
toast.add({
97-
title: "Switched to Organization",
98-
description: `You are now working in ${org?.name || "the organization"}`,
99-
color: "success",
68+
title: "Switch Failed",
69+
description: error.message || "Failed to switch to organization",
70+
color: "error",
10071
});
72+
return;
10173
}
10274
75+
const orgList = orgs.value;
76+
const org = orgList?.find((o: Organization) => o.id === entityId);
77+
const isPersonal = org?.slug?.startsWith("user_");
78+
toast.add({
79+
title: isPersonal ? "Personal Workspace" : "Switched to Organization",
80+
description: isPersonal
81+
? "You are now in your personal workspace"
82+
: `You are now working in ${org?.name || "the organization"}`,
83+
color: "success",
84+
});
85+
10386
// useActiveOrganization hook will auto-update
10487
// useAsyncData with watch will auto-refetch organizations
10588
} catch (error) {
@@ -115,24 +98,36 @@ async function switchContext(entityId: string) {
11598
11699
// Build dropdown menu items
117100
const items = computed<DropdownMenuItem[][]>(() => {
118-
const personalOption = {
119-
label: "Personal",
120-
icon: "i-lucide-user",
121-
id: "personal",
122-
onSelect: () => switchContext("personal"),
123-
};
124-
125101
const orgList = orgs.value;
126-
const organizationOptions =
127-
orgList?.map((org: Organization) => ({
102+
103+
// Separate personal and team organizations
104+
const personalOrg = orgList?.find((org: Organization) => org.slug?.startsWith("user_"));
105+
const teamOrgs = orgList?.filter((org: Organization) => !org.slug?.startsWith("user_")) || [];
106+
107+
const organizationOptions = [
108+
// Personal organization at top
109+
...(personalOrg
110+
? [
111+
{
112+
label: personalOrg.name,
113+
icon: "i-lucide-user",
114+
id: personalOrg.id,
115+
badge: "Personal",
116+
onSelect: () => switchContext(personalOrg.id),
117+
},
118+
]
119+
: []),
120+
// Team organizations
121+
...teamOrgs.map((org: Organization) => ({
128122
label: org.name,
129123
icon: "i-lucide-building",
130124
id: org.id,
131125
onSelect: () => switchContext(org.id),
132-
})) || [];
126+
})),
127+
];
133128
134129
return [
135-
[personalOption, ...organizationOptions],
130+
organizationOptions,
136131
[
137132
{
138133
label: "Create organization",

app/components/UserMenu.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { DropdownMenuItem } from "@nuxt/ui";
3+
34
import { authClient } from "~/utils/auth";
45
56
defineProps<{

app/components/dashboard/DomainDeleteModal.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
2-
import type { Domain } from "~~/shared/types/domain";
32
import * as z from "zod";
3+
import type { Domain } from "~~/shared/types/domain";
4+
45
import { authClient } from "~/utils/auth";
56
67
const props = defineProps<{

app/components/dashboard/DomainVerifyModal.vue

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { Domain } from "~~/shared/types/domain";
3+
34
import { authClient } from "~/utils/auth";
45
56
const props = defineProps<{
@@ -107,18 +108,18 @@ async function verifyDomain() {
107108
<template #body>
108109
<div class="mt-4 space-y-3">
109110
<div class="flex items-start gap-3">
110-
<span class="text-sm font-medium min-w-16">Type:</span>
111+
<span class="min-w-16 text-sm font-medium">Type:</span>
111112
<UBadge variant="subtle">TXT</UBadge>
112113
</div>
113114
<div class="flex items-start gap-3">
114-
<span class="text-sm font-medium min-w-16">Host/Name:</span>
115-
<span class="text-sm font-mono">@</span>
116-
<span class="text-xs text-muted-foreground">(or {{ domain.domainName }})</span>
115+
<span class="min-w-16 text-sm font-medium">Host/Name:</span>
116+
<span class="font-mono text-sm">@</span>
117+
<span class="text-muted-foreground text-xs">(or {{ domain.domainName }})</span>
117118
</div>
118119
<div class="flex items-start gap-3">
119-
<span class="text-sm font-medium min-w-16">Value:</span>
120-
<div class="flex items-center gap-2 flex-1">
121-
<span class="text-sm font-mono break-all">
120+
<span class="min-w-16 text-sm font-medium">Value:</span>
121+
<div class="flex flex-1 items-center gap-2">
122+
<span class="font-mono text-sm break-all">
122123
{{ domain.verificationToken }}
123124
</span>
124125
<UButton

app/components/dashboard/LinkDateRangePicker.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ const selectRange = (range: { days?: number; months?: number; years?: number })
102102
<template #trailing>
103103
<UIcon
104104
name="i-lucide-chevron-down"
105-
class="shrink-0 text-dimmed size-5 group-data-[state=open]:rotate-180 transition-transform duration-200"
105+
class="text-dimmed size-5 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180"
106106
/>
107107
</template>
108108
</UButton>
109109

110110
<template #content>
111-
<div class="flex items-stretch sm:divide-x divide-default">
112-
<div class="hidden sm:flex flex-col justify-center">
111+
<div class="divide-default flex items-stretch sm:divide-x">
112+
<div class="hidden flex-col justify-center sm:flex">
113113
<UButton
114114
v-for="(range, index) in ranges"
115115
:key="index"

app/components/dashboard/LinkDeleteModal.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
2-
import type { Link } from "~~/shared/types/link";
32
import * as z from "zod";
3+
import type { Link } from "~~/shared/types/link";
4+
45
import { authClient } from "~/utils/auth";
56
67
const props = defineProps<{

0 commit comments

Comments
 (0)