@henosia/app-next 1.0.2 → 1.0.4

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 (30) hide show
  1. package/.agents/skills/henosia-app-next/SKILL.md +228 -0
  2. package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/loading.tsx +10 -0
  3. package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/page.tsx +15 -0
  4. package/.agents/skills/henosia-app-next/assets/template/app/api/auth/[...all]/route.ts +9 -0
  5. package/.agents/skills/henosia-app-next/assets/template/app/api/henosia-platform/[...all]/route.ts +2 -0
  6. package/.agents/skills/henosia-app-next/assets/template/app/layout.tsx +35 -0
  7. package/.agents/skills/henosia-app-next/assets/template/components/app-switcher.tsx +148 -0
  8. package/.agents/skills/henosia-app-next/assets/template/components/nav-user.tsx +143 -0
  9. package/.agents/skills/henosia-app-next/assets/template/components/query-provider.tsx +23 -0
  10. package/.agents/skills/henosia-app-next/assets/template/components/sign-in.tsx +85 -0
  11. package/.agents/skills/henosia-app-next/assets/template/lib/auth-client.ts +3 -0
  12. package/.agents/skills/henosia-app-next/assets/template/lib/supabase/client.ts +2 -0
  13. package/.agents/skills/henosia-app-next/assets/template/lib/supabase/server.ts +2 -0
  14. package/.agents/skills/henosia-app-next/assets/template/middleware.ts +29 -0
  15. package/.agents/skills/henosia-app-next/assets/template/next.config.mjs +65 -0
  16. package/.agents/skills/henosia-app-next/assets/template/package.json +15 -0
  17. package/.agents/skills/henosia-app-next/assets/template/tsconfig.json +26 -0
  18. package/.agents/skills/henosia-auth-guards/SKILL.md +121 -0
  19. package/README.md +26 -13
  20. package/dist/api/platform.mjs +3 -2
  21. package/dist/auth/middleware.mjs +8 -4
  22. package/dist/auth/server-guards.d.mts +116 -0
  23. package/dist/auth/server-guards.mjs +167 -0
  24. package/dist/auth/server.d.mts +1 -1
  25. package/dist/index.d.mts +3 -2
  26. package/dist/index.mjs +2 -1
  27. package/dist/platform/app-switcher.d.mts +1 -1
  28. package/dist/shared-BWt7Sysv.d.mts +19 -0
  29. package/dist/shared.d.mts +1 -17
  30. package/package.json +3 -1
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: henosia-app-next
3
+ description: |
4
+ Integrate the `@henosia/app-next` package into a Next.js app: install,
5
+ configure middleware, mount the better-auth and Henosia Platform catch-all
6
+ routes, wire up the sign-in page, and (optionally) add the app switcher and
7
+ Supabase helpers. Load this skill when adding `@henosia/app-next` to a
8
+ project, scaffolding a new Henosia-Auth-backed Next.js app, or troubleshooting
9
+ the integration scaffolding (middleware, sign-in route, providers,
10
+ re-exports). For per-page / per-route authorization once integration is in
11
+ place, use the `henosia-auth-guards` skill instead.
12
+ enabled: false # flip to true to enable during Henosia Auth integration in projects with legacy auth
13
+ ---
14
+
15
+ # Integrating `@henosia/app-next`
16
+
17
+ `@henosia/app-next` is the Henosia integration for Next.js apps. It provides:
18
+
19
+ - Henosia Auth middleware (pre-check + token refresh + optional Supabase session exchange).
20
+ - better-auth and Henosia Platform catch-all Route Handlers.
21
+ - A React `authClient` (better-auth) and a `useAppSwitcher` hook.
22
+ - Optional Supabase server/browser client factories.
23
+ - Server-side auth guards — see the **`henosia-auth-guards` skill** for those
24
+ (this skill covers the one-time integration; that skill covers the per-route
25
+ authorization work an agent does on every protected page/route/server action).
26
+
27
+ ## Authoritative reference
28
+
29
+ For complete and up-to-date package usage details (peer dependencies, subpath
30
+ exports, Supabase exchange behavior, app-switcher hook options), read:
31
+
32
+ - `node_modules/@henosia/app-next/README.md`
33
+
34
+ The README is the source of truth. This skill summarizes the integration
35
+ workflow and explains how to use the bundled reference template.
36
+
37
+ ## Environment variables — do not edit
38
+
39
+ `@henosia/app-next` requires several env vars (`BETTER_AUTH_URL`,
40
+ `BETTER_AUTH_SECRET`, `HENOSIA_AUTH_*`, optional `NEXT_PUBLIC_SUPABASE_*`). In
41
+ the consuming app, **these are provided automatically by the Henosia builder
42
+ environment** which is where this skill is invoked from.
43
+
44
+ - ❌ Do **not** add Henosia env vars to `.env.local`, deployment
45
+ config, or CI scripts.
46
+ - ❌ Do **not** read or echo their values back to the user.
47
+ - ✅ Treat the env as a managed contract. If a value is missing at runtime, the
48
+ package surfaces a `[Henosia Auth] Missing required env var: …` error — that
49
+ is a Henosia environment issue, not a code change.
50
+ - See the README "Required environment variables" section for the full list and
51
+ what each one is for, but you should not need to set any of them yourself.
52
+
53
+ ## Reference template (use this as the starting point)
54
+
55
+ The package ships a working reference template at:
56
+
57
+ ```
58
+ node_modules/@henosia/app-next/.agents/skills/henosia-app-next/assets/template/
59
+ ```
60
+
61
+ Use it as the source for all integration files. **Always copy or merge from the
62
+ template — do not re-derive these files from scratch.**
63
+
64
+ ### File-to-target-path map
65
+
66
+ | Template file | Target in consuming app | Required? |
67
+ |----------------------------------------------|----------------------------------------------|--------------------------------------------|
68
+ | `middleware.ts` | `middleware.ts` (app root) | Required |
69
+ | `app/api/auth/[...all]/route.ts` | Same path | Required |
70
+ | `app/api/henosia-platform/[...all]/route.ts` | Same path | Required |
71
+ | `lib/auth-client.ts` | `lib/auth-client.ts` (or app's equivalent) | Required |
72
+ | `app/(auth)/sign-in/page.tsx` | A route that resolves to `/sign-in` | Required |
73
+ | `app/(auth)/sign-in/loading.tsx` | Same folder as the sign-in page | Recommended |
74
+ | `components/sign-in.tsx` | `components/sign-in.tsx` | Required (used by the sign-in page) |
75
+ | `app/layout.tsx` | Merge into existing root layout | Required (partial merge) |
76
+ | `components/query-provider.tsx` | `components/query-provider.tsx` | Required if using the app switcher |
77
+ | `components/app-switcher.tsx` | `components/app-switcher.tsx` | Optional (only if surfacing the switcher) |
78
+ | `components/nav-user.tsx` | `components/nav-user.tsx` | Optional (only if surfacing user identity) |
79
+ | `lib/supabase/server.ts` | `lib/supabase/server.ts` | Optional (only if using Supabase) |
80
+ | `lib/supabase/client.ts` | `lib/supabase/client.ts` | Optional (only if using Supabase) |
81
+ | `package.json` | Additive merge into existing `package.json` | Required (additive) |
82
+ | `tsconfig.json` | Additive merge into existing `tsconfig.json` | Required (additive) |
83
+ | `next.config.mjs` | Additive merge into existing `next.config.*` | Required (additive) |
84
+
85
+ The sign-in page lives under the `(auth)` Next.js route group in the template.
86
+ The route group itself is an organization choice; what matters is that the
87
+ URL path resolves to `/sign-in` (the value of `HENOSIA_AUTH_SIGN_IN_PATH_NAME`).
88
+
89
+ ### Template conventions — follow exactly
90
+
91
+ When copying a template file into the consuming app, apply these rules:
92
+
93
+ 1. **Strip `//@ts-nocheck (remove when using the template)`** from the first
94
+ line of every copied file. It exists only so the template files type-check
95
+ in isolation here.
96
+ 2. **Strip `// Template use guidance: …`** lines. They are instructions for the
97
+ integrating agent (you), not for the consuming app.
98
+ 3. **Preserve verbatim** every comment from `// To Henosia assistant regarding
99
+ this file:` to end-of-file. These currently appear in `middleware.ts` and
100
+ `app/api/auth/[...all]/route.ts`. They protect the Henosia Auth flows from
101
+ well-intentioned future edits and **must remain in the consuming app's
102
+ files**, byte-for-byte.
103
+ 4. **`package.json` is an additive merge.** The template uses
104
+ `"__add_these__:"` and `"__others_omitted_for_brevity__"` sentinel keys to
105
+ mark merge boundaries. Add the listed deps to the consuming app's existing
106
+ `dependencies` (preserve everything already there). Use the template's
107
+ pinned versions for `@henosia/app-next`, `@tanstack/react-query`, and
108
+ `better-auth` unless the consuming app already has a newer compatible
109
+ version. Drop the sentinel keys when merging.
110
+ 5. **`tsconfig.json` is an additive merge.** Ensure the consuming app has the
111
+ `@/*` path alias under `compilerOptions.paths` and the `next` plugin under
112
+ `compilerOptions.plugins`; the template's other settings are typical Next.js
113
+ defaults — only fold them in if missing.
114
+ 6. **`app/layout.tsx` is a partial merge.** Preserve the host app's existing
115
+ layout structure (fonts, metadata, theming, providers, slots, etc.). Fold
116
+ in only:
117
+ - The `usePathname()` import + `path !== HENOSIA_AUTH_SIGN_IN_PATH_NAME`
118
+ branching, so the sign-in page renders without the host app's chrome.
119
+ - The `<QueryProvider>` wrap (required for the app switcher and any other
120
+ `@tanstack/react-query` consumer).
121
+ The template comment `// Preserve the existing layout elements as it stands
122
+ today when using this template` is the directive — follow it.
123
+ 7. **`components/sign-in.tsx`, `app-switcher.tsx`, `nav-user.tsx` are
124
+ reference UIs.** Adapt the visuals to the host app's design system, but
125
+ **preserve the Henosia-specific behavior**:
126
+ - `sign-in.tsx`: keep `signIn.social({ provider: "henosia", callbackURL: "/" })`,
127
+ the `data-henosia-auth-sign-in` attribute on the trigger, and the
128
+ `loading` UX around the call.
129
+ - `app-switcher.tsx`: keep the `useAppSwitcher({ enabled: open })` hook
130
+ usage, the `groupedApps` rendering, and the `error` / `isLoading` paths.
131
+ - `nav-user.tsx`: keep `authClient.useSession()`, and on sign-out keep the
132
+ `router.push(HENOSIA_AUTH_SIGN_IN_PATH_NAME)` + `router.refresh()`
133
+ sequence inside `authClient.signOut`'s `onSuccess`.
134
+ 8. **The thin re-export files** (`lib/auth-client.ts`, `lib/supabase/server.ts`,
135
+ `lib/supabase/client.ts`) exist so the rest of the consuming app imports
136
+ from `@/lib/...` and the `@henosia/app-next` subpaths are referenced from
137
+ exactly one place. Keep them as one-line re-exports.
138
+
139
+ ## Integration checklist
140
+
141
+ Work through these in order. Most steps map directly to a template file or a
142
+ README section.
143
+
144
+ 1. **Install the package and peer dependencies.** See README "Installation".
145
+ Use `pnpm`/`npm`/`yarn` per the host app's convention. Install
146
+ `@supabase/ssr` and `@supabase/supabase-js` only if the app uses Supabase.
147
+ 2. **Do not configure env vars.** They are provided by the Henosia builder
148
+ environment (see "Environment variables — do not edit" above).
149
+ 3. **Add `middleware.ts`** at the app root. Copy from the template, applying
150
+ the template-conventions rules. Keep the `// To Henosia assistant…` block.
151
+ 4. **Mount `app/api/auth/[...all]/route.ts`** by re-exporting `GET, POST` from
152
+ `@henosia/app-next/api/auth`. Copy from the template; keep the `// To
153
+ Henosia assistant…` block.
154
+ 5. **Mount `app/api/henosia-platform/[...all]/route.ts`** by re-exporting `GET`
155
+ from `@henosia/app-next/api/platform`. Copy from the template.
156
+ 6. **Add the React auth client re-export** at `lib/auth-client.ts`.
157
+ 7. **Create the sign-in route at `/sign-in`.** Copy `app/(auth)/sign-in/page.tsx`,
158
+ `app/(auth)/sign-in/loading.tsx`, and `components/sign-in.tsx`. The
159
+ route group is optional; the resolved URL must be `/sign-in`.
160
+ 8. **Merge `app/layout.tsx`** per the partial-merge rule. Wrap the tree with
161
+ `<QueryProvider>` and skip the host app's chrome on
162
+ `HENOSIA_AUTH_SIGN_IN_PATH_NAME`.
163
+ 9. **Add `<QueryProvider>`** (`components/query-provider.tsx`) if you don't
164
+ already have a `QueryClientProvider` higher in the tree. Required for
165
+ `useAppSwitcher` and any other `@tanstack/react-query` consumer.
166
+ 10. **(Optional) Add the app switcher.** Copy `components/app-switcher.tsx`
167
+ where you want it in your shell (e.g. inside a `<Sidebar>`). It depends on
168
+ shadcn/ui primitives — install the ones it imports if missing.
169
+ 11. **(Optional) Add the user nav / sign-out menu.** Copy
170
+ `components/nav-user.tsx`. Adapt visuals; keep the Henosia behavior.
171
+ 12. **(Optional) Add Supabase helpers.** When `NEXT_PUBLIC_SUPABASE_URL` is
172
+ set in the Henosia builder env, install the Supabase peer deps and copy
173
+ `lib/supabase/server.ts` and `lib/supabase/client.ts` from the template.
174
+ The middleware automatically exchanges the Henosia Auth token for a
175
+ Supabase session — no extra wiring required. See README "5. (Optional)
176
+ Supabase helpers".
177
+ 13. **Protect every non-public page, layout, server action, and route
178
+ handler.** Use the **`henosia-auth-guards` skill**. The middleware is a
179
+ pre-check only — it does **not** satisfy the per-route authorization
180
+ requirement.
181
+ 14. Carefully remove the old auth flow components and routes that Henosia Auth replaces
182
+
183
+ ## Subpath imports cheat sheet
184
+
185
+ ```ts
186
+ // Server-side helpers (advanced; prefer the auth guards)
187
+ import { auth, verifyHenosiaAuthToken, isUnauthorizedException, isPageRequest, HENOSIA_AUTH_SIGN_IN_PATH_NAME } from '@henosia/app-next'
188
+
189
+ // Shared constants & types (no runtime deps; safe to import from server or client)
190
+ import { HENOSIA_AUTH_SIGN_IN_PATH_NAME, HENOSIA_AUTH_GET_SESSION_PATH_NAME, HENOSIA_ORGANIZATION_CTX_COOKIE } from '@henosia/app-next/shared'
191
+ import type { HenosiaOrganizationContext } from '@henosia/app-next/shared'
192
+
193
+ // Middleware
194
+ import { createHenosiaAuthMiddleware } from '@henosia/app-next/auth/middleware'
195
+
196
+ // Server-side auth guards (see henosia-auth-guards skill)
197
+ import { requireHenosiaAuth, getHenosiaAuth, routeWithHenosiaAuth } from '@henosia/app-next/auth/server-guards'
198
+
199
+ // React auth client
200
+ import { authClient } from '@henosia/app-next/auth/client'
201
+
202
+ // Catch-all route re-exports
203
+ export { GET, POST } from '@henosia/app-next/api/auth'
204
+ export { GET } from '@henosia/app-next/api/platform'
205
+
206
+ // App-switcher hook
207
+ import { useAppSwitcher } from '@henosia/app-next/platform/app-switcher'
208
+
209
+ // Supabase helpers (peer deps required)
210
+ import { createClient } from '@henosia/app-next/supabase/server'
211
+ import { createClient } from '@henosia/app-next/supabase/client'
212
+ ```
213
+
214
+ ## Lower-level helpers — usually not needed
215
+
216
+ `auth`, `verifyHenosiaAuthToken`, `isUnauthorizedException`, `isPageRequest` are
217
+ exported from the package root for advanced/edge cases. For protecting pages
218
+ and routes, **always prefer the guards** documented in the
219
+ `henosia-auth-guards` skill. They encode the canonical redirect / 401 / 500
220
+ contract, apply the correct security headers, and integrate with React's
221
+ `cache()` for per-render deduplication.
222
+
223
+ ## See also
224
+
225
+ - `henosia-auth-guards` skill — the per-page/per-route authorization workflow.
226
+ You will need this on virtually every Henosia-Auth-backed Next.js project.
227
+ - `@henosia/app-next` README at `node_modules/@henosia/app-next/README.md`.
228
+ - Reference template at `node_modules/@henosia/app-next/.agents/skills/henosia-app-next/assets/template/`.
@@ -0,0 +1,10 @@
1
+ export default function SignInLoading() {
2
+ return (
3
+ <div className="flex min-h-screen items-center justify-center">
4
+ <div className="text-center">
5
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-gray-900 mx-auto"></div>
6
+ <p className="mt-4 text-gray-600">Loading...</p>
7
+ </div>
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import {SignIn} from "@/components/sign-in";
3
+
4
+ export default function Page() {
5
+
6
+ return (
7
+ <div className="w-full">
8
+ <div className="flex items-center flex-col justify-center w-full md:py-10">
9
+ <div className="md:w-[400px]">
10
+ <SignIn />
11
+ </div>
12
+ </div>
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,9 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ // Template use guidance: Preserve comments verbatim from the line `// To Henosia assistant regarding this file:` and below
3
+ export { GET, POST } from '@henosia/app-next/api/auth'
4
+
5
+ // To Henosia assistant regarding this file:
6
+ // Henosia Auth is a managed solution that is outside your scope of work.
7
+ // Henosia Auth flows must be preserved at all times.
8
+ // All non-public pages and routes must be protected using `requireHenosiaAuth` or `routeWithHenosiaAuth` from `@henosia/app-next`.
9
+ // Any potential issues with Henosia Auth flows must be referred to Henosia technical support: https://docs.henosia.com/support
@@ -0,0 +1,2 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ export { GET } from '@henosia/app-next/api/platform'
@@ -0,0 +1,35 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ // this partial layout file illustrates how to modify the layout based on whether the path is the sign in page
3
+ // it should be merged with the existing layout's structure and dependencies
4
+ "use client";
5
+ import type * as React from "react";
6
+ import { usePathname } from "next/navigation";
7
+ import {HENOSIA_AUTH_SIGN_IN_PATH_NAME} from "@henosia/app-next/shared";
8
+ import { QueryProvider } from "@/components/query-provider";
9
+
10
+ // ... other required imports and setup, e.g. fonts
11
+
12
+ export default function RootLayout({
13
+ children
14
+ }: Readonly<{
15
+ children: React.ReactNode;
16
+ breadcrumb: React.ReactNode;
17
+ }>) {
18
+ const path = usePathname();
19
+ return (
20
+ <html lang="en" suppressHydrationWarning>
21
+ <body>
22
+ <QueryProvider>
23
+ {path !== HENOSIA_AUTH_SIGN_IN_PATH_NAME ? (
24
+ <>
25
+ {/* Preserve the existing layout elements as it stands today when using this template */}
26
+ <main>{children}</main>
27
+ </>
28
+ ) : (
29
+ children
30
+ )}
31
+ </QueryProvider>
32
+ </body>
33
+ </html>
34
+ );
35
+ }
@@ -0,0 +1,148 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client"
3
+
4
+ import * as React from "react"
5
+ import {useState} from "react";
6
+ import {ChevronsUpDown, GalleryVerticalEnd, Plus, PanelTop} from "lucide-react"
7
+
8
+ import {
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ } from "@/components/ui/popover"
13
+ import {
14
+ Command,
15
+ CommandEmpty,
16
+ CommandGroup,
17
+ CommandInput,
18
+ CommandItem,
19
+ CommandList,
20
+ CommandSeparator,
21
+ } from "@/components/ui/command"
22
+ import {
23
+ SidebarMenu,
24
+ SidebarMenuButton,
25
+ SidebarMenuItem,
26
+ useSidebar,
27
+ } from "@/components/ui/sidebar"
28
+ import {Skeleton} from "@/components/ui/skeleton";
29
+ import {AppSwitcherApp, useAppSwitcher} from "@henosia/app-next/platform/app-switcher";
30
+
31
+ export function AppSwitcher() {
32
+ const { isMobile } = useSidebar();
33
+ const [open, setOpen] = useState(false);
34
+
35
+ const {organization, groupedApps, error, isLoading} = useAppSwitcher({
36
+ enabled: open
37
+ });
38
+
39
+ const handleSelectApp = (app: AppSwitcherApp) => {
40
+ window.open(app.url);
41
+ setOpen(false);
42
+ };
43
+
44
+ return (
45
+ <SidebarMenu>
46
+ <SidebarMenuItem>
47
+ <Popover open={open} onOpenChange={setOpen}>
48
+ <PopoverTrigger asChild>
49
+ <SidebarMenuButton
50
+ size="lg"
51
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
52
+ >
53
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg text-sidebar-primary-foreground">
54
+ {organization ? (
55
+ <>
56
+ {organization.logoUrl ? (
57
+ <>
58
+ {/* eslint-disable-next-line @next/next/no-img-element */}
59
+ <img alt={`Logo for ${organization.name}`} src={organization.logoUrl} className={"size-6"} />
60
+ </>
61
+ ) : (
62
+ <GalleryVerticalEnd className={"size-4 text-primary"} />
63
+ )}
64
+ </>
65
+ ) : (
66
+ <Skeleton className={"size-6 opacity-40 rounded-lg"} />
67
+ )}
68
+ </div>
69
+ <div className="grid flex-1 text-left text-sm leading-tight">
70
+ <span className="truncate font-semibold">
71
+ {organization?.name ?? <Skeleton className={"w-16 h-[1em] opacity-40"} />}
72
+ </span>
73
+ </div>
74
+ <ChevronsUpDown className="ml-auto" />
75
+ </SidebarMenuButton>
76
+ </PopoverTrigger>
77
+ <PopoverContent
78
+ className="w-[22rem] rounded-lg p-0"
79
+ align="start"
80
+ side={isMobile ? "bottom" : "right"}
81
+ sideOffset={4}
82
+ >
83
+ <Command>
84
+ <CommandInput placeholder="Search apps…" />
85
+ <CommandList className="max-h-[80vh]">
86
+ {isLoading ? (
87
+ <div className="flex flex-col gap-1 p-2" role="status" aria-label="Loading apps">
88
+ {Array.from({ length: 3 }).map((_, i) => (
89
+ <React.Fragment key={i}>
90
+ {i === 0 && (<div className={"p-2"}><Skeleton className="h-2 w-24" /></div>)}
91
+ <div key={i} className="flex items-center gap-2 p-2">
92
+ <Skeleton className="size-6 rounded-sm" />
93
+ <Skeleton className="h-3 flex-1" />
94
+ </div>
95
+ </React.Fragment>
96
+ ))}
97
+ </div>
98
+ ) : error ? (
99
+ <div className="p-4 text-sm text-destructive">
100
+ {error.message ?? 'Failed to load apps.'}
101
+ </div>
102
+ ) : (
103
+ <>
104
+ <CommandEmpty>No apps found.</CommandEmpty>
105
+ {groupedApps.map(({ group, apps }) => (
106
+ <CommandGroup key={group} heading={group}>
107
+ {apps.map((app) => (
108
+ <CommandItem
109
+ key={app.id}
110
+ value={`${group} ${app.name}`}
111
+ onSelect={() => handleSelectApp(app)}
112
+ className="gap-2 p-2"
113
+ title={`${app.name} – ${app.url}`}
114
+ >
115
+ <div className="flex size-6 items-center justify-center rounded-sm border bg-background">
116
+ <PanelTop className="size-4 shrink-0" />
117
+ </div>
118
+ <span className="truncate">{app.name}</span>
119
+ </CommandItem>
120
+ ))}
121
+ </CommandGroup>
122
+ ))}
123
+ </>
124
+ )}
125
+ <CommandSeparator />
126
+ <CommandGroup>
127
+ <CommandItem
128
+ disabled={isLoading}
129
+ onSelect={() => {
130
+ window.open('https://app.henosia.com/app');
131
+ setOpen(false)
132
+ }}
133
+ className="gap-2 p-2"
134
+ >
135
+ <div className="flex size-6 items-center justify-center rounded-md border bg-background">
136
+ <Plus className="size-4" />
137
+ </div>
138
+ <div className="font-medium text-muted-foreground">Build new app</div>
139
+ </CommandItem>
140
+ </CommandGroup>
141
+ </CommandList>
142
+ </Command>
143
+ </PopoverContent>
144
+ </Popover>
145
+ </SidebarMenuItem>
146
+ </SidebarMenu>
147
+ )
148
+ }
@@ -0,0 +1,143 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client"
3
+
4
+ import {
5
+ BadgeCheck,
6
+ ChevronsUpDown,
7
+ LogOut,
8
+ } from "lucide-react"
9
+
10
+ import {
11
+ Avatar,
12
+ AvatarFallback,
13
+ AvatarImage,
14
+ } from "@/components/ui/avatar"
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuGroup,
19
+ DropdownMenuItem,
20
+ DropdownMenuLabel,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuTrigger,
23
+ } from "@/components/ui/dropdown-menu"
24
+ import {
25
+ SidebarMenu,
26
+ SidebarMenuButton,
27
+ SidebarMenuItem,
28
+ useSidebar,
29
+ } from "@/components/ui/sidebar"
30
+ import { Skeleton } from "@/components/ui/skeleton"
31
+ import Link from "next/link"
32
+ import { useRouter } from "next/navigation"
33
+ import { authClient } from "@/lib/auth-client";
34
+ import { HENOSIA_AUTH_SIGN_IN_PATH_NAME } from "@henosia/app-next/shared"
35
+
36
+ function getInitials(name?: string | null): string {
37
+ if (!name) return ""
38
+ const parts = name.trim().split(/\s+/).filter(Boolean)
39
+ if (parts.length === 0) return ""
40
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
41
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
42
+ }
43
+
44
+ export function NavUser() {
45
+ const { isMobile } = useSidebar()
46
+ const router = useRouter()
47
+ const { data: session, isPending } = authClient.useSession()
48
+
49
+ if (isPending) {
50
+ return (
51
+ <SidebarMenu>
52
+ <SidebarMenuItem>
53
+ <SidebarMenuButton size="lg" disabled>
54
+ <Skeleton className="h-8 w-8 rounded-lg" />
55
+ <div className="grid flex-1 gap-1 text-left text-sm leading-tight">
56
+ <Skeleton className="h-3 w-24" />
57
+ <Skeleton className="h-3 w-32" />
58
+ </div>
59
+ </SidebarMenuButton>
60
+ </SidebarMenuItem>
61
+ </SidebarMenu>
62
+ )
63
+ }
64
+
65
+ const sessionUser = session?.user
66
+ if (!sessionUser) {
67
+ return null
68
+ }
69
+
70
+ const name = sessionUser.name ?? ""
71
+ const email = sessionUser.email ?? ""
72
+ const avatar = sessionUser.image ?? undefined
73
+ const initials = getInitials(name || email)
74
+
75
+ const handleSignOut = async () => {
76
+ await authClient.signOut({
77
+ fetchOptions: {
78
+ onSuccess: () => {
79
+ router.push(HENOSIA_AUTH_SIGN_IN_PATH_NAME)
80
+ router.refresh()
81
+ },
82
+ },
83
+ })
84
+ }
85
+
86
+ return (
87
+ <SidebarMenu>
88
+ <SidebarMenuItem>
89
+ <DropdownMenu>
90
+ <DropdownMenuTrigger asChild>
91
+ <SidebarMenuButton
92
+ size="lg"
93
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
94
+ >
95
+ <Avatar className="h-8 w-8 rounded-lg">
96
+ {avatar ? <AvatarImage src={avatar} alt={name} /> : null}
97
+ <AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
98
+ </Avatar>
99
+ <div className="grid flex-1 text-left text-sm leading-tight">
100
+ <span className="truncate font-semibold">{name}</span>
101
+ <span className="truncate text-xs">{email}</span>
102
+ </div>
103
+ <ChevronsUpDown className="ml-auto size-4" />
104
+ </SidebarMenuButton>
105
+ </DropdownMenuTrigger>
106
+ <DropdownMenuContent
107
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
108
+ side={isMobile ? "bottom" : "right"}
109
+ align="end"
110
+ sideOffset={4}
111
+ >
112
+ <DropdownMenuLabel className="p-0 font-normal">
113
+ <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
114
+ <Avatar className="h-8 w-8 rounded-lg">
115
+ {avatar ? <AvatarImage src={avatar} alt={name} /> : null}
116
+ <AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
117
+ </Avatar>
118
+ <div className="grid flex-1 text-left text-sm leading-tight">
119
+ <span className="truncate font-semibold">{name}</span>
120
+ <span className="truncate text-xs">{email}</span>
121
+ </div>
122
+ </div>
123
+ </DropdownMenuLabel>
124
+ <DropdownMenuSeparator />
125
+ <DropdownMenuGroup>
126
+ <DropdownMenuItem asChild={true}>
127
+ <Link target={'_blank'} href={"https://app.henosia.com/settings/profile"}>
128
+ <BadgeCheck />
129
+ Profile
130
+ </Link>
131
+ </DropdownMenuItem>
132
+ </DropdownMenuGroup>
133
+ <DropdownMenuSeparator />
134
+ <DropdownMenuItem onSelect={handleSignOut}>
135
+ <LogOut />
136
+ Log out
137
+ </DropdownMenuItem>
138
+ </DropdownMenuContent>
139
+ </DropdownMenu>
140
+ </SidebarMenuItem>
141
+ </SidebarMenu>
142
+ )
143
+ }
@@ -0,0 +1,23 @@
1
+ //@ts-nocheck (remove when using the template)
2
+ "use client";
3
+
4
+ import * as React from "react";
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
+
7
+ export function QueryProvider({ children }: { children: React.ReactNode }) {
8
+ const [queryClient] = React.useState(
9
+ () =>
10
+ new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ staleTime: 60 * 1000,
14
+ refetchOnWindowFocus: false,
15
+ },
16
+ },
17
+ })
18
+ );
19
+
20
+ return (
21
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
22
+ );
23
+ }