create-swift-rust 0.3.1 → 0.7.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/dist/index.js +87 -881
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- 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 +101 -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 +29 -0
- package/templates/default/vercel.json +23 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { formatDate } from "@/lib/format";
|
|
2
|
+
import { BLUR } from "@/lib/blur";
|
|
3
|
+
import { getPost, posts } from "@/lib/posts";
|
|
4
|
+
import type { Metadata } from "swift-rust";
|
|
5
|
+
import Image from "swift-rust/image";
|
|
6
|
+
import { Link } from "swift-rust";
|
|
7
|
+
import { notFound } from "swift-rust/router";
|
|
8
|
+
|
|
9
|
+
export function generateStaticParams() {
|
|
10
|
+
return posts.map((p) => ({ slug: p.slug }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
|
|
14
|
+
const post = getPost(params.slug);
|
|
15
|
+
if (!post) return { title: "Not found" };
|
|
16
|
+
return {
|
|
17
|
+
title: post.title,
|
|
18
|
+
description: post.excerpt,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function BlogPostPage({ params }: { params: { slug: string } }) {
|
|
23
|
+
const post = getPost(params.slug);
|
|
24
|
+
if (!post) notFound();
|
|
25
|
+
|
|
26
|
+
const related = posts.filter((p) => p.slug !== post.slug).slice(0, 2);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<article className="container-prose py-16 sm:py-20">
|
|
30
|
+
<Link
|
|
31
|
+
href="/blog"
|
|
32
|
+
className="inline-flex items-center gap-1.5 text-[0.875rem] text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-fg)]"
|
|
33
|
+
>
|
|
34
|
+
<svg
|
|
35
|
+
viewBox="0 0 24 24"
|
|
36
|
+
className="h-3.5 w-3.5"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
strokeWidth="2"
|
|
40
|
+
>
|
|
41
|
+
<path d="M19 12H5M11 18l-6-6 6-6" strokeLinecap="round" strokeLinejoin="round" />
|
|
42
|
+
</svg>
|
|
43
|
+
All posts
|
|
44
|
+
</Link>
|
|
45
|
+
|
|
46
|
+
<header className="mt-10 border-b border-[var(--color-border)] pb-10">
|
|
47
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
48
|
+
{post.tags.map((tag) => (
|
|
49
|
+
<span key={tag} className="badge badge-accent">
|
|
50
|
+
{tag}
|
|
51
|
+
</span>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
<h1 className="mt-5 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
55
|
+
{post.title}
|
|
56
|
+
</h1>
|
|
57
|
+
<p className="mt-5 text-pretty text-lg leading-relaxed text-[var(--color-fg-muted)]">
|
|
58
|
+
{post.excerpt}
|
|
59
|
+
</p>
|
|
60
|
+
<div className="mt-8 flex items-center gap-3 text-[0.875rem] text-[var(--color-fg-subtle)]">
|
|
61
|
+
<span className="font-medium text-[var(--color-fg)]">{post.author.name}</span>
|
|
62
|
+
<span>·</span>
|
|
63
|
+
<time dateTime={post.date}>{formatDate(post.date)}</time>
|
|
64
|
+
<span>·</span>
|
|
65
|
+
<span>{post.readingTime}</span>
|
|
66
|
+
</div>
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
{post.cover ? (
|
|
70
|
+
<Image
|
|
71
|
+
src={post.cover}
|
|
72
|
+
alt={post.title}
|
|
73
|
+
width={1200}
|
|
74
|
+
height={630}
|
|
75
|
+
placeholder="blur"
|
|
76
|
+
blurDataURL={BLUR}
|
|
77
|
+
loader={({ src }) => src}
|
|
78
|
+
className="mt-10 aspect-[1200/630] w-full rounded-2xl border border-[var(--color-border)] object-cover"
|
|
79
|
+
/>
|
|
80
|
+
) : null}
|
|
81
|
+
|
|
82
|
+
<div className="prose prose-stone mt-10 max-w-none text-[1.0625rem] leading-[1.8] text-[var(--color-fg)]">
|
|
83
|
+
{post.body.split("\n\n").map((paragraph, i) => (
|
|
84
|
+
<p key={i} className="mb-6">
|
|
85
|
+
{paragraph}
|
|
86
|
+
</p>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{related.length > 0 ? (
|
|
91
|
+
<div className="mt-20 border-t border-[var(--color-border)] pt-12">
|
|
92
|
+
<h2 className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
93
|
+
Keep reading
|
|
94
|
+
</h2>
|
|
95
|
+
<ul className="mt-6 grid gap-px overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-border)] sm:grid-cols-2">
|
|
96
|
+
{related.map((p) => (
|
|
97
|
+
<li key={p.slug} className="bg-[var(--color-surface)]">
|
|
98
|
+
<Link
|
|
99
|
+
href={`/blog/${p.slug}`}
|
|
100
|
+
className="group block p-5 transition-colors hover:bg-[var(--color-surface-2)]"
|
|
101
|
+
>
|
|
102
|
+
<p className="text-[0.75rem] text-[var(--color-fg-subtle)]">
|
|
103
|
+
<time dateTime={p.date}>{formatDate(p.date)}</time>
|
|
104
|
+
</p>
|
|
105
|
+
<h3 className="mt-2 text-[1rem] font-semibold text-[var(--color-fg)] group-hover:text-[var(--color-accent)]">
|
|
106
|
+
{p.title}
|
|
107
|
+
</h3>
|
|
108
|
+
</Link>
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
</div>
|
|
113
|
+
) : null}
|
|
114
|
+
</article>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { formatDate } from "@/lib/format";
|
|
2
|
+
import { getAllTags, posts } from "@/lib/posts";
|
|
3
|
+
import type { Metadata } from "swift-rust";
|
|
4
|
+
import { Link } from "swift-rust";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: "Blog",
|
|
8
|
+
description: "Field notes, release announcements, and the occasional rant about bundlers.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function BlogIndexPage() {
|
|
12
|
+
const tags = getAllTags();
|
|
13
|
+
const [featured, ...rest] = posts;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="container-page py-16 sm:py-20">
|
|
17
|
+
<div className="mx-auto max-w-2xl">
|
|
18
|
+
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">Blog</h1>
|
|
19
|
+
<p className="mt-4 text-[var(--color-fg-muted)]">
|
|
20
|
+
Field notes, release announcements, and the occasional rant about bundlers.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{featured ? (
|
|
25
|
+
<Link
|
|
26
|
+
href={`/blog/${featured.slug}`}
|
|
27
|
+
className="group mt-16 grid gap-8 border-b border-[var(--color-border)] pb-16 lg:grid-cols-[1.3fr_1fr] lg:items-center"
|
|
28
|
+
>
|
|
29
|
+
<div>
|
|
30
|
+
<span className="badge badge-accent">Featured</span>
|
|
31
|
+
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-[var(--color-fg)] group-hover:text-[var(--color-accent)] sm:text-4xl">
|
|
32
|
+
{featured.title}
|
|
33
|
+
</h2>
|
|
34
|
+
<p className="mt-4 text-pretty leading-relaxed text-[var(--color-fg-muted)]">
|
|
35
|
+
{featured.excerpt}
|
|
36
|
+
</p>
|
|
37
|
+
<div className="mt-6 flex items-center gap-3 text-[0.8125rem] text-[var(--color-fg-subtle)]">
|
|
38
|
+
<span>{featured.author.name}</span>
|
|
39
|
+
<span>·</span>
|
|
40
|
+
<time dateTime={featured.date}>{formatDate(featured.date)}</time>
|
|
41
|
+
<span>·</span>
|
|
42
|
+
<span>{featured.readingTime}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="aspect-[4/3] overflow-hidden rounded-2xl border border-[var(--color-border)] bg-gradient-to-br from-[var(--color-accent-soft)] via-[var(--color-surface)] to-[var(--color-surface-2)]" />
|
|
46
|
+
</Link>
|
|
47
|
+
) : null}
|
|
48
|
+
|
|
49
|
+
<div className="mt-12 flex flex-wrap items-center gap-2">
|
|
50
|
+
<span className="text-[0.75rem] font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
51
|
+
Tags:
|
|
52
|
+
</span>
|
|
53
|
+
<Link
|
|
54
|
+
href="/blog"
|
|
55
|
+
className="badge hover:border-[var(--color-border-strong)] hover:bg-[var(--color-surface-2)]"
|
|
56
|
+
>
|
|
57
|
+
All
|
|
58
|
+
</Link>
|
|
59
|
+
{tags.map((tag) => (
|
|
60
|
+
<Link
|
|
61
|
+
key={tag}
|
|
62
|
+
href={`/blog/tag/${tag}`}
|
|
63
|
+
className="badge hover:border-[var(--color-border-strong)] hover:bg-[var(--color-surface-2)]"
|
|
64
|
+
>
|
|
65
|
+
{tag}
|
|
66
|
+
</Link>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<ul className="mt-12 grid gap-px overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-border)] sm:grid-cols-2">
|
|
71
|
+
{rest.map((post) => (
|
|
72
|
+
<li key={post.slug} className="bg-[var(--color-surface)]">
|
|
73
|
+
<Link
|
|
74
|
+
href={`/blog/${post.slug}`}
|
|
75
|
+
className="group flex h-full flex-col p-6 transition-colors hover:bg-[var(--color-surface-2)]"
|
|
76
|
+
>
|
|
77
|
+
<div className="flex items-center gap-2 text-[0.75rem] text-[var(--color-fg-subtle)]">
|
|
78
|
+
<time dateTime={post.date}>{formatDate(post.date)}</time>
|
|
79
|
+
<span>·</span>
|
|
80
|
+
<span>{post.readingTime}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<h3 className="mt-3 text-[1.125rem] font-semibold text-[var(--color-fg)] group-hover:text-[var(--color-accent)]">
|
|
83
|
+
{post.title}
|
|
84
|
+
</h3>
|
|
85
|
+
<p className="mt-2 flex-1 text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
|
|
86
|
+
{post.excerpt}
|
|
87
|
+
</p>
|
|
88
|
+
<div className="mt-6 flex flex-wrap gap-1.5">
|
|
89
|
+
{post.tags.map((tag) => (
|
|
90
|
+
<span key={tag} className="badge">
|
|
91
|
+
{tag}
|
|
92
|
+
</span>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</Link>
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { formatDate } from "@/lib/format";
|
|
2
|
+
import { getAllTags, getPostsByTag } 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 getAllTags().map((tag) => ({ tag }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function generateMetadata({ params }: { params: { tag: string } }): Metadata {
|
|
12
|
+
return { title: `Tag: ${params.tag}`, description: `Posts tagged ${params.tag}` };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function TagPage({ params }: { params: { tag: string } }) {
|
|
16
|
+
const tag = params.tag;
|
|
17
|
+
const tagged = getPostsByTag(tag);
|
|
18
|
+
if (tagged.length === 0) notFound();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="container-page py-16 sm:py-20">
|
|
22
|
+
<Link
|
|
23
|
+
href="/blog"
|
|
24
|
+
className="inline-flex items-center gap-1.5 text-[0.875rem] text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-fg)]"
|
|
25
|
+
>
|
|
26
|
+
<svg
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
className="h-3.5 w-3.5"
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
strokeWidth="2"
|
|
32
|
+
>
|
|
33
|
+
<path d="M19 12H5M11 18l-6-6 6-6" strokeLinecap="round" strokeLinejoin="round" />
|
|
34
|
+
</svg>
|
|
35
|
+
All posts
|
|
36
|
+
</Link>
|
|
37
|
+
<div className="mt-8">
|
|
38
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
39
|
+
Tag
|
|
40
|
+
</p>
|
|
41
|
+
<h1 className="mt-2 text-4xl font-semibold tracking-tight sm:text-5xl">{tag}</h1>
|
|
42
|
+
<p className="mt-3 text-[var(--color-fg-muted)]">
|
|
43
|
+
{tagged.length} {tagged.length === 1 ? "post" : "posts"}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<ul className="mt-12 divide-y divide-[var(--color-border)] border-y border-[var(--color-border)]">
|
|
48
|
+
{tagged.map((post) => (
|
|
49
|
+
<li key={post.slug}>
|
|
50
|
+
<Link
|
|
51
|
+
href={`/blog/${post.slug}`}
|
|
52
|
+
className="group flex items-start justify-between gap-6 py-6 transition-colors"
|
|
53
|
+
>
|
|
54
|
+
<div className="flex-1">
|
|
55
|
+
<div className="flex items-center gap-2 text-[0.75rem] text-[var(--color-fg-subtle)]">
|
|
56
|
+
<time dateTime={post.date}>{formatDate(post.date)}</time>
|
|
57
|
+
<span>·</span>
|
|
58
|
+
<span>{post.readingTime}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<h3 className="mt-2 text-[1.0625rem] font-semibold text-[var(--color-fg)] group-hover:text-[var(--color-accent)]">
|
|
61
|
+
{post.title}
|
|
62
|
+
</h3>
|
|
63
|
+
<p className="mt-1.5 text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
|
|
64
|
+
{post.excerpt}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<svg
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
className="mt-1 h-4 w-4 shrink-0 text-[var(--color-fg-subtle)] transition-all group-hover:translate-x-0.5 group-hover:text-[var(--color-fg)]"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
strokeWidth="2"
|
|
73
|
+
>
|
|
74
|
+
<path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
|
75
|
+
</svg>
|
|
76
|
+
</Link>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Metadata } from "swift-rust";
|
|
2
|
+
import { Link } from "swift-rust";
|
|
3
|
+
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Contact",
|
|
7
|
+
description: "Get in touch with the Swift Rust team.",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const CHANNELS = [
|
|
11
|
+
{
|
|
12
|
+
name: "GitHub",
|
|
13
|
+
handle: "colesites/swift-rust",
|
|
14
|
+
href: siteConfig.githubUrl,
|
|
15
|
+
description: "Issues, PRs, and discussion.",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "Discord",
|
|
19
|
+
handle: "discord.gg/swift-rust",
|
|
20
|
+
href: "https://discord.gg/swift-rust",
|
|
21
|
+
description: "Real-time help and announcements.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "Email",
|
|
25
|
+
handle: "hello@swift-rust.dev",
|
|
26
|
+
href: "mailto:hello@swift-rust.dev",
|
|
27
|
+
description: "Security reports and private inquiries.",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export default function ContactPage() {
|
|
32
|
+
return (
|
|
33
|
+
<div className="container-page py-16 sm:py-20">
|
|
34
|
+
<div className="grid gap-16 lg:grid-cols-[1fr_1fr]">
|
|
35
|
+
<div>
|
|
36
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
37
|
+
Contact
|
|
38
|
+
</p>
|
|
39
|
+
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
40
|
+
Let's talk.
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="mt-5 text-pretty text-lg leading-relaxed text-[var(--color-fg-muted)]">
|
|
43
|
+
Bug reports, feature requests, partnership ideas, or just to say hi. Pick a channel or
|
|
44
|
+
use the form.
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<ul className="mt-10 space-y-6">
|
|
48
|
+
{CHANNELS.map((c) => (
|
|
49
|
+
<li key={c.name} className="group">
|
|
50
|
+
<Link
|
|
51
|
+
href={c.href}
|
|
52
|
+
className="flex items-start justify-between gap-6 border-b border-[var(--color-border)] pb-6 transition-colors last:border-0"
|
|
53
|
+
>
|
|
54
|
+
<div>
|
|
55
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
56
|
+
{c.name}
|
|
57
|
+
</p>
|
|
58
|
+
<p className="mt-1 font-mono text-[0.9375rem] text-[var(--color-fg)] group-hover:text-[var(--color-accent)]">
|
|
59
|
+
{c.handle}
|
|
60
|
+
</p>
|
|
61
|
+
<p className="mt-1 text-[0.875rem] text-[var(--color-fg-muted)]">
|
|
62
|
+
{c.description}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
<svg
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
className="mt-1 h-4 w-4 shrink-0 text-[var(--color-fg-subtle)] transition-all group-hover:translate-x-0.5 group-hover:text-[var(--color-fg)]"
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
strokeWidth="2"
|
|
71
|
+
>
|
|
72
|
+
<path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
|
73
|
+
</svg>
|
|
74
|
+
</Link>
|
|
75
|
+
</li>
|
|
76
|
+
))}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div>
|
|
81
|
+
<form className="card p-6 sm:p-8">
|
|
82
|
+
<h2 className="text-[0.95rem] font-semibold">Send a message</h2>
|
|
83
|
+
<p className="mt-1 text-[0.8125rem] text-[var(--color-fg-muted)]">
|
|
84
|
+
We'll get back to you within a business day.
|
|
85
|
+
</p>
|
|
86
|
+
<div className="mt-6 space-y-5">
|
|
87
|
+
<div>
|
|
88
|
+
<label htmlFor="contact-name" className="block text-[0.8125rem] font-medium">
|
|
89
|
+
Name
|
|
90
|
+
</label>
|
|
91
|
+
<input id="contact-name" name="name" type="text" className="input mt-1.5" />
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<label htmlFor="contact-email" className="block text-[0.8125rem] font-medium">
|
|
95
|
+
Email
|
|
96
|
+
</label>
|
|
97
|
+
<input id="contact-email" name="email" type="email" className="input mt-1.5" />
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<label htmlFor="contact-subject" className="block text-[0.8125rem] font-medium">
|
|
101
|
+
Subject
|
|
102
|
+
</label>
|
|
103
|
+
<select id="contact-subject" name="subject" className="input mt-1.5">
|
|
104
|
+
<option>Bug report</option>
|
|
105
|
+
<option>Feature request</option>
|
|
106
|
+
<option>Partnership</option>
|
|
107
|
+
<option>Other</option>
|
|
108
|
+
</select>
|
|
109
|
+
</div>
|
|
110
|
+
<div>
|
|
111
|
+
<label htmlFor="contact-message" className="block text-[0.8125rem] font-medium">
|
|
112
|
+
Message
|
|
113
|
+
</label>
|
|
114
|
+
<textarea
|
|
115
|
+
id="contact-message"
|
|
116
|
+
name="message"
|
|
117
|
+
rows={5}
|
|
118
|
+
className="textarea mt-1.5"
|
|
119
|
+
placeholder="What's on your mind?"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
<button type="submit" className="btn btn-primary w-full">
|
|
123
|
+
Send message
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</form>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { formatNumber } from "@/lib/format";
|
|
2
|
+
import { trafficMax, trafficSeries } from "@/lib/stats";
|
|
3
|
+
import type { Metadata } from "swift-rust";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = { title: "Analytics" };
|
|
6
|
+
|
|
7
|
+
const SOURCES = [
|
|
8
|
+
{ name: "Direct", value: 412000, pct: 42 },
|
|
9
|
+
{ name: "Search", value: 281000, pct: 28 },
|
|
10
|
+
{ name: "Social", value: 175000, pct: 18 },
|
|
11
|
+
{ name: "Referral", value: 121000, pct: 12 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const PAGES = [
|
|
15
|
+
{ path: "/", views: 982000, avg: "1:42" },
|
|
16
|
+
{ path: "/blog/introducing-swift-rust", views: 218000, avg: "3:18" },
|
|
17
|
+
{ path: "/dashboard", views: 154000, avg: "2:05" },
|
|
18
|
+
{ path: "/fonts", views: 98000, avg: "0:48" },
|
|
19
|
+
{ path: "/blog/streaming-ssr-in-rust", views: 87000, avg: "4:12" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export default function AnalyticsPage() {
|
|
23
|
+
return (
|
|
24
|
+
<div className="px-6 py-10 sm:px-10">
|
|
25
|
+
<p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
|
|
26
|
+
Dashboard
|
|
27
|
+
</p>
|
|
28
|
+
<h1 className="mt-1 text-3xl font-semibold tracking-tight">Analytics</h1>
|
|
29
|
+
<p className="mt-1 text-[var(--color-fg-muted)]">Last 7 days, all projects combined.</p>
|
|
30
|
+
|
|
31
|
+
<div className="mt-8 card p-6">
|
|
32
|
+
<div className="flex items-center justify-between">
|
|
33
|
+
<h2 className="text-[0.95rem] font-semibold">Requests over time</h2>
|
|
34
|
+
<span className="text-[0.75rem] text-[var(--color-fg-subtle)]">
|
|
35
|
+
Total: {formatNumber(trafficSeries.reduce((s, d) => s + d.requests, 0))}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="mt-6 flex h-64 items-end gap-3">
|
|
39
|
+
{trafficSeries.map((d) => (
|
|
40
|
+
<div key={d.day} className="flex flex-1 flex-col items-center gap-2">
|
|
41
|
+
<div
|
|
42
|
+
className="w-full rounded-t-md bg-[var(--color-accent)] transition-opacity hover:opacity-80"
|
|
43
|
+
style={{ height: `${(d.requests / trafficMax) * 100}%` }}
|
|
44
|
+
title={`${d.requests.toLocaleString()} requests`}
|
|
45
|
+
/>
|
|
46
|
+
<span className="text-[0.75rem] font-medium text-[var(--color-fg-subtle)]">
|
|
47
|
+
{d.day}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="mt-6 grid gap-6 lg:grid-cols-2">
|
|
55
|
+
<div className="card p-6">
|
|
56
|
+
<h2 className="text-[0.95rem] font-semibold">Traffic sources</h2>
|
|
57
|
+
<ul className="mt-5 space-y-4">
|
|
58
|
+
{SOURCES.map((s) => (
|
|
59
|
+
<li key={s.name}>
|
|
60
|
+
<div className="flex items-center justify-between text-[0.875rem]">
|
|
61
|
+
<span className="font-medium">{s.name}</span>
|
|
62
|
+
<span className="font-mono text-[var(--color-fg-muted)]">
|
|
63
|
+
{formatNumber(s.value)}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-[var(--color-surface-2)]">
|
|
67
|
+
<div
|
|
68
|
+
className="h-full rounded-full bg-[var(--color-accent)]"
|
|
69
|
+
style={{ width: `${s.pct}%` }}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="card p-6">
|
|
78
|
+
<h2 className="text-[0.95rem] font-semibold">Top pages</h2>
|
|
79
|
+
<ul className="mt-5 divide-y divide-[var(--color-border)]">
|
|
80
|
+
{PAGES.map((p) => (
|
|
81
|
+
<li
|
|
82
|
+
key={p.path}
|
|
83
|
+
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
|
|
84
|
+
>
|
|
85
|
+
<span className="truncate font-mono text-[0.8125rem] text-[var(--color-fg)]">
|
|
86
|
+
{p.path}
|
|
87
|
+
</span>
|
|
88
|
+
<div className="flex items-center gap-6 text-[0.8125rem]">
|
|
89
|
+
<span className="font-mono text-[var(--color-fg-muted)]">
|
|
90
|
+
{formatNumber(p.views)}
|
|
91
|
+
</span>
|
|
92
|
+
<span className="w-12 text-right font-mono text-[var(--color-fg-subtle)]">
|
|
93
|
+
{p.avg}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Sidebar } from "@/components/sidebar";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex">
|
|
7
|
+
<Sidebar />
|
|
8
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function DashboardLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="px-6 py-10 sm:px-10">
|
|
4
|
+
<div className="h-4 w-16 animate-pulse rounded bg-[var(--color-surface-2)]" />
|
|
5
|
+
<div className="mt-2 h-9 w-72 animate-pulse rounded bg-[var(--color-surface-2)]" />
|
|
6
|
+
<div className="mt-2 h-4 w-96 animate-pulse rounded bg-[var(--color-surface-2)]" />
|
|
7
|
+
<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">
|
|
8
|
+
{[0, 1, 2, 3].map((i) => (
|
|
9
|
+
<div key={i} className="bg-[var(--color-surface)] p-6">
|
|
10
|
+
<div className="h-3 w-20 animate-pulse rounded bg-[var(--color-surface-2)]" />
|
|
11
|
+
<div className="mt-3 h-8 w-24 animate-pulse rounded bg-[var(--color-surface-2)]" />
|
|
12
|
+
<div className="mt-3 h-3 w-32 animate-pulse rounded bg-[var(--color-surface-2)]" />
|
|
13
|
+
</div>
|
|
14
|
+
))}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|