@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.
- package/.agents/skills/henosia-app-next/SKILL.md +228 -0
- package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/loading.tsx +10 -0
- package/.agents/skills/henosia-app-next/assets/template/app/(auth)/sign-in/page.tsx +15 -0
- package/.agents/skills/henosia-app-next/assets/template/app/api/auth/[...all]/route.ts +9 -0
- package/.agents/skills/henosia-app-next/assets/template/app/api/henosia-platform/[...all]/route.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/app/layout.tsx +35 -0
- package/.agents/skills/henosia-app-next/assets/template/components/app-switcher.tsx +148 -0
- package/.agents/skills/henosia-app-next/assets/template/components/nav-user.tsx +143 -0
- package/.agents/skills/henosia-app-next/assets/template/components/query-provider.tsx +23 -0
- package/.agents/skills/henosia-app-next/assets/template/components/sign-in.tsx +85 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/auth-client.ts +3 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/supabase/client.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/lib/supabase/server.ts +2 -0
- package/.agents/skills/henosia-app-next/assets/template/middleware.ts +29 -0
- package/.agents/skills/henosia-app-next/assets/template/next.config.mjs +65 -0
- package/.agents/skills/henosia-app-next/assets/template/package.json +15 -0
- package/.agents/skills/henosia-app-next/assets/template/tsconfig.json +26 -0
- package/.agents/skills/henosia-auth-guards/SKILL.md +121 -0
- package/README.md +26 -13
- package/dist/api/platform.mjs +3 -2
- package/dist/auth/middleware.mjs +8 -4
- package/dist/auth/server-guards.d.mts +116 -0
- package/dist/auth/server-guards.mjs +167 -0
- package/dist/auth/server.d.mts +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/platform/app-switcher.d.mts +1 -1
- package/dist/shared-BWt7Sysv.d.mts +19 -0
- package/dist/shared.d.mts +1 -17
- 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,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
|
+
}
|