@pylonsync/create-pylon 0.3.346 → 0.3.347

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 (60) hide show
  1. package/package.json +1 -1
  2. package/templates/crm/README.md +5 -4
  3. package/templates/crm/app/companies/companies-view.tsx +6 -3
  4. package/templates/crm/app/companies/page.tsx +3 -8
  5. package/templates/crm/app/contacts/contacts-view.tsx +6 -3
  6. package/templates/crm/app/contacts/page.tsx +3 -8
  7. package/templates/crm/app/deals/[id]/deal-view.tsx +6 -3
  8. package/templates/crm/app/deals/[id]/page.tsx +3 -13
  9. package/templates/crm/app/layout.tsx +4 -4
  10. package/templates/crm/app/page.tsx +2 -19
  11. package/templates/crm/app/pipeline-view.tsx +5 -4
  12. package/templates/crm/app/workspace.tsx +12 -10
  13. package/templates/crm/{app/login → components}/auth-form.tsx +7 -4
  14. package/templates/crm/components/require-auth.tsx +66 -0
  15. package/templates/helpdesk/app/customers/customers-view.tsx +6 -3
  16. package/templates/helpdesk/app/customers/page.tsx +3 -8
  17. package/templates/helpdesk/app/inbox-view.tsx +5 -4
  18. package/templates/helpdesk/app/layout.tsx +4 -4
  19. package/templates/helpdesk/app/page.tsx +3 -19
  20. package/templates/helpdesk/app/tickets/[id]/page.tsx +3 -13
  21. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +5 -4
  22. package/templates/helpdesk/app/workspace.tsx +12 -8
  23. package/templates/{inventory/app/login → helpdesk/components}/auth-form.tsx +7 -4
  24. package/templates/helpdesk/components/require-auth.tsx +66 -0
  25. package/templates/inventory/app/layout.tsx +4 -4
  26. package/templates/inventory/app/movements/movements-view.tsx +6 -3
  27. package/templates/inventory/app/movements/page.tsx +3 -8
  28. package/templates/inventory/app/page.tsx +3 -16
  29. package/templates/inventory/app/products/[id]/page.tsx +3 -13
  30. package/templates/inventory/app/products/[id]/product-view.tsx +5 -4
  31. package/templates/inventory/app/products-view.tsx +5 -4
  32. package/templates/inventory/app/workspace.tsx +12 -8
  33. package/templates/{helpdesk/app/login → inventory/components}/auth-form.tsx +7 -4
  34. package/templates/inventory/components/require-auth.tsx +66 -0
  35. package/templates/invoices/app/clients/clients-view.tsx +6 -3
  36. package/templates/invoices/app/clients/page.tsx +3 -8
  37. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +5 -4
  38. package/templates/invoices/app/invoices/[id]/page.tsx +3 -13
  39. package/templates/invoices/app/invoices-view.tsx +5 -4
  40. package/templates/invoices/app/layout.tsx +4 -4
  41. package/templates/invoices/app/page.tsx +3 -19
  42. package/templates/invoices/app/workspace.tsx +13 -8
  43. package/templates/invoices/{app/login → components}/auth-form.tsx +7 -4
  44. package/templates/invoices/components/require-auth.tsx +66 -0
  45. package/templates/projects/app/clients/clients-view.tsx +6 -3
  46. package/templates/projects/app/clients/page.tsx +3 -8
  47. package/templates/projects/app/layout.tsx +4 -4
  48. package/templates/projects/app/page.tsx +3 -15
  49. package/templates/projects/app/projects/[id]/page.tsx +3 -13
  50. package/templates/projects/app/projects/[id]/project-view.tsx +5 -4
  51. package/templates/projects/app/projects-view.tsx +5 -4
  52. package/templates/projects/app/workspace.tsx +12 -8
  53. package/templates/projects/components/auth-form.tsx +125 -0
  54. package/templates/projects/components/require-auth.tsx +66 -0
  55. package/templates/crm/app/login/page.tsx +0 -38
  56. package/templates/helpdesk/app/login/page.tsx +0 -38
  57. package/templates/inventory/app/login/page.tsx +0 -38
  58. package/templates/invoices/app/login/page.tsx +0 -38
  59. package/templates/projects/app/login/auth-form.tsx +0 -122
  60. package/templates/projects/app/login/page.tsx +0 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.346",
