create-swift-rust 0.4.0 → 0.7.1
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/dist/index.js +74 -938
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/templates/default/README.md +29 -0
- package/templates/default/_env.example +5 -0
- package/templates/default/_env.local +5 -0
- package/templates/default/_gitignore +14 -0
- package/templates/default/biome.json +12 -0
- package/templates/default/globals.d.ts +19 -0
- package/templates/default/package.json +30 -0
- package/templates/default/postcss.config.mjs +7 -0
- package/templates/default/public/blog/introducing-swift-rust.svg +12 -0
- package/templates/default/public/og-image.svg +25 -0
- package/templates/default/public/samples/axelborg-towers-modern-architecture.jpg +0 -0
- package/templates/default/public/samples/brown-high-skyscrapers.jpg +0 -0
- package/templates/default/public/samples/close-up-portrait-gorgeous-young-woman.jpg +0 -0
- package/templates/default/public/samples/full-shot-woman-taking-selfie.jpg +0 -0
- package/templates/default/public/samples/happy-black-parents-with-kids-making-video-call-smart-phone-home.jpg +0 -0
- package/templates/default/public/samples/metaverse-concept-collage-design.jpg +0 -0
- package/templates/default/src/app/about/layout.tsx +5 -0
- package/templates/default/src/app/about/page.tsx +74 -0
- package/templates/default/src/app/about/team/page.tsx +88 -0
- package/templates/default/src/app/api/contact/route.ts +15 -0
- package/templates/default/src/app/api/health/route.ts +7 -0
- package/templates/default/src/app/api/posts/[id]/route.ts +33 -0
- package/templates/default/src/app/api/posts/route.ts +29 -0
- package/templates/default/src/app/api/search/route.ts +15 -0
- package/templates/default/src/app/api/stats/route.ts +16 -0
- package/templates/default/src/app/blog/[slug]/page.tsx +116 -0
- package/templates/default/src/app/blog/page.tsx +115 -0
- package/templates/default/src/app/blog/tag/[tag]/page.tsx +82 -0
- package/templates/default/src/app/contact/page.tsx +131 -0
- package/templates/default/src/app/dashboard/analytics/page.tsx +103 -0
- package/templates/default/src/app/dashboard/layout.tsx +11 -0
- package/templates/default/src/app/dashboard/loading.tsx +18 -0
- package/templates/default/src/app/dashboard/page.tsx +181 -0
- package/templates/default/src/app/dashboard/posts/[id]/not-found.tsx +23 -0
- package/templates/default/src/app/dashboard/posts/[id]/page.tsx +87 -0
- package/templates/default/src/app/dashboard/posts/new/page.tsx +103 -0
- package/templates/default/src/app/dashboard/posts/page.tsx +81 -0
- package/templates/default/src/app/dashboard/settings/page.tsx +119 -0
- package/templates/default/src/app/error.tsx +42 -0
- package/templates/default/src/app/favicon.ico +0 -0
- package/templates/default/src/app/favicon.svg +8 -0
- package/templates/default/src/app/fonts/page.tsx +610 -0
- package/templates/default/src/app/globals.css +238 -0
- package/templates/default/src/app/images/page.tsx +56 -0
- package/templates/default/src/app/layout.tsx +44 -0
- package/templates/default/src/app/loading.tsx +13 -0
- package/templates/default/src/app/not-found.tsx +25 -0
- package/templates/default/src/app/page.tsx +410 -0
- package/templates/default/src/app/videos/page.tsx +182 -0
- package/templates/default/src/components/font-studio.tsx +378 -0
- package/templates/default/src/components/footer.tsx +75 -0
- package/templates/default/src/components/logo.tsx +26 -0
- package/templates/default/src/components/nav.tsx +102 -0
- package/templates/default/src/components/sidebar.tsx +109 -0
- package/templates/default/src/lib/blur.ts +5 -0
- package/templates/default/src/lib/format.ts +31 -0
- package/templates/default/src/lib/posts.ts +132 -0
- package/templates/default/src/lib/site.config.ts +39 -0
- package/templates/default/src/lib/stats.ts +25 -0
- package/templates/default/swift-rust.config.json +8 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/vercel.json +23 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { formatNumber, formatPercent, relativeTime } from "@/lib/format";
|
|
2
|
+
import { posts } from "@/lib/posts";
|
|
3
|
+
import { stats, trafficMax, trafficSeries } from "@/lib/stats";
|
|
4
|
+
import type { Metadata } from "swift-rust";
|
|
5
|
+
import { Link } from "swift-rust";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = { title: "Dashboard" };
|
|
8
|
+
|
|
9
|
+
const ACTIVITY = [
|
|
10
|
+
{
|
|
11
|
+
who: "alex@swift-rust.dev",
|
|
12
|
+
what: "published",
|
|
13
|
+
target: "Streaming SSR in Rust",
|
|
14
|
+
when: "2026-06-01T08:32:00Z",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
who: "maria@swift-rust.dev",
|
|
18
|
+
what: "updated",
|
|
19
|
+
target: "Geist font spec",
|
|
20
|
+
when: "2026-05-31T15:10:00Z",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
who: "jordan@swift-rust.dev",
|
|
24
|
+
what: "deployed",
|
|
25
|
+
target: "v0.0.9-rc3",
|
|
26
|
+
when: "2026-05-31T09:45:00Z",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
who: "priya@swift-rust.dev",
|
|
30
|
+
what: "merged",
|
|
31
|
+
target: "feat: edge streaming",
|
|
32
|
+
when: "2026-05-30T18:22:00Z",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
who: "sam@swift-rust.dev",
|
|
36
|
+
what: "commented on",
|
|
37
|
+
target: "binary size regression",
|
|
38
|
+
when: "2026-05-30T11:08:00Z",
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export default function DashboardOverviewPage() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="px-6 py-10 sm:px-10">
|
|
45
|
+
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
46
|
+
<div>
|
|
47
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
48
|
+
Overview
|
|
49
|
+
</p>
|
|
50
|
+
<h1 className="mt-1 text-3xl font-semibold tracking-tight">Welcome back, Alex</h1>
|
|
51
|
+
<p className="mt-1 text-[var(--color-fg-muted)]">
|
|
52
|
+
Here's what happened across your projects in the last 30 days.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex items-center gap-2">
|
|
56
|
+
<Link href="/dashboard/posts/new" className="btn btn-outline btn-sm">
|
|
57
|
+
New post
|
|
58
|
+
</Link>
|
|
59
|
+
<Link href="/dashboard/settings" className="btn btn-primary btn-sm">
|
|
60
|
+
Settings
|
|
61
|
+
</Link>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="mt-10 grid gap-px overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-border)] sm:grid-cols-2 lg:grid-cols-4">
|
|
66
|
+
{stats.map((s) => (
|
|
67
|
+
<div key={s.label} className="bg-[var(--color-surface)] p-6">
|
|
68
|
+
<p className="text-[0.8125rem] text-[var(--color-fg-muted)]">{s.label}</p>
|
|
69
|
+
<p className="mt-2 text-3xl font-semibold tracking-tight">{s.value}</p>
|
|
70
|
+
<div className="mt-3 flex items-center gap-2 text-[0.75rem]">
|
|
71
|
+
<span
|
|
72
|
+
className={`inline-flex items-center gap-0.5 font-medium ${
|
|
73
|
+
s.delta >= 0 ? "text-[#16a34a]" : "text-[#dc2626]"
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
{s.delta >= 0 ? "↑" : "↓"} {formatPercent(Math.abs(s.delta))}
|
|
77
|
+
</span>
|
|
78
|
+
<span className="text-[var(--color-fg-subtle)]">{s.context}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="mt-6 grid gap-6 lg:grid-cols-[1.4fr_1fr]">
|
|
85
|
+
<div className="card p-6">
|
|
86
|
+
<div className="flex items-center justify-between">
|
|
87
|
+
<div>
|
|
88
|
+
<h2 className="text-[0.95rem] font-semibold">Traffic</h2>
|
|
89
|
+
<p className="text-[0.8125rem] text-[var(--color-fg-muted)]">Last 7 days</p>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex items-center gap-1 text-[0.75rem]">
|
|
92
|
+
<span className="badge badge-dot badge-success">Healthy</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="mt-6 flex h-48 items-end gap-2">
|
|
96
|
+
{trafficSeries.map((d) => (
|
|
97
|
+
<div key={d.day} className="flex flex-1 flex-col items-center gap-2">
|
|
98
|
+
<div
|
|
99
|
+
className="w-full rounded-t-md bg-[var(--color-accent)]/80 transition-colors hover:bg-[var(--color-accent)]"
|
|
100
|
+
style={{ height: `${(d.requests / trafficMax) * 100}%` }}
|
|
101
|
+
title={`${d.requests.toLocaleString()} requests`}
|
|
102
|
+
/>
|
|
103
|
+
<span className="text-[0.7rem] font-medium text-[var(--color-fg-subtle)]">
|
|
104
|
+
{d.day}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
<div className="mt-4 flex items-center justify-between border-t border-[var(--color-border)] pt-4 text-[0.8125rem]">
|
|
110
|
+
<span className="text-[var(--color-fg-muted)]">Total</span>
|
|
111
|
+
<span className="font-mono font-medium">
|
|
112
|
+
{formatNumber(trafficSeries.reduce((s, d) => s + d.requests, 0))}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="card p-6">
|
|
118
|
+
<h2 className="text-[0.95rem] font-semibold">Recent activity</h2>
|
|
119
|
+
<ul className="mt-4 -mx-2 divide-y divide-[var(--color-border)]">
|
|
120
|
+
{ACTIVITY.map((a, i) => (
|
|
121
|
+
<li key={i} className="flex items-start gap-3 px-2 py-3">
|
|
122
|
+
<div className="mt-0.5 h-2 w-2 shrink-0 rounded-full bg-[var(--color-accent)]" />
|
|
123
|
+
<div className="min-w-0 flex-1">
|
|
124
|
+
<p className="text-[0.875rem] leading-snug text-[var(--color-fg)]">
|
|
125
|
+
<span className="font-medium">{a.who}</span>{" "}
|
|
126
|
+
<span className="text-[var(--color-fg-muted)]">{a.what}</span>{" "}
|
|
127
|
+
<span className="font-medium">{a.target}</span>
|
|
128
|
+
</p>
|
|
129
|
+
<p className="mt-0.5 text-[0.75rem] text-[var(--color-fg-subtle)]">
|
|
130
|
+
{relativeTime(a.when)}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="mt-6 card p-6">
|
|
140
|
+
<div className="flex items-center justify-between">
|
|
141
|
+
<h2 className="text-[0.95rem] font-semibold">Latest posts</h2>
|
|
142
|
+
<Link
|
|
143
|
+
href="/dashboard/posts"
|
|
144
|
+
className="text-[0.8125rem] font-medium text-[var(--color-accent)] hover:text-[var(--color-accent-hover)]"
|
|
145
|
+
>
|
|
146
|
+
View all
|
|
147
|
+
</Link>
|
|
148
|
+
</div>
|
|
149
|
+
<table className="mt-4 w-full text-left text-[0.875rem]">
|
|
150
|
+
<thead>
|
|
151
|
+
<tr className="border-b border-[var(--color-border)] text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
152
|
+
<th className="pb-2 font-semibold">Title</th>
|
|
153
|
+
<th className="pb-2 font-semibold">Author</th>
|
|
154
|
+
<th className="pb-2 font-semibold">Status</th>
|
|
155
|
+
<th className="pb-2 text-right font-semibold">Date</th>
|
|
156
|
+
</tr>
|
|
157
|
+
</thead>
|
|
158
|
+
<tbody>
|
|
159
|
+
{posts.slice(0, 4).map((p) => (
|
|
160
|
+
<tr key={p.slug} className="border-b border-[var(--color-border)] last:border-0">
|
|
161
|
+
<td className="py-3 pr-4">
|
|
162
|
+
<Link
|
|
163
|
+
href={`/blog/${p.slug}`}
|
|
164
|
+
className="font-medium text-[var(--color-fg)] hover:text-[var(--color-accent)]"
|
|
165
|
+
>
|
|
166
|
+
{p.title}
|
|
167
|
+
</Link>
|
|
168
|
+
</td>
|
|
169
|
+
<td className="py-3 pr-4 text-[var(--color-fg-muted)]">{p.author.name}</td>
|
|
170
|
+
<td className="py-3 pr-4">
|
|
171
|
+
<span className="badge badge-success badge-dot">Published</span>
|
|
172
|
+
</td>
|
|
173
|
+
<td className="py-3 text-right text-[var(--color-fg-subtle)]">{p.date}</td>
|
|
174
|
+
</tr>
|
|
175
|
+
))}
|
|
176
|
+
</tbody>
|
|
177
|
+
</table>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Link } from "swift-rust";
|
|
2
|
+
|
|
3
|
+
export default function PostNotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mx-auto max-w-md px-6 py-20 text-center sm:px-10">
|
|
6
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
7
|
+
404
|
|
8
|
+
</p>
|
|
9
|
+
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Post not found</h1>
|
|
10
|
+
<p className="mt-3 text-[var(--color-fg-muted)]">
|
|
11
|
+
That post doesn't exist, was unpublished, or you don't have access.
|
|
12
|
+
</p>
|
|
13
|
+
<div className="mt-6 flex justify-center gap-2">
|
|
14
|
+
<Link href="/dashboard/posts" className="btn btn-primary btn-sm">
|
|
15
|
+
Back to posts
|
|
16
|
+
</Link>
|
|
17
|
+
<Link href="/dashboard" className="btn btn-outline btn-sm">
|
|
18
|
+
Dashboard
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { formatDate } from "@/lib/format";
|
|
2
|
+
import { getPost, posts } from "@/lib/posts";
|
|
3
|
+
import type { Metadata } from "swift-rust";
|
|
4
|
+
import { Link } from "swift-rust";
|
|
5
|
+
import { notFound } from "swift-rust/router";
|
|
6
|
+
|
|
7
|
+
export function generateStaticParams() {
|
|
8
|
+
return posts.map((p) => ({ id: p.slug }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function generateMetadata({ params }: { params: { id: string } }): Metadata {
|
|
12
|
+
const post = getPost(params.id);
|
|
13
|
+
if (!post) return { title: "Post not found" };
|
|
14
|
+
return { title: `Edit: ${post.title}` };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function DashboardPostPage({ params }: { params: { id: string } }) {
|
|
18
|
+
const post = getPost(params.id);
|
|
19
|
+
if (!post) notFound();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="mx-auto max-w-3xl px-6 py-10 sm:px-10">
|
|
23
|
+
<Link
|
|
24
|
+
href="/dashboard/posts"
|
|
25
|
+
className="inline-flex items-center gap-1.5 text-[0.875rem] text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
|
|
26
|
+
>
|
|
27
|
+
<svg
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
className="h-3.5 w-3.5"
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="2"
|
|
33
|
+
>
|
|
34
|
+
<path d="M19 12H5M11 18l-6-6 6-6" strokeLinecap="round" strokeLinejoin="round" />
|
|
35
|
+
</svg>
|
|
36
|
+
All posts
|
|
37
|
+
</Link>
|
|
38
|
+
|
|
39
|
+
<div className="mt-8 flex items-start justify-between gap-4">
|
|
40
|
+
<div>
|
|
41
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
42
|
+
Post
|
|
43
|
+
</p>
|
|
44
|
+
<h1 className="mt-1 text-3xl font-semibold tracking-tight">{post.title}</h1>
|
|
45
|
+
<p className="mt-2 text-[0.875rem] text-[var(--color-fg-muted)]">
|
|
46
|
+
By {post.author.name} · {formatDate(post.date)} · {post.readingTime}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<span className="badge badge-success badge-dot">Published</span>
|
|
51
|
+
<Link href={`/blog/${post.slug}`} className="btn btn-outline btn-sm">
|
|
52
|
+
View live
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="mt-8 card p-6">
|
|
58
|
+
<h2 className="text-[0.95rem] font-semibold">Excerpt</h2>
|
|
59
|
+
<p className="mt-2 text-[0.9375rem] leading-relaxed text-[var(--color-fg-muted)]">
|
|
60
|
+
{post.excerpt}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="mt-6 card p-6">
|
|
65
|
+
<h2 className="text-[0.95rem] font-semibold">Tags</h2>
|
|
66
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
67
|
+
{post.tags.map((t) => (
|
|
68
|
+
<span key={t} className="badge badge-accent">
|
|
69
|
+
{t}
|
|
70
|
+
</span>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="mt-6 card p-6">
|
|
76
|
+
<h2 className="text-[0.95rem] font-semibold">Body preview</h2>
|
|
77
|
+
<div className="mt-3 max-h-72 overflow-y-auto text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
|
|
78
|
+
{post.body.split("\n\n").map((p, i) => (
|
|
79
|
+
<p key={i} className="mb-3">
|
|
80
|
+
{p}
|
|
81
|
+
</p>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Metadata } from "swift-rust";
|
|
2
|
+
import { Link } from "swift-rust";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = { title: "New post" };
|
|
5
|
+
|
|
6
|
+
export default function NewPostPage() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="mx-auto max-w-2xl px-6 py-10 sm:px-10">
|
|
9
|
+
<Link
|
|
10
|
+
href="/dashboard/posts"
|
|
11
|
+
className="inline-flex items-center gap-1.5 text-[0.875rem] text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
|
|
12
|
+
>
|
|
13
|
+
<svg
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
className="h-3.5 w-3.5"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
>
|
|
20
|
+
<path d="M19 12H5M11 18l-6-6 6-6" strokeLinecap="round" strokeLinejoin="round" />
|
|
21
|
+
</svg>
|
|
22
|
+
All posts
|
|
23
|
+
</Link>
|
|
24
|
+
|
|
25
|
+
<h1 className="mt-6 text-3xl font-semibold tracking-tight">New post</h1>
|
|
26
|
+
<p className="mt-1 text-[var(--color-fg-muted)]">
|
|
27
|
+
Compose a draft. Save without publishing, or hit publish when you're ready.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<form className="mt-8 space-y-6">
|
|
31
|
+
<div>
|
|
32
|
+
<label
|
|
33
|
+
htmlFor="title"
|
|
34
|
+
className="block text-[0.8125rem] font-medium text-[var(--color-fg)]"
|
|
35
|
+
>
|
|
36
|
+
Title
|
|
37
|
+
</label>
|
|
38
|
+
<input
|
|
39
|
+
id="title"
|
|
40
|
+
name="title"
|
|
41
|
+
type="text"
|
|
42
|
+
placeholder="A title for your post"
|
|
43
|
+
className="input mt-2"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<label
|
|
48
|
+
htmlFor="slug"
|
|
49
|
+
className="block text-[0.8125rem] font-medium text-[var(--color-fg)]"
|
|
50
|
+
>
|
|
51
|
+
Slug
|
|
52
|
+
</label>
|
|
53
|
+
<input
|
|
54
|
+
id="slug"
|
|
55
|
+
name="slug"
|
|
56
|
+
type="text"
|
|
57
|
+
placeholder="a-slug-for-your-post"
|
|
58
|
+
className="input mt-2 font-mono"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<label
|
|
63
|
+
htmlFor="excerpt"
|
|
64
|
+
className="block text-[0.8125rem] font-medium text-[var(--color-fg)]"
|
|
65
|
+
>
|
|
66
|
+
Excerpt
|
|
67
|
+
</label>
|
|
68
|
+
<textarea
|
|
69
|
+
id="excerpt"
|
|
70
|
+
name="excerpt"
|
|
71
|
+
placeholder="A short summary that shows up on the blog index."
|
|
72
|
+
className="textarea mt-2"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<label
|
|
77
|
+
htmlFor="body"
|
|
78
|
+
className="block text-[0.8125rem] font-medium text-[var(--color-fg)]"
|
|
79
|
+
>
|
|
80
|
+
Body
|
|
81
|
+
</label>
|
|
82
|
+
<textarea
|
|
83
|
+
id="body"
|
|
84
|
+
name="body"
|
|
85
|
+
placeholder="Write your post here. Markdown is supported."
|
|
86
|
+
className="textarea mt-2 min-h-[16rem] font-mono"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex items-center justify-end gap-2 border-t border-[var(--color-border)] pt-6">
|
|
90
|
+
<Link href="/dashboard/posts" className="btn btn-outline">
|
|
91
|
+
Cancel
|
|
92
|
+
</Link>
|
|
93
|
+
<button type="button" className="btn btn-ghost">
|
|
94
|
+
Save draft
|
|
95
|
+
</button>
|
|
96
|
+
<button type="submit" className="btn btn-accent">
|
|
97
|
+
Publish
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</form>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { formatDate } from "@/lib/format";
|
|
2
|
+
import { posts } from "@/lib/posts";
|
|
3
|
+
import type { Metadata } from "swift-rust";
|
|
4
|
+
import { Link } from "swift-rust";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = { title: "Posts" };
|
|
7
|
+
|
|
8
|
+
export default function PostsPage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="px-6 py-10 sm:px-10">
|
|
11
|
+
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
12
|
+
<div>
|
|
13
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
14
|
+
Dashboard
|
|
15
|
+
</p>
|
|
16
|
+
<h1 className="mt-1 text-3xl font-semibold tracking-tight">Posts</h1>
|
|
17
|
+
<p className="mt-1 text-[var(--color-fg-muted)]">{posts.length} total</p>
|
|
18
|
+
</div>
|
|
19
|
+
<Link href="/dashboard/posts/new" className="btn btn-primary btn-sm">
|
|
20
|
+
<svg
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
className="h-4 w-4"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
strokeWidth="2"
|
|
26
|
+
>
|
|
27
|
+
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
|
28
|
+
</svg>
|
|
29
|
+
New post
|
|
30
|
+
</Link>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div className="mt-8 card overflow-hidden">
|
|
34
|
+
<table className="w-full text-left text-[0.875rem]">
|
|
35
|
+
<thead>
|
|
36
|
+
<tr className="border-b border-[var(--color-border)] bg-[var(--color-surface-2)] text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
37
|
+
<th className="px-5 py-3 font-semibold">Title</th>
|
|
38
|
+
<th className="px-5 py-3 font-semibold">Author</th>
|
|
39
|
+
<th className="px-5 py-3 font-semibold">Status</th>
|
|
40
|
+
<th className="px-5 py-3 font-semibold">Tags</th>
|
|
41
|
+
<th className="px-5 py-3 text-right font-semibold">Date</th>
|
|
42
|
+
</tr>
|
|
43
|
+
</thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
{posts.map((p) => (
|
|
46
|
+
<tr
|
|
47
|
+
key={p.slug}
|
|
48
|
+
className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-surface-2)]"
|
|
49
|
+
>
|
|
50
|
+
<td className="px-5 py-3.5">
|
|
51
|
+
<Link
|
|
52
|
+
href={`/dashboard/posts/${p.slug}`}
|
|
53
|
+
className="font-medium text-[var(--color-fg)] hover:text-[var(--color-accent)]"
|
|
54
|
+
>
|
|
55
|
+
{p.title}
|
|
56
|
+
</Link>
|
|
57
|
+
</td>
|
|
58
|
+
<td className="px-5 py-3.5 text-[var(--color-fg-muted)]">{p.author.name}</td>
|
|
59
|
+
<td className="px-5 py-3.5">
|
|
60
|
+
<span className="badge badge-success badge-dot">Published</span>
|
|
61
|
+
</td>
|
|
62
|
+
<td className="px-5 py-3.5">
|
|
63
|
+
<div className="flex flex-wrap gap-1">
|
|
64
|
+
{p.tags.map((t) => (
|
|
65
|
+
<span key={t} className="badge">
|
|
66
|
+
{t}
|
|
67
|
+
</span>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</td>
|
|
71
|
+
<td className="px-5 py-3.5 text-right text-[var(--color-fg-subtle)]">
|
|
72
|
+
<time dateTime={p.date}>{formatDate(p.date)}</time>
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
))}
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Metadata } from "swift-rust";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = { title: "Settings" };
|
|
4
|
+
|
|
5
|
+
export default function SettingsPage() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="mx-auto max-w-2xl px-6 py-10 sm:px-10">
|
|
8
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
9
|
+
Dashboard
|
|
10
|
+
</p>
|
|
11
|
+
<h1 className="mt-1 text-3xl font-semibold tracking-tight">Settings</h1>
|
|
12
|
+
<p className="mt-1 text-[var(--color-fg-muted)]">Manage your project preferences.</p>
|
|
13
|
+
|
|
14
|
+
<div className="mt-10 space-y-10">
|
|
15
|
+
<section>
|
|
16
|
+
<h2 className="text-[0.95rem] font-semibold">Profile</h2>
|
|
17
|
+
<p className="mt-1 text-[0.875rem] text-[var(--color-fg-muted)]">
|
|
18
|
+
Information that shows up on posts you author.
|
|
19
|
+
</p>
|
|
20
|
+
<div className="mt-5 space-y-4">
|
|
21
|
+
<div>
|
|
22
|
+
<label htmlFor="name" className="block text-[0.8125rem] font-medium">
|
|
23
|
+
Name
|
|
24
|
+
</label>
|
|
25
|
+
<input id="name" type="text" defaultValue="Alex Chen" className="input mt-1.5" />
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<label htmlFor="email" className="block text-[0.8125rem] font-medium">
|
|
29
|
+
Email
|
|
30
|
+
</label>
|
|
31
|
+
<input
|
|
32
|
+
id="email"
|
|
33
|
+
type="email"
|
|
34
|
+
defaultValue="alex@swift-rust.dev"
|
|
35
|
+
className="input mt-1.5"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<label htmlFor="bio" className="block text-[0.8125rem] font-medium">
|
|
40
|
+
Bio
|
|
41
|
+
</label>
|
|
42
|
+
<textarea
|
|
43
|
+
id="bio"
|
|
44
|
+
className="textarea mt-1.5"
|
|
45
|
+
defaultValue="Founding engineer at Swift Rust. Likes Rust, dislikes webpack."
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section className="border-t border-[var(--color-border)] pt-8">
|
|
52
|
+
<h2 className="text-[0.95rem] font-semibold">Rendering</h2>
|
|
53
|
+
<p className="mt-1 text-[0.875rem] text-[var(--color-fg-muted)]">
|
|
54
|
+
How the framework renders pages for this project.
|
|
55
|
+
</p>
|
|
56
|
+
<div className="mt-5 grid gap-2">
|
|
57
|
+
{[
|
|
58
|
+
{
|
|
59
|
+
id: "ssr-wasm",
|
|
60
|
+
label: "SSR + WASM",
|
|
61
|
+
hint: "Default. Server-rendered HTML, WASM-hydrated.",
|
|
62
|
+
},
|
|
63
|
+
{ id: "ssr", label: "SSR only", hint: "No client JavaScript." },
|
|
64
|
+
{ id: "ssr-htmx", label: "SSR + HTMX", hint: "Progressive enhancement." },
|
|
65
|
+
{ id: "wasm", label: "Full WASM SPA", hint: "Single-page app in WebAssembly." },
|
|
66
|
+
].map((opt, i) => (
|
|
67
|
+
<label
|
|
68
|
+
key={opt.id}
|
|
69
|
+
className="flex cursor-pointer items-start gap-3 rounded-lg border border-[var(--color-border)] p-4 transition-colors hover:bg-[var(--color-surface-2)] has-[:checked]:border-[var(--color-accent)] has-[:checked]:bg-[var(--color-accent-soft)]"
|
|
70
|
+
>
|
|
71
|
+
<input
|
|
72
|
+
type="radio"
|
|
73
|
+
name="renderer"
|
|
74
|
+
defaultChecked={i === 0}
|
|
75
|
+
className="mt-1 h-4 w-4 cursor-pointer accent-[var(--color-accent)]"
|
|
76
|
+
/>
|
|
77
|
+
<div>
|
|
78
|
+
<p className="font-medium">{opt.label}</p>
|
|
79
|
+
<p className="text-[0.8125rem] text-[var(--color-fg-muted)]">{opt.hint}</p>
|
|
80
|
+
</div>
|
|
81
|
+
</label>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<section className="border-t border-[var(--color-border)] pt-8">
|
|
87
|
+
<h2 className="text-[0.95rem] font-semibold">Danger zone</h2>
|
|
88
|
+
<p className="mt-1 text-[0.875rem] text-[var(--color-fg-muted)]">
|
|
89
|
+
Irreversible actions. Be sure before proceeding.
|
|
90
|
+
</p>
|
|
91
|
+
<div className="mt-5 flex items-center justify-between rounded-lg border border-[#fecaca] bg-[#fef2f2] p-4">
|
|
92
|
+
<div>
|
|
93
|
+
<p className="font-medium text-[#991b1b]">Delete project</p>
|
|
94
|
+
<p className="text-[0.8125rem] text-[#b91c1c]">
|
|
95
|
+
All posts, settings, and data will be permanently removed.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className="btn btn-sm"
|
|
101
|
+
style={{ background: "#dc2626", color: "white" }}
|
|
102
|
+
>
|
|
103
|
+
Delete
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<div className="flex justify-end gap-2 border-t border-[var(--color-border)] pt-8">
|
|
109
|
+
<button type="button" className="btn btn-outline">
|
|
110
|
+
Cancel
|
|
111
|
+
</button>
|
|
112
|
+
<button type="submit" className="btn btn-primary">
|
|
113
|
+
Save changes
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { Link } from "swift-rust";
|
|
4
|
+
|
|
5
|
+
export default function GlobalError({
|
|
6
|
+
error,
|
|
7
|
+
reset,
|
|
8
|
+
}: {
|
|
9
|
+
error: Error & { digest?: string };
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
console.error(error);
|
|
14
|
+
}, [error]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="container-page py-24">
|
|
18
|
+
<div className="mx-auto max-w-xl text-center">
|
|
19
|
+
<span className="badge badge-dot" style={{ background: "#fef2f2", color: "#dc2626" }}>
|
|
20
|
+
Runtime error
|
|
21
|
+
</span>
|
|
22
|
+
<h1 className="mt-5 text-4xl font-semibold tracking-tight">Something went wrong.</h1>
|
|
23
|
+
<p className="mt-4 text-[var(--color-fg-muted)]">
|
|
24
|
+
We logged the error. You can try again, or head back to the homepage.
|
|
25
|
+
</p>
|
|
26
|
+
{error.digest ? (
|
|
27
|
+
<p className="mt-2 font-mono text-[0.75rem] text-[var(--color-fg-subtle)]">
|
|
28
|
+
digest: {error.digest}
|
|
29
|
+
</p>
|
|
30
|
+
) : null}
|
|
31
|
+
<div className="mt-8 flex justify-center gap-2">
|
|
32
|
+
<button type="button" onClick={reset} className="btn btn-primary">
|
|
33
|
+
Try again
|
|
34
|
+
</button>
|
|
35
|
+
<Link href="/" className="btn btn-outline">
|
|
36
|
+
Home
|
|
37
|
+
</Link>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#0a0a0a"/>
|
|
3
|
+
<g stroke="#ffffff" stroke-width="1.9" stroke-linecap="round" fill="none">
|
|
4
|
+
<circle cx="16" cy="16" r="6.4"/>
|
|
5
|
+
<path d="M16 4.4v2.6M16 25v2.6M4.4 16h2.6M25 16h2.6M7.9 7.9l1.8 1.8M22.3 22.3l1.8 1.8M7.9 24.1l1.8-1.8M22.3 9.7l1.8-1.8"/>
|
|
6
|
+
</g>
|
|
7
|
+
<path d="M17.8 9.3 L11.6 17 H15.2 L14.2 22.8 L20.4 15 H16.8 Z" fill="#fb923c"/>
|
|
8
|
+
</svg>
|