@pylonsync/create-pylon 0.3.345 → 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.
- package/package.json +1 -1
- package/templates/crm/README.md +5 -4
- package/templates/crm/app/companies/companies-view.tsx +6 -3
- package/templates/crm/app/companies/page.tsx +3 -8
- package/templates/crm/app/contacts/contacts-view.tsx +6 -3
- package/templates/crm/app/contacts/page.tsx +3 -8
- package/templates/crm/app/deals/[id]/deal-view.tsx +6 -3
- package/templates/crm/app/deals/[id]/page.tsx +3 -13
- package/templates/crm/app/layout.tsx +4 -4
- package/templates/crm/app/page.tsx +2 -19
- package/templates/crm/app/pipeline-view.tsx +5 -4
- package/templates/crm/app/workspace.tsx +12 -10
- package/templates/crm/{app/login → components}/auth-form.tsx +7 -4
- package/templates/crm/components/require-auth.tsx +66 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +6 -3
- package/templates/helpdesk/app/customers/page.tsx +3 -8
- package/templates/helpdesk/app/inbox-view.tsx +5 -4
- package/templates/helpdesk/app/layout.tsx +4 -4
- package/templates/helpdesk/app/page.tsx +3 -19
- package/templates/helpdesk/app/tickets/[id]/page.tsx +3 -13
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +5 -4
- package/templates/helpdesk/app/workspace.tsx +12 -8
- package/templates/{inventory/app/login → helpdesk/components}/auth-form.tsx +7 -4
- package/templates/helpdesk/components/require-auth.tsx +66 -0
- package/templates/inventory/app/layout.tsx +4 -4
- package/templates/inventory/app/movements/movements-view.tsx +6 -3
- package/templates/inventory/app/movements/page.tsx +3 -8
- package/templates/inventory/app/page.tsx +3 -16
- package/templates/inventory/app/products/[id]/page.tsx +3 -13
- package/templates/inventory/app/products/[id]/product-view.tsx +5 -4
- package/templates/inventory/app/products-view.tsx +5 -4
- package/templates/inventory/app/workspace.tsx +12 -8
- package/templates/{helpdesk/app/login → inventory/components}/auth-form.tsx +7 -4
- package/templates/inventory/components/require-auth.tsx +66 -0
- package/templates/invoices/app/clients/clients-view.tsx +6 -3
- package/templates/invoices/app/clients/page.tsx +3 -8
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +5 -4
- package/templates/invoices/app/invoices/[id]/page.tsx +3 -13
- package/templates/invoices/app/invoices-view.tsx +5 -4
- package/templates/invoices/app/layout.tsx +4 -4
- package/templates/invoices/app/page.tsx +3 -19
- package/templates/invoices/app/workspace.tsx +13 -8
- package/templates/invoices/{app/login → components}/auth-form.tsx +7 -4
- package/templates/invoices/components/require-auth.tsx +66 -0
- package/templates/projects/app/clients/clients-view.tsx +6 -3
- package/templates/projects/app/clients/page.tsx +3 -8
- package/templates/projects/app/layout.tsx +4 -4
- package/templates/projects/app/page.tsx +3 -15
- package/templates/projects/app/projects/[id]/page.tsx +3 -13
- package/templates/projects/app/projects/[id]/project-view.tsx +5 -4
- package/templates/projects/app/projects-view.tsx +5 -4
- package/templates/projects/app/workspace.tsx +12 -8
- package/templates/projects/components/auth-form.tsx +125 -0
- package/templates/projects/components/require-auth.tsx +66 -0
- package/templates/crm/app/login/page.tsx +0 -38
- package/templates/helpdesk/app/login/page.tsx +0 -38
- package/templates/inventory/app/login/page.tsx +0 -38
- package/templates/invoices/app/login/page.tsx +0 -38
- package/templates/projects/app/login/auth-form.tsx +0 -122
- 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.
|
|
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"
|
package/templates/crm/README.md
CHANGED
|
@@ -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
|
-
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
16
|
+
export function CompaniesView() {
|
|
16
17
|
const [open, setOpen] = useState(false);
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
|
-
<
|
|
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
|
-
|
|
129
|
+
</Workspace>
|
|
130
|
+
</RequireAuth>
|
|
128
131
|
);
|
|
129
132
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import 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({
|
|
11
|
-
|
|
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(
|
|
16
|
+
export function ContactsView() {
|
|
16
17
|
const [open, setOpen] = useState(false);
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
|
-
<
|
|
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
|
-
|
|
139
|
+
</Workspace>
|
|
140
|
+
</RequireAuth>
|
|
138
141
|
);
|
|
139
142
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import 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({
|
|
11
|
-
|
|
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({
|
|
17
|
+
export function DealView({ dealId }: { dealId: string }) {
|
|
17
18
|
const router = useRouter();
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
|
-
<
|
|
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
|
-
|
|
174
|
+
</Workspace>
|
|
175
|
+
</RequireAuth>
|
|
173
176
|
);
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import 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
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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"
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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={
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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(
|
|
17
|
+
export function CustomersView() {
|
|
17
18
|
const router = useRouter();
|
|
18
19
|
const [open, setOpen] = useState(false);
|
|
19
20
|
|
|
20
21
|
return (
|
|
21
|
-
<
|
|
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
|
-
|
|
140
|
+
</Workspace>
|
|
141
|
+
</RequireAuth>
|
|
139
142
|
);
|
|
140
143
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import 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({
|
|
11
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
162
|
+
</Workspace>
|
|
163
|
+
</RequireAuth>
|
|
163
164
|
);
|
|
164
165
|
}
|