3
+ "version": "0.3.347",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -71,10 +71,11 @@ no policy allows an anonymous read — keep it that way if you add one.
71
71
 
72
72
  ## Theme
73
73
 
74
- Dark by default (`class="dark"` in `app/layout.tsx`). The light tokens in
75
- `app/globals.css` are complete, so removing that class flips the whole app. Both
76
- use the standard shadcn variables, so `npx shadcn@latest add <component>` drops
77
- in already themed.
74
+ Light by default. The dark tokens in `app/globals.css` are complete, so adding
75
+ `className="dark"` to the `<html>` in `app/layout.tsx` flips the whole app
76
+ wire it to a stored preference or `prefers-color-scheme` for a toggle. Both use
77
+ the standard shadcn variables, so `npx shadcn@latest add <component>` drops in
78
+ already themed.
78
79
 
79
80
  ## Grow it
80
81
 
@@ -10,13 +10,15 @@ import { EmptyState } from "@/components/empty-state";
10
10
  import { RecordDialog } from "@/components/record-dialog";
11
11
  import { Avatar } from "@/components/avatar";
12
12
  import { money, relativeTime } from "@/lib/pipeline";
13
+ import { RequireAuth } from "@/components/require-auth";
13
14
  import { Workspace, type CompanyRow, type DealRow } from "../workspace";
14
15
 
