create-zerotal 1.0.0
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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/package.json +43 -0
- package/src/index.ts +136 -0
- package/src/prompts.ts +81 -0
- package/src/scaffold.ts +104 -0
- package/templates/admin/_env.example +2 -0
- package/templates/admin/_gitignore +4 -0
- package/templates/admin/app/admin/ProductResource.ts +192 -0
- package/templates/admin/app/admin/SettingsResource.ts +37 -0
- package/templates/admin/app/admin/UserResource.ts +103 -0
- package/templates/admin/app/admin/index.ts +46 -0
- package/templates/admin/app/admin/shared.ts +36 -0
- package/templates/admin/app/admin/widgets.ts +51 -0
- package/templates/admin/app/models/Product.ts +21 -0
- package/templates/admin/app/models/Setting.ts +14 -0
- package/templates/admin/app/models/User.ts +20 -0
- package/templates/admin/bootstrap/app.ts +16 -0
- package/templates/admin/bootstrap/providers.ts +23 -0
- package/templates/admin/config/app.ts +22 -0
- package/templates/admin/config/auth.ts +5 -0
- package/templates/admin/config/cache.ts +7 -0
- package/templates/admin/config/database.ts +14 -0
- package/templates/admin/config/session.ts +10 -0
- package/templates/admin/database/seeders/DatabaseSeeder.ts +50 -0
- package/templates/admin/package.json +18 -0
- package/templates/admin/tests/admin_test.ts +58 -0
- package/templates/admin/tsconfig.json +20 -0
- package/templates/admin/zt.ts +15 -0
- package/templates/api/_env.example +4 -0
- package/templates/api/_gitignore +5 -0
- package/templates/api/app/controllers/AuthController.ts +69 -0
- package/templates/api/app/controllers/WelcomeController.ts +10 -0
- package/templates/api/app/middleware/RequireAuth.ts +10 -0
- package/templates/api/app/models/User.ts +16 -0
- package/templates/api/bootstrap/app.ts +19 -0
- package/templates/api/config/app.ts +8 -0
- package/templates/api/config/database.ts +6 -0
- package/templates/api/config/notifications.ts +13 -0
- package/templates/api/config/queue.ts +6 -0
- package/templates/api/config/session.ts +10 -0
- package/templates/api/database/migrations/0001_create_users_table.ts +18 -0
- package/templates/api/package.json +16 -0
- package/templates/api/routes/index.ts +13 -0
- package/templates/api/tests/README.md +77 -0
- package/templates/api/tests/auth.test.ts +134 -0
- package/templates/api/tests/helpers.ts +36 -0
- package/templates/api/tsconfig.json +14 -0
- package/templates/api/zt.ts +16 -0
- package/templates/flow/_env.example +2 -0
- package/templates/flow/_gitignore +6 -0
- package/templates/flow/app/flow/layouts/app.tsx +52 -0
- package/templates/flow/app/flow/pages/about.tsx +35 -0
- package/templates/flow/app/flow/pages/contact.tsx +47 -0
- package/templates/flow/app/flow/pages/index.tsx +48 -0
- package/templates/flow/bootstrap/app.ts +8 -0
- package/templates/flow/bootstrap/providers.ts +6 -0
- package/templates/flow/config/app.ts +28 -0
- package/templates/flow/package.json +20 -0
- package/templates/flow/public/zt.svg +17 -0
- package/templates/flow/resources/css/app.css +1 -0
- package/templates/flow/resources/js/app.ts +10 -0
- package/templates/flow/resources/js/env.d.ts +23 -0
- package/templates/flow/tests/smoke.test.ts +56 -0
- package/templates/flow/tsconfig.json +18 -0
- package/templates/flow/zt.ts +15 -0
- package/templates/minimal/_env.example +2 -0
- package/templates/minimal/_gitignore +6 -0
- package/templates/minimal/bootstrap/app.ts +9 -0
- package/templates/minimal/config/app.ts +28 -0
- package/templates/minimal/package.json +20 -0
- package/templates/minimal/resources/css/app.css +1 -0
- package/templates/minimal/resources/js/app.ts +2 -0
- package/templates/minimal/resources/js/components/Logo.tsx +48 -0
- package/templates/minimal/resources/js/env.d.ts +13 -0
- package/templates/minimal/resources/js/layouts/AppLayout.tsx +56 -0
- package/templates/minimal/resources/js/pages/about.tsx +30 -0
- package/templates/minimal/resources/js/pages/contact.tsx +49 -0
- package/templates/minimal/resources/js/pages/welcome.tsx +42 -0
- package/templates/minimal/routes/index.ts +16 -0
- package/templates/minimal/tests/smoke.test.ts +51 -0
- package/templates/minimal/tsconfig.json +19 -0
- package/templates/minimal/zt.ts +15 -0
- package/templates/react/_env.example +4 -0
- package/templates/react/_gitignore +5 -0
- package/templates/react/app/exceptions/Handler.ts +55 -0
- package/templates/react/app/routes/about.ts +8 -0
- package/templates/react/app/routes/contact.ts +31 -0
- package/templates/react/app/routes/index.ts +11 -0
- package/templates/react/bootstrap/app.ts +12 -0
- package/templates/react/bootstrap/providers.ts +10 -0
- package/templates/react/config/app.ts +25 -0
- package/templates/react/config/inertia.ts +7 -0
- package/templates/react/config/session.ts +11 -0
- package/templates/react/package.json +26 -0
- package/templates/react/public/zt.svg +17 -0
- package/templates/react/resources/app.html +39 -0
- package/templates/react/resources/css/app.css +206 -0
- package/templates/react/resources/js/Components/Button.tsx +67 -0
- package/templates/react/resources/js/Components/Card.tsx +40 -0
- package/templates/react/resources/js/Components/Code.tsx +33 -0
- package/templates/react/resources/js/Components/Field.tsx +125 -0
- package/templates/react/resources/js/Components/FlashToasts.tsx +79 -0
- package/templates/react/resources/js/Components/Icons.tsx +149 -0
- package/templates/react/resources/js/Components/ThemeToggle.tsx +47 -0
- package/templates/react/resources/js/Layouts/AppLayout.tsx +163 -0
- package/templates/react/resources/js/app.tsx +29 -0
- package/templates/react/resources/js/env.d.ts +13 -0
- package/templates/react/resources/js/lib/cn.ts +13 -0
- package/templates/react/resources/js/lib/site.ts +11 -0
- package/templates/react/resources/js/pages/about.tsx +146 -0
- package/templates/react/resources/js/pages/contact.tsx +152 -0
- package/templates/react/resources/js/pages/error.tsx +91 -0
- package/templates/react/resources/js/pages/home.tsx +199 -0
- package/templates/react/resources/js/pages.generated.ts +13 -0
- package/templates/react/resources/js/types.ts +17 -0
- package/templates/react/tests/smoke.test.ts +69 -0
- package/templates/react/tsconfig.json +21 -0
- package/templates/react/zt.ts +15 -0
- package/templates/vue/_env.example +4 -0
- package/templates/vue/_gitignore +5 -0
- package/templates/vue/app/routes/about.ts +7 -0
- package/templates/vue/app/routes/contact.ts +7 -0
- package/templates/vue/app/routes/index.ts +8 -0
- package/templates/vue/bootstrap/app.ts +8 -0
- package/templates/vue/bootstrap/providers.ts +6 -0
- package/templates/vue/config/app.ts +25 -0
- package/templates/vue/config/inertia.ts +7 -0
- package/templates/vue/package.json +24 -0
- package/templates/vue/resources/app.html +13 -0
- package/templates/vue/resources/css/app.css +1 -0
- package/templates/vue/resources/js/Layouts/AppLayout.vue +63 -0
- package/templates/vue/resources/js/app.tsx +19 -0
- package/templates/vue/resources/js/env.d.ts +20 -0
- package/templates/vue/resources/js/pages/about.vue +41 -0
- package/templates/vue/resources/js/pages/contact.vue +54 -0
- package/templates/vue/resources/js/pages/home.vue +54 -0
- package/templates/vue/resources/js/pages.generated.ts +12 -0
- package/templates/vue/tests/smoke.test.ts +46 -0
- package/templates/vue/tsconfig.json +21 -0
- package/templates/vue/zt.ts +15 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-wide constants the UI reads.
|
|
3
|
+
*
|
|
4
|
+
* `APP_NAME` is stamped in when the project is scaffolded — the same value as
|
|
5
|
+
* `name` in config/app.ts and the `<title>` suffix in resources/js/app.tsx.
|
|
6
|
+
* Change all three together when you rename the app.
|
|
7
|
+
*/
|
|
8
|
+
export const APP_NAME = "{{name}}";
|
|
9
|
+
|
|
10
|
+
/** Where the header and footer "Docs" links point. */
|
|
11
|
+
export const DOCS_URL = "https://github.com/zerotaldev/zerotal";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Head } from "@inertiajs/react";
|
|
3
|
+
import AppLayout from "../Layouts/AppLayout";
|
|
4
|
+
import { ButtonLink } from "../Components/Button";
|
|
5
|
+
import { Card } from "../Components/Card";
|
|
6
|
+
import { Code } from "../Components/Code";
|
|
7
|
+
import { ArrowRightIcon } from "../Components/Icons";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
title: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The trip this very page made, from URL to rendered React. */
|
|
14
|
+
const LIFECYCLE = [
|
|
15
|
+
{
|
|
16
|
+
where: "app/routes/about.ts",
|
|
17
|
+
what: (
|
|
18
|
+
<>
|
|
19
|
+
The URL <Code>/about</Code> is this file's path. Its exported <Code>GET</Code> function runs
|
|
20
|
+
on the server.
|
|
21
|
+
</>
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
where: 'Inertia.render("about", props)',
|
|
26
|
+
what: (
|
|
27
|
+
<>
|
|
28
|
+
Names the page component and hands it data. On a first load the server returns HTML; on
|
|
29
|
+
every visit after that, just JSON.
|
|
30
|
+
</>
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
where: "resources/js/pages/about.tsx",
|
|
35
|
+
what: (
|
|
36
|
+
<>
|
|
37
|
+
Receives <Code>props</Code> as React props. Each page is bundled into its own chunk and
|
|
38
|
+
fetched the first time it is needed.
|
|
39
|
+
</>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
where: "Layouts/AppLayout.tsx",
|
|
44
|
+
what: (
|
|
45
|
+
<>
|
|
46
|
+
Wraps the page. Assigned via <Code>Page.layout</Code>, it stays mounted across visits — the
|
|
47
|
+
header never re-renders.
|
|
48
|
+
</>
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const TREE = [
|
|
54
|
+
{ path: "app/routes/", note: "One file per URL. Exports GET, POST, PUT, DELETE." },
|
|
55
|
+
{ path: "bootstrap/", note: "Application creation and the provider list." },
|
|
56
|
+
{ path: "config/", note: "Typed config objects, read with config('app.name')." },
|
|
57
|
+
{ path: "resources/js/pages/", note: "Inertia page components, one per screen." },
|
|
58
|
+
{ path: "resources/js/Layouts/", note: "Persistent shells that survive navigation." },
|
|
59
|
+
{ path: "resources/js/Components/", note: "Buttons, cards, fields — the shared vocabulary." },
|
|
60
|
+
{ path: "resources/css/app.css", note: "Design tokens. Every colour in the app starts here." },
|
|
61
|
+
{ path: "tests/", note: "Runs with the app booted via bun zt test." },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function About({ title }: Props) {
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<Head title={title} />
|
|
68
|
+
|
|
69
|
+
<div className="mx-auto max-w-3xl">
|
|
70
|
+
<header>
|
|
71
|
+
<p className="font-mono text-xs tracking-[0.2em] text-muted-foreground uppercase">
|
|
72
|
+
The tour
|
|
73
|
+
</p>
|
|
74
|
+
<h1 className="mt-3 text-3xl font-bold tracking-tight text-balance sm:text-4xl">
|
|
75
|
+
{title}
|
|
76
|
+
</h1>
|
|
77
|
+
<p className="mt-4 text-lg leading-relaxed text-pretty text-muted-foreground">
|
|
78
|
+
One TypeScript project serves the HTML, owns the data, and renders the React. Here is
|
|
79
|
+
how the page you are reading got here.
|
|
80
|
+
</p>
|
|
81
|
+
</header>
|
|
82
|
+
|
|
83
|
+
<section className="mt-14">
|
|
84
|
+
<h2 className="font-semibold">From URL to rendered page</h2>
|
|
85
|
+
|
|
86
|
+
<ol className="mt-6 space-y-px">
|
|
87
|
+
{LIFECYCLE.map((step, index) => (
|
|
88
|
+
<li key={step.where} className="flex gap-4">
|
|
89
|
+
{/* The rail: a number, and a line joining it to the next step. */}
|
|
90
|
+
<div className="flex flex-col items-center">
|
|
91
|
+
<span className="grid size-8 shrink-0 place-items-center rounded-full border border-border bg-card font-mono text-sm text-primary">
|
|
92
|
+
{index + 1}
|
|
93
|
+
</span>
|
|
94
|
+
{index < LIFECYCLE.length - 1 ? (
|
|
95
|
+
<span aria-hidden="true" className="w-px flex-1 bg-border" />
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="pb-8">
|
|
100
|
+
<p className="font-mono text-sm text-foreground">{step.where}</p>
|
|
101
|
+
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
|
|
102
|
+
{step.what}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ol>
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
<section className="mt-6">
|
|
111
|
+
<h2 className="font-semibold">Where things live</h2>
|
|
112
|
+
|
|
113
|
+
<Card className="mt-5 divide-y divide-border overflow-hidden">
|
|
114
|
+
{TREE.map(({ path, note }) => (
|
|
115
|
+
<div
|
|
116
|
+
key={path}
|
|
117
|
+
className="flex flex-col gap-1 px-5 py-3.5 sm:flex-row sm:items-baseline sm:gap-6"
|
|
118
|
+
>
|
|
119
|
+
<span className="shrink-0 font-mono text-sm text-primary sm:w-56">{path}</span>
|
|
120
|
+
<span className="text-sm text-muted-foreground">{note}</span>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</Card>
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<section className="mt-14 rounded-xl border border-border bg-accent/40 p-6 sm:p-8">
|
|
127
|
+
<h2 className="font-semibold">See a form do all of it</h2>
|
|
128
|
+
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
|
129
|
+
The contact page posts to a route, validates on the server, and comes back with either a
|
|
130
|
+
flash message or the errors and your original input — no client-side state to reconcile.
|
|
131
|
+
</p>
|
|
132
|
+
<ButtonLink href="/contact" className="mt-5">
|
|
133
|
+
Open the contact form
|
|
134
|
+
<ArrowRightIcon className="size-4" />
|
|
135
|
+
</ButtonLink>
|
|
136
|
+
</section>
|
|
137
|
+
</div>
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
(About as { layout?: (page: ReactNode) => ReactNode }).layout = (page) => (
|
|
143
|
+
<AppLayout>{page}</AppLayout>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
export default About;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { FormEvent, ReactNode } from "react";
|
|
2
|
+
import { Head, useForm, usePage } from "@inertiajs/react";
|
|
3
|
+
import AppLayout from "../Layouts/AppLayout";
|
|
4
|
+
import { Button } from "../Components/Button";
|
|
5
|
+
import { Card } from "../Components/Card";
|
|
6
|
+
import { Code } from "../Components/Code";
|
|
7
|
+
import { TextAreaField, TextField } from "../Components/Field";
|
|
8
|
+
import { MailIcon } from "../Components/Icons";
|
|
9
|
+
import type { SharedProps } from "../types";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A working form, end to end.
|
|
17
|
+
*
|
|
18
|
+
* Worth reading as a pattern rather than as content: the browser posts, the
|
|
19
|
+
* server validates, and a failure comes back as a redirect carrying `errors`
|
|
20
|
+
* and `old`. There is no client-side validation to keep in step with the
|
|
21
|
+
* server's rules, and no fetch/loading/error state to hand-roll — `useForm`
|
|
22
|
+
* exposes `processing` and reads the error bag off the page props.
|
|
23
|
+
*
|
|
24
|
+
* The matching route is app/routes/contact.ts.
|
|
25
|
+
*/
|
|
26
|
+
function Contact({ title }: Props) {
|
|
27
|
+
// Repopulate from the input the server flashed back after a failed validate().
|
|
28
|
+
const { old } = usePage<SharedProps>().props;
|
|
29
|
+
|
|
30
|
+
const { data, setData, post, processing, errors } = useForm({
|
|
31
|
+
name: typeof old["name"] === "string" ? old["name"] : "",
|
|
32
|
+
email: typeof old["email"] === "string" ? old["email"] : "",
|
|
33
|
+
message: typeof old["message"] === "string" ? old["message"] : "",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function submit(event: FormEvent) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
post("/contact");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<Head title={title} />
|
|
44
|
+
|
|
45
|
+
<div className="mx-auto max-w-4xl">
|
|
46
|
+
<header className="max-w-2xl">
|
|
47
|
+
<p className="font-mono text-xs tracking-[0.2em] text-muted-foreground uppercase">
|
|
48
|
+
Say hello
|
|
49
|
+
</p>
|
|
50
|
+
<h1 className="mt-3 text-3xl font-bold tracking-tight text-balance sm:text-4xl">
|
|
51
|
+
{title}
|
|
52
|
+
</h1>
|
|
53
|
+
<p className="mt-4 text-lg leading-relaxed text-pretty text-muted-foreground">
|
|
54
|
+
Submit it empty, or with a malformed address, and watch the server send the errors and
|
|
55
|
+
your input straight back.
|
|
56
|
+
</p>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<div className="mt-12 grid gap-6 lg:grid-cols-5">
|
|
60
|
+
<Card className="p-6 sm:p-8 lg:col-span-3">
|
|
61
|
+
<form onSubmit={submit} className="space-y-5" noValidate>
|
|
62
|
+
<TextField
|
|
63
|
+
label="Name"
|
|
64
|
+
name="name"
|
|
65
|
+
value={data.name}
|
|
66
|
+
error={errors.name}
|
|
67
|
+
autoComplete="name"
|
|
68
|
+
placeholder="Ada Lovelace"
|
|
69
|
+
onChange={(event) => setData("name", event.target.value)}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<TextField
|
|
73
|
+
label="Email"
|
|
74
|
+
name="email"
|
|
75
|
+
type="email"
|
|
76
|
+
value={data.email}
|
|
77
|
+
error={errors.email}
|
|
78
|
+
autoComplete="email"
|
|
79
|
+
placeholder="ada@example.com"
|
|
80
|
+
onChange={(event) => setData("email", event.target.value)}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<TextAreaField
|
|
84
|
+
label="Message"
|
|
85
|
+
name="message"
|
|
86
|
+
value={data.message}
|
|
87
|
+
error={errors.message}
|
|
88
|
+
hint="At least 10 characters."
|
|
89
|
+
placeholder="What's on your mind?"
|
|
90
|
+
onChange={(event) => setData("message", event.target.value)}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<div className="flex items-center gap-3 pt-1">
|
|
94
|
+
<Button type="submit" disabled={processing}>
|
|
95
|
+
{processing ? (
|
|
96
|
+
<span
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
99
|
+
/>
|
|
100
|
+
) : null}
|
|
101
|
+
{processing ? "Sending…" : "Send message"}
|
|
102
|
+
</Button>
|
|
103
|
+
<p className="text-sm text-muted-foreground">Nothing is stored — see the route.</p>
|
|
104
|
+
</div>
|
|
105
|
+
</form>
|
|
106
|
+
</Card>
|
|
107
|
+
|
|
108
|
+
<div className="space-y-6 lg:col-span-2">
|
|
109
|
+
<Card className="p-6">
|
|
110
|
+
<h2 className="font-semibold text-card-foreground">What happens on submit</h2>
|
|
111
|
+
<ol className="mt-4 space-y-3 text-sm leading-relaxed text-muted-foreground">
|
|
112
|
+
<li>
|
|
113
|
+
<span className="font-medium text-foreground">1.</span> The POST reaches{" "}
|
|
114
|
+
<Code>app/routes/contact.ts</Code>.
|
|
115
|
+
</li>
|
|
116
|
+
<li>
|
|
117
|
+
<span className="font-medium text-foreground">2.</span> <Code>validate()</Code>{" "}
|
|
118
|
+
checks the body and returns typed data.
|
|
119
|
+
</li>
|
|
120
|
+
<li>
|
|
121
|
+
<span className="font-medium text-foreground">3.</span> If it fails, errors and
|
|
122
|
+
input are flashed and you are redirected back.
|
|
123
|
+
</li>
|
|
124
|
+
<li>
|
|
125
|
+
<span className="font-medium text-foreground">4.</span> If it passes, a success
|
|
126
|
+
flash becomes the toast in the corner.
|
|
127
|
+
</li>
|
|
128
|
+
</ol>
|
|
129
|
+
</Card>
|
|
130
|
+
|
|
131
|
+
<Card className="p-6">
|
|
132
|
+
<h2 className="font-semibold text-card-foreground">Prefer email?</h2>
|
|
133
|
+
<a
|
|
134
|
+
href="mailto:hello@example.com"
|
|
135
|
+
className="mt-3 inline-flex items-center gap-2 text-sm font-medium text-primary transition-opacity hover:opacity-80"
|
|
136
|
+
>
|
|
137
|
+
<MailIcon className="size-4" />
|
|
138
|
+
hello@example.com
|
|
139
|
+
</a>
|
|
140
|
+
</Card>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
(Contact as { layout?: (page: ReactNode) => ReactNode }).layout = (page) => (
|
|
149
|
+
<AppLayout>{page}</AppLayout>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
export default Contact;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Head } from "@inertiajs/react";
|
|
3
|
+
import AppLayout from "../Layouts/AppLayout";
|
|
4
|
+
import { ButtonLink, buttonClass } from "../Components/Button";
|
|
5
|
+
import { DOCS_URL } from "../lib/site";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
status: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The page behind every HTTP error the app renders to a browser.
|
|
13
|
+
*
|
|
14
|
+
* `app/exceptions/Handler.ts` picks the status and renders this component, so a
|
|
15
|
+
* missing URL lands inside the app's own shell instead of on a bare framework
|
|
16
|
+
* page. In development the framework's stack-trace page still wins for 500s —
|
|
17
|
+
* this is what production shows.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const COPY: Record<number, { title: string; body: string }> = {
|
|
21
|
+
403: {
|
|
22
|
+
title: "Not allowed",
|
|
23
|
+
body: "You are signed in, but this page is not yours to see.",
|
|
24
|
+
},
|
|
25
|
+
404: {
|
|
26
|
+
title: "No page here",
|
|
27
|
+
body: "The URL does not match any route. Check the address, or head back to the start.",
|
|
28
|
+
},
|
|
29
|
+
419: {
|
|
30
|
+
title: "The page expired",
|
|
31
|
+
body: "Your session timed out before the form was submitted. Reload and try once more.",
|
|
32
|
+
},
|
|
33
|
+
429: {
|
|
34
|
+
title: "Too many requests",
|
|
35
|
+
body: "You have been rate limited. Give it a moment and try again.",
|
|
36
|
+
},
|
|
37
|
+
500: {
|
|
38
|
+
title: "Something broke",
|
|
39
|
+
body: "An error on our side stopped this request. It has been logged.",
|
|
40
|
+
},
|
|
41
|
+
503: {
|
|
42
|
+
title: "Down for maintenance",
|
|
43
|
+
body: "The app is briefly unavailable. It should be back shortly.",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const FALLBACK = {
|
|
48
|
+
title: "Something went wrong",
|
|
49
|
+
body: "That request could not be completed.",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function ErrorPage({ status }: Props) {
|
|
53
|
+
const { title, body } = COPY[status] ?? FALLBACK;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Head title={`${status} — ${title}`} />
|
|
58
|
+
|
|
59
|
+
<section className="mx-auto max-w-lg py-10 text-center sm:py-20">
|
|
60
|
+
<p
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
className="bg-linear-to-b from-foreground to-primary bg-clip-text font-mono text-7xl font-bold text-transparent tabular-nums sm:text-8xl"
|
|
63
|
+
>
|
|
64
|
+
{status}
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<h1 className="mt-6 text-2xl font-bold tracking-tight text-balance sm:text-3xl">{title}</h1>
|
|
68
|
+
|
|
69
|
+
<p className="mt-3 leading-relaxed text-pretty text-muted-foreground">{body}</p>
|
|
70
|
+
|
|
71
|
+
<div className="mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
|
72
|
+
<ButtonLink href="/">Back to home</ButtonLink>
|
|
73
|
+
<a
|
|
74
|
+
href={DOCS_URL}
|
|
75
|
+
target="_blank"
|
|
76
|
+
rel="noreferrer"
|
|
77
|
+
className={buttonClass("secondary", "md")}
|
|
78
|
+
>
|
|
79
|
+
Read the docs
|
|
80
|
+
</a>
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
(ErrorPage as { layout?: (page: ReactNode) => ReactNode }).layout = (page) => (
|
|
88
|
+
<AppLayout>{page}</AppLayout>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export default ErrorPage;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Head } from "@inertiajs/react";
|
|
3
|
+
import AppLayout from "../Layouts/AppLayout";
|
|
4
|
+
import { ButtonLink, buttonClass } from "../Components/Button";
|
|
5
|
+
import { Card, CardIcon } from "../Components/Card";
|
|
6
|
+
import { Code, Command } from "../Components/Code";
|
|
7
|
+
import {
|
|
8
|
+
ArrowRightIcon,
|
|
9
|
+
BoltIcon,
|
|
10
|
+
ClockIcon,
|
|
11
|
+
DatabaseIcon,
|
|
12
|
+
FolderIcon,
|
|
13
|
+
LayersIcon,
|
|
14
|
+
ShieldIcon,
|
|
15
|
+
TerminalIcon,
|
|
16
|
+
} from "../Components/Icons";
|
|
17
|
+
import { DOCS_URL } from "../lib/site";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
title: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const docs = (page: string) => `${DOCS_URL}/blob/main/docs/${page}`;
|
|
25
|
+
|
|
26
|
+
const FEATURES = [
|
|
27
|
+
{
|
|
28
|
+
icon: FolderIcon,
|
|
29
|
+
heading: "File-based routing",
|
|
30
|
+
body: (
|
|
31
|
+
<>
|
|
32
|
+
Every file under <Code>app/routes</Code> is a URL. Export a <Code>GET</Code> or{" "}
|
|
33
|
+
<Code>POST</Code> function and the route exists — there is no registration list to keep in
|
|
34
|
+
sync.
|
|
35
|
+
</>
|
|
36
|
+
),
|
|
37
|
+
href: docs("routing.md"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
icon: LayersIcon,
|
|
41
|
+
heading: "Pages, not endpoints",
|
|
42
|
+
body: (
|
|
43
|
+
<>
|
|
44
|
+
A route returns <Code>Inertia.render("home", props)</Code> and this component
|
|
45
|
+
receives them as React props. No API layer in between to build, version or keep in step.
|
|
46
|
+
</>
|
|
47
|
+
),
|
|
48
|
+
href: docs("inertia.md"),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
icon: ShieldIcon,
|
|
52
|
+
heading: "Validation that comes back",
|
|
53
|
+
body: (
|
|
54
|
+
<>
|
|
55
|
+
<Code>validate()</Code> returns typed data on success. On failure it flashes the errors and
|
|
56
|
+
the submitted input and sends the user back — the form repopulates itself.
|
|
57
|
+
</>
|
|
58
|
+
),
|
|
59
|
+
href: docs("validator.md"),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
icon: DatabaseIcon,
|
|
63
|
+
heading: "An ORM you can read",
|
|
64
|
+
body: (
|
|
65
|
+
<>
|
|
66
|
+
Models, relationships, migrations, factories, and a query builder that still looks like the
|
|
67
|
+
query you meant to write.
|
|
68
|
+
</>
|
|
69
|
+
),
|
|
70
|
+
href: docs("orm/index.md"),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
icon: ClockIcon,
|
|
74
|
+
heading: "Background work, built in",
|
|
75
|
+
body: (
|
|
76
|
+
<>
|
|
77
|
+
Push a job onto a queue or put it on a schedule. Same app, same types, one{" "}
|
|
78
|
+
<Code>bun zt worker</Code> away.
|
|
79
|
+
</>
|
|
80
|
+
),
|
|
81
|
+
href: docs("queue.md"),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
icon: TerminalIcon,
|
|
85
|
+
heading: "A CLI that writes the boilerplate",
|
|
86
|
+
body: (
|
|
87
|
+
<>
|
|
88
|
+
<Code>make:page</Code>, <Code>make:controller</Code>, <Code>route:list</Code>,{" "}
|
|
89
|
+
<Code>repl</Code> — and <Code>make:command</Code> when you need one of your own.
|
|
90
|
+
</>
|
|
91
|
+
),
|
|
92
|
+
href: docs("commands.md"),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const NEXT_STEPS = [
|
|
97
|
+
{ command: "bun zt route:list", note: "Every route this app registers, in one table." },
|
|
98
|
+
{ command: "bun zt make:page Dashboard", note: "Scaffold a page with its layout wired up." },
|
|
99
|
+
{ command: "bun zt test", note: "Run the suite with the application booted." },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function Home({ title, message }: Props) {
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
<Head title="Home" />
|
|
106
|
+
|
|
107
|
+
<section className="text-center">
|
|
108
|
+
<span className="inline-flex items-center gap-2.5 rounded-full border border-border bg-card px-3.5 py-1.5 text-sm font-medium text-muted-foreground">
|
|
109
|
+
<span className="relative flex size-2">
|
|
110
|
+
<span className="absolute inline-flex size-full animate-ping rounded-full bg-success opacity-60" />
|
|
111
|
+
<span className="relative inline-flex size-2 rounded-full bg-success" />
|
|
112
|
+
</span>
|
|
113
|
+
The install worked
|
|
114
|
+
</span>
|
|
115
|
+
|
|
116
|
+
<h1 className="mx-auto mt-6 max-w-3xl bg-linear-to-br from-foreground via-foreground to-primary bg-clip-text text-4xl font-bold tracking-tight text-balance text-transparent sm:text-6xl">
|
|
117
|
+
{title}
|
|
118
|
+
</h1>
|
|
119
|
+
|
|
120
|
+
<p className="mx-auto mt-5 max-w-xl text-lg leading-relaxed text-pretty text-muted-foreground">
|
|
121
|
+
{message}
|
|
122
|
+
</p>
|
|
123
|
+
|
|
124
|
+
<div className="mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
|
125
|
+
<ButtonLink href="/about" size="lg">
|
|
126
|
+
Take the tour
|
|
127
|
+
<ArrowRightIcon className="size-4" />
|
|
128
|
+
</ButtonLink>
|
|
129
|
+
<a
|
|
130
|
+
href={DOCS_URL}
|
|
131
|
+
target="_blank"
|
|
132
|
+
rel="noreferrer"
|
|
133
|
+
className={buttonClass("secondary", "lg")}
|
|
134
|
+
>
|
|
135
|
+
Read the docs
|
|
136
|
+
</a>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<p className="mt-8 text-sm text-muted-foreground">
|
|
140
|
+
Edit <Code>resources/js/pages/home.tsx</Code> and the page reloads itself.
|
|
141
|
+
</p>
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<section className="mt-20 sm:mt-28">
|
|
145
|
+
<h2 className="text-center font-mono text-xs tracking-[0.2em] text-muted-foreground uppercase">
|
|
146
|
+
What you already have
|
|
147
|
+
</h2>
|
|
148
|
+
|
|
149
|
+
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
150
|
+
{FEATURES.map(({ icon: Icon, heading, body, href }) => (
|
|
151
|
+
<Card key={heading} interactive className="group relative p-5">
|
|
152
|
+
<CardIcon className="transition-colors group-hover:border-primary/40">
|
|
153
|
+
<Icon className="size-5" />
|
|
154
|
+
</CardIcon>
|
|
155
|
+
|
|
156
|
+
<h3 className="mt-4 font-semibold text-card-foreground">
|
|
157
|
+
{/* The pseudo-element stretches this link over the whole card, so the
|
|
158
|
+
entire surface is clickable while its accessible name stays the heading. */}
|
|
159
|
+
<a
|
|
160
|
+
href={href}
|
|
161
|
+
target="_blank"
|
|
162
|
+
rel="noreferrer"
|
|
163
|
+
className="after:absolute after:inset-0 after:rounded-xl"
|
|
164
|
+
>
|
|
165
|
+
{heading}
|
|
166
|
+
</a>
|
|
167
|
+
</h3>
|
|
168
|
+
|
|
169
|
+
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
|
170
|
+
</Card>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
<section className="mt-20 sm:mt-28">
|
|
176
|
+
<div className="flex items-center gap-2.5">
|
|
177
|
+
<BoltIcon className="size-5 text-primary" />
|
|
178
|
+
<h2 className="font-semibold">Where to go next</h2>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="mt-5 grid gap-4 sm:grid-cols-3">
|
|
182
|
+
{NEXT_STEPS.map(({ command, note }) => (
|
|
183
|
+
<div key={command} className="space-y-2">
|
|
184
|
+
<Command>{command}</Command>
|
|
185
|
+
<p className="px-1 text-sm text-muted-foreground">{note}</p>
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
</section>
|
|
190
|
+
</>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Inertia persistent layout — preserved across client-side navigations.
|
|
195
|
+
(Home as { layout?: (page: ReactNode) => ReactNode }).layout = (page) => (
|
|
196
|
+
<AppLayout>{page}</AppLayout>
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
export default Home;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Auto-generated by @zerotal/inertia — do not edit manually.
|
|
2
|
+
// Regenerate with: bun zt inertia:build
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANT: These are dynamic import THUNKS, not static imports.
|
|
5
|
+
// Bun.build with splitting:true creates one .js chunk per page.
|
|
6
|
+
// Converting to static imports will bundle ALL pages into app.js.
|
|
7
|
+
|
|
8
|
+
export const pages: Record<string, () => Promise<{ default: unknown }>> = {
|
|
9
|
+
about: () => import("./pages/about.tsx"),
|
|
10
|
+
contact: () => import("./pages/contact.tsx"),
|
|
11
|
+
error: () => import("./pages/error.tsx"),
|
|
12
|
+
home: () => import("./pages/home.tsx"),
|
|
13
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props `@zerotal/inertia` merges into every page, so no controller has to pass
|
|
3
|
+
* them. Type `usePage<SharedProps>()` with this to read them safely.
|
|
4
|
+
*
|
|
5
|
+
* Add your own here after registering them with `Inertia.share(...)`.
|
|
6
|
+
*/
|
|
7
|
+
export interface SharedProps {
|
|
8
|
+
/** The signed-in user, or null. Populated once an auth provider is registered. */
|
|
9
|
+
auth: { user: { id: number; name: string; email: string } | null };
|
|
10
|
+
/** One-request-only messages set with `http.flash("success" | "error", …)`. */
|
|
11
|
+
flash: { success: string | null; error: string | null };
|
|
12
|
+
/** Field errors from a failed `validate()` call, keyed by field name. */
|
|
13
|
+
errors: Record<string, string>;
|
|
14
|
+
/** Submitted input from a failed `validate()`, so a form can repopulate itself. */
|
|
15
|
+
old: Record<string, unknown>;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|