15
- export function CompaniesView({ email }: { email: string }) {
16
+ export function CompaniesView() {
16
17
  const [open, setOpen] = useState(false);
17
18
 
18
19
  return (
19
- <Workspace email={email} pathname="/companies">
20
+ <RequireAuth title="CRM" description="Your team shares one pipeline. Anyone with an account sees it.">
21
+ <Workspace pathname="/companies">
20
22
  {(data) => {
21
23
  // Pipeline per company, computed here rather than stored — one less
22
24
  // thing to keep in sync when a deal moves.
@@ -124,6 +126,7 @@ export function CompaniesView({ email }: { email: string }) {
124
126
  </>
125
127
  );
126
128
  }}
127
- </Workspace>
129
+ </Workspace>
130
+ </RequireAuth>
128
131
  );
129
132
  }
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { CompaniesView } from "./companies-view";
4
4
 
@@ -7,11 +7,6 @@ export const metadata: Metadata = {
7
7
  robots: "noindex",
8
8
  };
9
9
 
10
- export default function CompaniesPage({ auth, response, serverData }: PageProps) {
11
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
12
- response.redirect("/login");
13
- return null;
14
- }
15
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
16
- return <CompaniesView email={me?.email ?? ""} />;
10
+ export default function CompaniesPage({}: PageProps) {
11
+ return <CompaniesView />;
17
12
  }
@@ -10,13 +10,15 @@ import { EmptyState } from "@/components/empty-state";
10
10
  import { RecordDialog } from "@/components/record-dialog";
11
11
  import { Avatar } from "@/components/avatar";
12
12
  import { relativeTime } from "@/lib/pipeline";
13
+ import { RequireAuth } from "@/components/require-auth";
13
14
  import { Workspace, type ContactRow } from "../workspace";
14
15
 
15
- export function ContactsView({ email }: { email: string }) {
16
+ export function ContactsView() {
16
17
  const [open, setOpen] = useState(false);
17
18
 
18
19
  return (
19
- <Workspace email={email} pathname="/contacts">
20
+ <RequireAuth title="CRM" description="Your team shares one pipeline. Anyone with an account sees it.">
21
+ <Workspace pathname="/contacts">
20
22
  {(data) => {
21
23
  const columns: ColumnDef<ContactRow>[] = [
22
24
  {
@@ -134,6 +136,7 @@ export function ContactsView({ email }: { email: string }) {
134
136
  </>
135
137
  );
136
138
  }}
137
- </Workspace>
139
+ </Workspace>
140
+ </RequireAuth>
138
141
  );
139
142
  }
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { ContactsView } from "./contacts-view";
4
4
 
@@ -7,11 +7,6 @@ export const metadata: Metadata = {
7
7
  robots: "noindex",
8
8
  };
9
9
 
10
- export default function ContactsPage({ auth, response, serverData }: PageProps) {
11
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
12
- response.redirect("/login");
13
- return null;
14
- }
15
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
16
- return <ContactsView email={me?.email ?? ""} />;
10
+ export default function ContactsPage({}: PageProps) {
11
+ return <ContactsView />;
17
12
  }
@@ -11,13 +11,15 @@ import { ActivityTimeline } from "@/components/activity-timeline";
11
11
  import { StageBadge } from "@/components/stage-badge";
12
12
  import { Avatar } from "@/components/avatar";
13
13
  import { PIPELINE, daysUntil, money, relativeTime } from "@/lib/pipeline";
14
+ import { RequireAuth } from "@/components/require-auth";
14
15
  import { Workspace } from "../../workspace";
15
16
 
16
- export function DealView({ email, dealId }: { email: string; dealId: string }) {
17
+ export function DealView({ dealId }: { dealId: string }) {
17
18
  const router = useRouter();
18
19
 
19
20
  return (
20
- <Workspace email={email} pathname="/">
21
+ <RequireAuth title="CRM" description="Your team shares one pipeline. Anyone with an account sees it.">
22
+ <Workspace pathname="/">
21
23
  {(data) => {
22
24
  const deal = data.deals.find((d) => d.id === dealId);
23
25
 
@@ -169,7 +171,8 @@ export function DealView({ email, dealId }: { email: string; dealId: string }) {
169
171
  </>
170
172
  );
171
173
  }}
172
- </Workspace>
174
+ </Workspace>
175
+ </RequireAuth>
173
176
  );
174
177
  }
175
178
 
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { DealView } from "./deal-view";
4
4
 
@@ -8,16 +8,6 @@ export const metadata: Metadata = {
8
8
  };
9
9
 
10
10
  /** `app/deals/[id]/page.tsx` → `/deals/:id`. */
11
- export default function DealPage({
12
- auth,
13
- response,
14
- params,
15
- serverData,
16
- }: PageProps<{ id: string }>) {
17
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
18
- response.redirect("/login");
19
- return null;
20
- }
21
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
22
- return <DealView email={me?.email ?? ""} dealId={params.id} />;
11
+ export default function DealPage({ params }: PageProps<{ id: string }>) {
12
+ return <DealView dealId={params.id} />;
23
13
  }
@@ -9,13 +9,13 @@ interface LayoutProps {
9
9
  * lives in `app/workspace.tsx` instead, because it needs the synced data; this
10
10
  * layer only sets up the page.
11
11
  *
12
- * `class="dark"` makes dark the default theme. The light tokens in globals.css
13
- * are complete and tested, so removing the class flips the whole app; swap it
14
- * for a stored preference when you want a toggle.
12
+ * Light by default. The dark tokens in globals.css are complete, so adding
13
+ * `className="dark"` here flips the whole app or wire it to a stored
14
+ * preference, or to `prefers-color-scheme`, when you want a toggle.
15
15
  */
16
16
  export default function RootLayout({ children }: LayoutProps) {
17
17
  return (
18
- <html lang="en" className="dark">
18
+ <html lang="en">
19
19
  <head>
20
20
  <meta charSet="utf-8" />
21
21
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { PipelineView } from "./pipeline-view";
4
4
 
@@ -10,27 +10,10 @@ export const metadata: Metadata = {
10
10
  /**
11
11
  * `/` — the pipeline board.
12
12
  *
13
- * Server-side auth gate only. Reading `auth` opts this render out of caching,
14
- * which is correct: every screen in this app is private.
15
- *
16
- * The signed-in email comes from the User row, not from `auth` — `PageAuth` is
17
- * `{ user_id, is_admin, tenant_id, roles }` and carries no email.
18
13
  */
19
- export default function PipelinePage({
20
- auth,
21
- response,
22
- searchParams,
23
- serverData,
24
- }: PageProps) {
25
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
26
- response.redirect("/login");
27
- return null;
28
- }
29
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
30
-
14
+ export default function PipelinePage({ searchParams }: PageProps) {
31
15
  return (
32
16
  <PipelineView
33
- email={me?.email ?? ""}
34
17
  // The ⌘K "New deal" action navigates here with ?new=deal.
35
18
  openNew={searchParams?.new === "deal"}
36
19
  />
@@ -10,13 +10,12 @@ import { MetricsBar } from "@/components/metrics-bar";
10
10
  import { PipelineBoard } from "@/components/pipeline-board";
11
11
  import { BoardSkeleton } from "@/components/board-skeleton";
12
12
  import { DealDialog } from "@/components/deal-dialog";
13
+ import { RequireAuth } from "@/components/require-auth";
13
14
  import { Workspace } from "./workspace";
14
15
 
15
16
  export function PipelineView({
16
- email,
17
17
  openNew,
18
18
  }: {
19
- email: string;
20
19
  openNew?: boolean;
21
20
  }) {
22
21
  const router = useRouter();
@@ -42,7 +41,8 @@ export function PipelineView({
42
41
  }, []);
43
42
 
44
43
  return (
45
- <Workspace email={email} pathname="/">
44
+ <RequireAuth title="CRM" description="Your team shares one pipeline. Anyone with an account sees it.">
45
+ <Workspace pathname="/">
46
46
  {(data) => (
47
47
  <>
48
48
  <PageHeader title="Pipeline" count={data.deals.length}>
@@ -94,6 +94,7 @@ export function PipelineView({
94
94
  />
95
95
  </>
96
96
  )}
97
- </Workspace>
97
+ </Workspace>
98
+ </RequireAuth>
98
99
  );
99
100
  }
@@ -73,16 +73,18 @@ export interface WorkspaceData {
73
73
  * so there's one behaviour to reason about rather than two.
74
74
  */
75
75
  export function Workspace({
76
- email,
77
76
  pathname,
78
77
  children,
79
78
  }: {
80
- email: string;
81
79
  pathname: string;
82
80
  children: (data: WorkspaceData) => React.ReactNode;
83
81
  }) {
84
82
  const router = useRouter();
85
- const { signOut } = useAuth();
83
+ // The signed-in identity comes from the CLIENT session, not from an SSR
84
+ // prop: the session cookie is SameSite=Lax and browsers withhold it inside
85
+ // the builder's cross-site preview iframe, so a server-resolved email would
86
+ // be empty exactly where this app is most often viewed.
87
+ const { signOut, userId } = useAuth();
86
88
  const { data: companies, loading: loadingCompanies } =
87
89
  db.useQuery<CompanyRow>("Company");
88
90
  const { data: contacts } = db.useQuery<ContactRow>("Contact");
@@ -90,6 +92,11 @@ export function Workspace({
90
92
  const { data: activities } = db.useQuery<ActivityRow>("Activity");
91
93
  const { data: users } = db.useQuery<UserRow>("User");
92
94
 
95
+ // Whoever is signed in, named from the synced User row rather than an
96
+ // SSR prop — see the note on useAuth above.
97
+ const signedInEmail =
98
+ (users ?? []).find((user) => user.id === userId)?.email ?? "";
99
+
93
100
  const [paletteOpen, setPaletteOpen] = useState(false);
94
101
  const seeded = useRef(false);
95
102
 
@@ -187,19 +194,14 @@ export function Workspace({
187
194
  <div className="flex h-screen overflow-hidden">
188
195
  <Sidebar
189
196
  workspace="CRM"
190
- email={email}
197
+ email={signedInEmail}
191
198
  pathname={pathname}
192
199
  counts={{
193
200
  "/companies": companyList.length,
194
201
  "/contacts": contactList.length,
195
202
  }}
196
203
  onOpenCommand={() => setPaletteOpen(true)}
197
- onSignOut={async () => {
198
- await signOut();
199
- // Full navigation so the SSR runtime re-resolves auth from the
200
- // cookie and the login page renders server-side.
201
- window.location.assign("/login");
202
- }}
204
+ onSignOut={() => void signOut()}
203
205
  />
204
206
  <main className="flex min-w-0 flex-1 flex-col">{children(data)}</main>
205
207
 
@@ -16,9 +16,10 @@ import { Label } from "@/components/ui/label";
16
16
  * then `persistSession` writes the token so the sync engine and `callFn`
17
17
  * authenticate as this user on the next load.
18
18
  *
19
- * The navigation is a full `assign`, not a client route change, so the SSR
20
- * runtime re-resolves auth from the HttpOnly cookie and the first paint of the
21
- * workspace is already signed in.
19
+ * No navigation afterwards: `persistSession` notifies the sync engine and the
20
+ * client-side gate swaps the app in. Reloading would hand the decision to the
21
+ * SSR cookie, which browsers withhold inside the builder's cross-site preview
22
+ * iframe — that's the sign-in loop this avoids.
22
23
  */
23
24
  export function AuthForm() {
24
25
  const [mode, setMode] = useState<"login" | "signup">("login");
@@ -36,8 +37,10 @@ export function AuthForm() {
36
37
  mode === "login"
37
38
  ? await passwordLogin({ email, password })
38
39
  : await passwordRegister({ email, password });
40
+ // No navigation: persistSession notifies the sync engine, RequireAuth
41
+ // re-renders, and the app appears in place. A full page load here would
42
+ // depend on the SSR session cookie, which a cross-site iframe withholds.
39
43
  persistSession(session);
40
- window.location.assign("/");
41
44
  } catch (err) {
42
45
  setError(messageFor(err));
43
46
  setPending(false);
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useAuth } from "@pylonsync/client";
5
+ import { AuthForm } from "@/components/auth-form";
6
+
7
+ /**
8
+ * Client-side auth gate.
9
+ *
10
+ * Deliberately NOT a server-side `response.redirect("/login")`. The session
11
+ * cookie is `SameSite=Lax`, which browsers withhold on cross-site iframe
12
+ * navigations — and this app is shown inside an iframe in the Pylon Cloud
13
+ * builder's live preview. An SSR gate there sees every request as anonymous no
14
+ * matter how many times you sign in, so signing in bounces straight back to the
15
+ * form. The token in local storage is visible to the client either way, so the
16
+ * gate belongs here.
17
+ *
18
+ * It also removes a full page load from the sign-in path: authenticating swaps
19
+ * this component's children in place instead of navigating.
20
+ */
21
+ export function RequireAuth({
22
+ title,
23
+ description,
24
+ children,
25
+ }: {
26
+ title: string;
27
+ description: string;
28
+ children: React.ReactNode;
29
+ }) {
30
+ const { isSignedIn, isLoaded } = useAuth();
31
+
32
+ // `isLoaded` covers the beat before the engine has resolved the stored token.
33
+ // Rendering the form during it would flash a sign-in screen at someone who is
34
+ // already signed in.
35
+ if (!isLoaded) {
36
+ return (
37
+ <main className="flex min-h-screen items-center justify-center">
38
+ <span className="text-[13px] text-muted-foreground">Loading…</span>
39
+ </main>
40
+ );
41
+ }
42
+
43
+ if (!isSignedIn) {
44
+ return (
45
+ <main className="flex min-h-screen items-center justify-center px-6">
46
+ <div className="w-full max-w-[320px]">
47
+ <div className="mb-7 flex items-center gap-2">
48
+ <span className="flex size-7 items-center justify-center rounded-md bg-primary text-[12px] font-bold text-primary-foreground">
49
+ {title.slice(0, 1).toUpperCase()}
50
+ </span>
51
+ <span className="text-[15px] font-semibold tracking-tight">{title}</span>
52
+ </div>
53
+ <h1 className="text-[19px] font-semibold tracking-tight">
54
+ Sign in to your workspace
55
+ </h1>
56
+ <p className="mt-1 text-[13px] text-muted-foreground">{description}</p>
57
+ <div className="mt-6">
58
+ <AuthForm />
59
+ </div>
60
+ </div>
61
+ </main>
62
+ );
63
+ }
64
+
65
+ return <>{children}</>;
66
+ }
@@ -11,14 +11,16 @@ import { RecordDialog } from "@/components/record-dialog";
11
11
  import { Avatar } from "@/components/avatar";
12
12
  import { relativeTime } from "@/lib/format";
13
13
  import { isOpen } from "@/lib/tickets";
14
+ import { RequireAuth } from "@/components/require-auth";
14
15
  import { Workspace, type CustomerRow } from "../workspace";
15
16
 
16
- export function CustomersView({ email }: { email: string }) {
17
+ export function CustomersView() {
17
18
  const router = useRouter();
18
19
  const [open, setOpen] = useState(false);
19
20
 
20
21
  return (
21
- <Workspace email={email} pathname="/customers">
22
+ <RequireAuth title="Helpdesk" description="Your team shares one queue. Anyone with an account sees it.">
23
+ <Workspace pathname="/customers">
22
24
  {(data) => {
23
25
  // Ticket counts per customer, derived rather than stored — one less
24
26
  // thing to keep in sync when a ticket is opened or solved.
@@ -135,6 +137,7 @@ export function CustomersView({ email }: { email: string }) {
135
137
  </>
136
138
  );
137
139
  }}
138
- </Workspace>
140
+ </Workspace>
141
+ </RequireAuth>
139
142
  );
140
143
  }
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { CustomersView } from "./customers-view";
4
4
 
@@ -7,11 +7,6 @@ export const metadata: Metadata = {
7
7
  robots: "noindex",
8
8
  };
9
9
 
10
- export default function CustomersPage({ auth, response, serverData }: PageProps) {
11
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
12
- response.redirect("/login");
13
- return null;
14
- }
15
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
16
- return <CustomersView email={me?.email ?? ""} />;
10
+ export default function CustomersPage({}: PageProps) {
11
+ return <CustomersView />;
17
12
  }
@@ -11,14 +11,13 @@ import { EmptyState } from "@/components/empty-state";
11
11
  import { TicketList } from "@/components/ticket-list";
12
12
  import { NewTicketDialog } from "@/components/new-ticket-dialog";
13
13
  import { STATUSES, applyFilter, counts, type Filter } from "@/lib/tickets";
14
+ import { RequireAuth } from "@/components/require-auth";
14
15
  import { Workspace } from "./workspace";
15
16
 
16
17
  export function InboxView({
17
- email,
18
18
  openNew,
19
19
  initialFilter,
20
20
  }: {
21
- email: string;
22
21
  openNew?: boolean;
23
22
  initialFilter?: string;
24
23
  }) {
@@ -47,7 +46,8 @@ export function InboxView({
47
46
  }, []);
48
47
 
49
48
  return (
50
- <Workspace email={email} pathname="/">
49
+ <RequireAuth title="Helpdesk" description="Your team shares one queue. Anyone with an account sees it.">
50
+ <Workspace pathname="/">
51
51
  {(data) => {
52
52
  const filter: Filter = {
53
53
  ...(status === "all" ? {} : { status }),
@@ -142,6 +142,7 @@ export function InboxView({
142
142
  </>
143
143
  );
144
144
  }}
145
- </Workspace>
145
+ </Workspace>
146
+ </RequireAuth>
146
147
  );
147
148
  }
@@ -9,13 +9,13 @@ interface LayoutProps {
9
9
  * lives in `app/workspace.tsx` instead, because it needs the synced data; this
10
10
  * layer only sets up the page.
11
11
  *
12
- * `class="dark"` makes dark the default theme. The light tokens in globals.css
13
- * are complete and tested, so removing the class flips the whole app; swap it
14
- * for a stored preference when you want a toggle.
12
+ * Light by default. The dark tokens in globals.css are complete, so adding
13
+ * `className="dark"` here flips the whole app or wire it to a stored
14
+ * preference, or to `prefers-color-scheme`, when you want a toggle.
15
15
  */
16
16
  export default function RootLayout({ children }: LayoutProps) {
17
17
  return (
18
- <html lang="en" className="dark">
18
+ <html lang="en">
19
19
  <head>
20
20
  <meta charSet="utf-8" />
21
21
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { InboxView } from "./inbox-view";
4
4
 
@@ -10,28 +10,12 @@ export const metadata: Metadata = {
10
10
  /**
11
11
  * `/` — the support queue.
12
12
  *
13
- * Server-side auth gate only. Reading `auth` opts this render out of caching,
14
- * which is correct: every screen here is private.
15
- *
16
- * The signed-in email comes from the User row, not from `auth` — `PageAuth` is
17
- * `{ user_id, is_admin, tenant_id, roles }` and carries no email.
18
13
  */
19
- export default function InboxPage({
20
- auth,
21
- response,
22
- searchParams,
23
- serverData,
24
- }: PageProps) {
25
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
26
- response.redirect("/login");
27
- return null;
28
- }
29
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
14
+ export default function InboxPage({ searchParams }: PageProps) {
30
15
 
31
16
  return (
32
17
  <InboxView
33
- email={me?.email ?? ""}
34
- openNew={searchParams?.new === "ticket"}
18
+ openNew={searchParams?.new === "ticket"}
35
19
  initialFilter={searchParams?.filter}
36
20
  />
37
21
  );
@@ -1,4 +1,4 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import type { Metadata, PageProps } from "@pylonsync/react";
3
3
  import { TicketView } from "./ticket-view";
4
4
 
@@ -8,16 +8,6 @@ export const metadata: Metadata = {
8
8
  };
9
9
 
10
10
  /** `app/tickets/[id]/page.tsx` -> `/tickets/:id`. */
11
- export default function TicketPage({
12
- auth,
13
- response,
14
- params,
15
- serverData,
16
- }: PageProps<{ id: string }>) {
17
- if (!auth.user_id || auth.user_id.startsWith("guest_")) {
18
- response.redirect("/login");
19
- return null;
20
- }
21
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
22
- return <TicketView email={me?.email ?? ""} ticketId={params.id} />;
11
+ export default function TicketPage({ params }: PageProps<{ id: string }>) {
12
+ return <TicketView ticketId={params.id} />;
23
13
  }
@@ -13,19 +13,19 @@ import { SlaIndicator } from "@/components/sla-indicator";
13
13
  import { Avatar } from "@/components/avatar";
14
14
  import { relativeTime } from "@/lib/format";
15
15
  import { PRIORITIES, STATUSES, ticketNumber } from "@/lib/tickets";
16
+ import { RequireAuth } from "@/components/require-auth";
16
17
  import { Workspace } from "../../workspace";
17
18
 
18
19
  export function TicketView({
19
- email,
20
20
  ticketId,
21
21
  }: {
22
- email: string;
23
22
  ticketId: string;
24
23
  }) {
25
24
  const router = useRouter();
26
25
 
27
26
  return (
28
- <Workspace email={email} pathname="/">
27
+ <RequireAuth title="Helpdesk" description="Your team shares one queue. Anyone with an account sees it.">
28
+ <Workspace pathname="/">
29
29
  {(data) => {
30
30
  const ticket = data.tickets.find((t) => t.id === ticketId);
31
31
 
@@ -159,6 +159,7 @@ export function TicketView({
159
159
  </>
160
160
  );
161
161
  }}
162
- </Workspace>
162
+ </Workspace>
163
+ </RequireAuth>
163
164
  );
164
165
  }