create-frootkit 1.1.4 → 1.2.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/README.md +50 -16
- package/dist/addons/database.d.ts +3 -2
- package/dist/addons/database.d.ts.map +1 -1
- package/dist/addons/database.js +472 -13
- package/dist/addons/database.js.map +1 -1
- package/dist/index.js +93 -18
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts +1 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +3 -3
- package/dist/scaffold.js.map +1 -1
- package/dist/template.d.ts +4 -5
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +6 -404
- package/dist/template.js.map +1 -1
- package/dist/templates/auth.d.ts +3 -0
- package/dist/templates/auth.d.ts.map +1 -0
- package/dist/templates/auth.js +487 -0
- package/dist/templates/auth.js.map +1 -0
- package/dist/templates/blog.d.ts +3 -0
- package/dist/templates/blog.d.ts.map +1 -0
- package/dist/templates/blog.js +369 -0
- package/dist/templates/blog.js.map +1 -0
- package/dist/templates/common.d.ts +5 -0
- package/dist/templates/common.d.ts.map +1 -0
- package/dist/templates/common.js +116 -0
- package/dist/templates/common.js.map +1 -0
- package/dist/templates/ecommerce.d.ts +3 -0
- package/dist/templates/ecommerce.d.ts.map +1 -0
- package/dist/templates/ecommerce.js +474 -0
- package/dist/templates/ecommerce.js.map +1 -0
- package/dist/templates/empty.d.ts +3 -0
- package/dist/templates/empty.d.ts.map +1 -0
- package/dist/templates/empty.js +85 -0
- package/dist/templates/empty.js.map +1 -0
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +100 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/starter.d.ts +3 -0
- package/dist/templates/starter.d.ts.map +1 -0
- package/dist/templates/starter.js +308 -0
- package/dist/templates/starter.js.map +1 -0
- package/dist/templates/types.d.ts +13 -0
- package/dist/templates/types.d.ts.map +1 -0
- package/dist/templates/types.js +5 -0
- package/dist/templates/types.js.map +1 -0
- package/package.json +2 -1
- package/templates/auth/README.md +11 -0
- package/templates/auth/package.json +24 -0
- package/templates/auth/src/app/api/auth/route.ts +148 -0
- package/templates/auth/src/app/dashboard/layout.tsx +34 -0
- package/templates/auth/src/app/dashboard/page.tsx +72 -0
- package/templates/auth/src/app/layout.tsx +31 -0
- package/templates/auth/src/app/login/page.tsx +18 -0
- package/templates/auth/src/app/page.tsx +60 -0
- package/templates/auth/src/app/signup/page.tsx +18 -0
- package/templates/auth/src/components/AuthForm.tsx +77 -0
- package/templates/auth/src/components/DashboardNav.tsx +51 -0
- package/templates/auth/src/components/MetricCard.tsx +26 -0
- package/templates/auth/src/components/Navbar.tsx +29 -0
- package/templates/auth/src/lib/auth.ts +145 -0
- package/templates/auth/src/styles.css +19 -0
- package/templates/auth/tsconfig.json +13 -0
- package/templates/blog/README.md +11 -0
- package/templates/blog/package.json +24 -0
- package/templates/blog/src/app/api/posts/route.ts +36 -0
- package/templates/blog/src/app/blog/[slug]/page.tsx +74 -0
- package/templates/blog/src/app/layout.tsx +31 -0
- package/templates/blog/src/app/page.tsx +44 -0
- package/templates/blog/src/components/Footer.tsx +16 -0
- package/templates/blog/src/components/Navbar.tsx +31 -0
- package/templates/blog/src/components/Newsletter.tsx +32 -0
- package/templates/blog/src/components/PostCard.tsx +47 -0
- package/templates/blog/src/data/posts.ts +75 -0
- package/templates/blog/src/styles.css +19 -0
- package/templates/blog/tsconfig.json +13 -0
- package/templates/ecommerce/README.md +11 -0
- package/templates/ecommerce/package.json +24 -0
- package/templates/ecommerce/src/app/api/checkout/route.ts +19 -0
- package/templates/ecommerce/src/app/api/products/route.ts +28 -0
- package/templates/ecommerce/src/app/cart/page.tsx +22 -0
- package/templates/ecommerce/src/app/layout.tsx +31 -0
- package/templates/ecommerce/src/app/page.tsx +59 -0
- package/templates/ecommerce/src/app/products/[id]/page.tsx +88 -0
- package/templates/ecommerce/src/components/CartDrawer.tsx +64 -0
- package/templates/ecommerce/src/components/Footer.tsx +16 -0
- package/templates/ecommerce/src/components/Navbar.tsx +32 -0
- package/templates/ecommerce/src/components/ProductCard.tsx +52 -0
- package/templates/ecommerce/src/data/products.ts +87 -0
- package/templates/ecommerce/src/styles.css +19 -0
- package/templates/ecommerce/tsconfig.json +13 -0
- package/templates/empty/README.md +10 -0
- package/templates/empty/package.json +24 -0
- package/templates/empty/src/app/layout.tsx +29 -0
- package/templates/empty/src/app/page.tsx +38 -0
- package/templates/empty/src/styles.css +19 -0
- package/templates/empty/tsconfig.json +13 -0
- package/templates/starter/README.md +22 -0
- package/templates/starter/package.json +24 -0
- package/templates/starter/src/app/api/health/route.ts +23 -0
- package/templates/starter/src/app/layout.tsx +31 -0
- package/templates/starter/src/app/page.tsx +122 -0
- package/templates/starter/src/components/FeatureCard.tsx +25 -0
- package/templates/starter/src/components/Footer.tsx +16 -0
- package/templates/starter/src/components/Header.tsx +45 -0
- package/templates/starter/src/components/TerminalPreview.tsx +56 -0
- package/templates/starter/src/styles.css +19 -0
- package/templates/starter/tsconfig.json +13 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { POSTS } from '../../../data/posts.js';
|
|
2
|
+
|
|
3
|
+
export async function GET(req: Request): Promise<Response> {
|
|
4
|
+
const url = new URL(req.url);
|
|
5
|
+
const tag = url.searchParams.get('tag');
|
|
6
|
+
const q = url.searchParams.get('q')?.toLowerCase();
|
|
7
|
+
|
|
8
|
+
let results = [...POSTS];
|
|
9
|
+
|
|
10
|
+
if (tag) {
|
|
11
|
+
results = results.filter((p) => p.tags.some((t) => t.toLowerCase() === tag.toLowerCase()));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (q) {
|
|
15
|
+
results = results.filter(
|
|
16
|
+
(p) =>
|
|
17
|
+
p.title.toLowerCase().includes(q) ||
|
|
18
|
+
p.summary.toLowerCase().includes(q) ||
|
|
19
|
+
p.category.toLowerCase().includes(q)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Response.json(
|
|
24
|
+
{
|
|
25
|
+
posts: results,
|
|
26
|
+
total: results.length,
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
status: 200,
|
|
31
|
+
headers: {
|
|
32
|
+
'content-type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { POSTS } from '../../../data/posts.js';
|
|
2
|
+
import { Navbar } from '../../../components/Navbar.js';
|
|
3
|
+
import { Footer } from '../../../components/Footer.js';
|
|
4
|
+
|
|
5
|
+
export async function generateStaticParams() {
|
|
6
|
+
return POSTS.map((post) => ({ slug: post.slug }));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function BlogPostPage({ params }: { params?: { slug?: string } }) {
|
|
10
|
+
const slug = params?.slug || 'welcome-to-frootkit';
|
|
11
|
+
const post = POSTS.find((p) => p.slug === slug) || POSTS[0]!;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div class="min-h-screen flex flex-col justify-between">
|
|
15
|
+
<Navbar />
|
|
16
|
+
|
|
17
|
+
<main class="max-w-3xl mx-auto px-6 py-12 flex-1 w-full">
|
|
18
|
+
<a
|
|
19
|
+
href="/"
|
|
20
|
+
class="inline-flex items-center gap-2 text-sm text-slate-400 hover:text-emerald-400 mb-8 transition-colors"
|
|
21
|
+
>
|
|
22
|
+
← Back to all articles
|
|
23
|
+
</a>
|
|
24
|
+
|
|
25
|
+
<header class="mb-10">
|
|
26
|
+
<div class="flex items-center gap-3 mb-4 text-xs font-mono">
|
|
27
|
+
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-medium">
|
|
28
|
+
{post.category}
|
|
29
|
+
</span>
|
|
30
|
+
<span class="text-slate-500">•</span>
|
|
31
|
+
<span class="text-slate-400">{post.readTime}</span>
|
|
32
|
+
<span class="text-slate-500">•</span>
|
|
33
|
+
<span class="text-slate-400">{post.date}</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<h1 class="text-3xl sm:text-5xl font-extrabold text-white tracking-tight mb-6 leading-tight">
|
|
37
|
+
{post.title}
|
|
38
|
+
</h1>
|
|
39
|
+
|
|
40
|
+
<div class="flex items-center gap-4 p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
|
|
41
|
+
<img
|
|
42
|
+
src={post.author.avatar}
|
|
43
|
+
alt={post.author.name}
|
|
44
|
+
class="w-12 h-12 rounded-full object-cover ring-2 ring-emerald-500/30"
|
|
45
|
+
/>
|
|
46
|
+
<div>
|
|
47
|
+
<div class="font-bold text-white text-sm">{post.author.name}</div>
|
|
48
|
+
<div class="text-xs text-slate-400">{post.author.role}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
<article class="prose prose-invert prose-emerald max-w-none space-y-6 text-slate-300 leading-relaxed text-base sm:text-lg">
|
|
54
|
+
{post.content.map((paragraph, idx) => (
|
|
55
|
+
<p key={idx}>{paragraph}</p>
|
|
56
|
+
))}
|
|
57
|
+
</article>
|
|
58
|
+
|
|
59
|
+
<div class="mt-12 pt-8 border-t border-slate-800/80 flex flex-wrap gap-2">
|
|
60
|
+
{post.tags.map((tag) => (
|
|
61
|
+
<span
|
|
62
|
+
key={tag}
|
|
63
|
+
class="px-3 py-1 rounded-lg bg-slate-900 text-xs text-slate-300 border border-slate-800"
|
|
64
|
+
>
|
|
65
|
+
#{tag}
|
|
66
|
+
</span>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</main>
|
|
70
|
+
|
|
71
|
+
<Footer />
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
|
|
3
|
+
export interface RootLayoutProps {
|
|
4
|
+
children: ComponentChildren;
|
|
5
|
+
metadata?: {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function RootLayout({ children, metadata }: RootLayoutProps) {
|
|
12
|
+
const title = metadata?.title || 'FrootBlog — Insights on Modern Web Performance';
|
|
13
|
+
const description =
|
|
14
|
+
metadata?.description ||
|
|
15
|
+
'High-performance engineering articles and tutorials powered by Preact SSR and FrootKit.';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" class="dark scroll-smooth">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charSet="UTF-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
22
|
+
<title>{title}</title>
|
|
23
|
+
<meta name="description" content={description} />
|
|
24
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
25
|
+
</head>
|
|
26
|
+
<body class="bg-[#030712] text-slate-100 min-h-screen antialiased selection:bg-emerald-500/20 selection:text-emerald-300">
|
|
27
|
+
{children}
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { POSTS } from '../data/posts.js';
|
|
2
|
+
import { Navbar } from '../components/Navbar.js';
|
|
3
|
+
import { PostCard } from '../components/PostCard.js';
|
|
4
|
+
import { Newsletter } from '../components/Newsletter.js';
|
|
5
|
+
import { Footer } from '../components/Footer.js';
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
title: 'FrootBlog — Insights on Modern Web Performance',
|
|
9
|
+
description: 'Deep dives into Preact SSR, fullstack TypeScript, and native styling.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function BlogHomePage() {
|
|
13
|
+
return (
|
|
14
|
+
<div class="min-h-screen flex flex-col justify-between">
|
|
15
|
+
<Navbar />
|
|
16
|
+
|
|
17
|
+
<main class="max-w-5xl mx-auto px-6 py-12 flex-1 w-full">
|
|
18
|
+
{/* Header Banner */}
|
|
19
|
+
<div class="mb-12 text-center sm:text-left">
|
|
20
|
+
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-xs font-semibold text-emerald-400 mb-4">
|
|
21
|
+
Engineering Journal
|
|
22
|
+
</div>
|
|
23
|
+
<h1 class="text-4xl sm:text-5xl font-black text-white tracking-tight mb-4">
|
|
24
|
+
Stories, Architecture & Tutorials
|
|
25
|
+
</h1>
|
|
26
|
+
<p class="text-slate-400 text-base sm:text-lg max-w-2xl">
|
|
27
|
+
Exploring the bleeding edge of sub-millisecond server rendering, zero-overhead client apps, and modern developer velocity.
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{/* Post Grid */}
|
|
32
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
33
|
+
{POSTS.map((post) => (
|
|
34
|
+
<PostCard key={post.slug} post={post} />
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<Newsletter />
|
|
39
|
+
</main>
|
|
40
|
+
|
|
41
|
+
<Footer />
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function Footer() {
|
|
2
|
+
return (
|
|
3
|
+
<footer class="border-t border-slate-900 bg-slate-950/60 py-8 text-center text-xs text-slate-500">
|
|
4
|
+
<div class="max-w-5xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
5
|
+
<div>
|
|
6
|
+
FrootBlog © {new Date().getFullYear()} OrbitNorth LLC. MIT Licensed.
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex items-center gap-6">
|
|
9
|
+
<a href="https://github.com/orbitnorth/frootkit" class="hover:text-slate-400 transition-colors">GitHub</a>
|
|
10
|
+
<a href="https://frootjs.org" class="hover:text-slate-400 transition-colors">Documentation</a>
|
|
11
|
+
<a href="/api/posts" class="hover:text-slate-400 transition-colors">Posts API</a>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</footer>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function Navbar() {
|
|
2
|
+
return (
|
|
3
|
+
<header class="border-b border-slate-800/80 bg-slate-950/60 backdrop-blur-md sticky top-0 z-50">
|
|
4
|
+
<div class="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
|
|
5
|
+
<a href="/" class="flex items-center gap-3 font-extrabold text-white text-lg tracking-tight hover:opacity-90">
|
|
6
|
+
<span class="w-8 h-8 rounded-xl bg-gradient-to-tr from-emerald-500 to-cyan-400 flex items-center justify-center text-slate-950 font-black text-sm">
|
|
7
|
+
F
|
|
8
|
+
</span>
|
|
9
|
+
<span>Froot<span class="text-emerald-400">Blog</span></span>
|
|
10
|
+
</a>
|
|
11
|
+
|
|
12
|
+
<nav class="flex items-center gap-6 text-sm font-medium">
|
|
13
|
+
<a href="/" class="text-slate-300 hover:text-white transition-colors">
|
|
14
|
+
Articles
|
|
15
|
+
</a>
|
|
16
|
+
<a href="/api/posts" class="text-slate-400 hover:text-emerald-400 transition-colors font-mono text-xs px-2 py-1 rounded bg-slate-900 border border-slate-800">
|
|
17
|
+
/api/posts
|
|
18
|
+
</a>
|
|
19
|
+
<a
|
|
20
|
+
href="https://github.com/orbitnorth/frootkit"
|
|
21
|
+
target="_blank"
|
|
22
|
+
rel="noreferrer"
|
|
23
|
+
class="text-slate-300 hover:text-white transition-colors"
|
|
24
|
+
>
|
|
25
|
+
GitHub
|
|
26
|
+
</a>
|
|
27
|
+
</nav>
|
|
28
|
+
</div>
|
|
29
|
+
</header>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function Newsletter() {
|
|
2
|
+
return (
|
|
3
|
+
<div class="rounded-3xl border border-emerald-500/20 bg-gradient-to-b from-emerald-950/20 to-slate-900/40 p-8 sm:p-12 text-center my-16 backdrop-blur-sm">
|
|
4
|
+
<div class="max-w-xl mx-auto">
|
|
5
|
+
<h3 class="text-2xl sm:text-3xl font-bold text-white mb-3">
|
|
6
|
+
Subscribe to the FrootKit Dispatch
|
|
7
|
+
</h3>
|
|
8
|
+
<p class="text-slate-400 text-sm mb-8">
|
|
9
|
+
Weekly deep-dives into modern web architectures, performance optimization, and fullstack TypeScript.
|
|
10
|
+
</p>
|
|
11
|
+
<form
|
|
12
|
+
action="/api/posts"
|
|
13
|
+
method="GET"
|
|
14
|
+
class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto"
|
|
15
|
+
>
|
|
16
|
+
<input
|
|
17
|
+
type="email"
|
|
18
|
+
placeholder="colleague@company.com"
|
|
19
|
+
required
|
|
20
|
+
class="flex-1 px-4 py-3 rounded-xl bg-slate-950 border border-slate-800 text-white text-sm focus:outline-none focus:border-emerald-500"
|
|
21
|
+
/>
|
|
22
|
+
<button
|
|
23
|
+
type="submit"
|
|
24
|
+
class="px-6 py-3 rounded-xl bg-emerald-500 text-slate-950 font-bold hover:bg-emerald-400 transition-colors text-sm"
|
|
25
|
+
>
|
|
26
|
+
Subscribe
|
|
27
|
+
</button>
|
|
28
|
+
</form>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Post } from '../data/posts.js';
|
|
2
|
+
|
|
3
|
+
export function PostCard({ post }: { post: Post }) {
|
|
4
|
+
return (
|
|
5
|
+
<article class="rounded-2xl border border-slate-800/80 bg-slate-900/30 p-6 backdrop-blur-sm hover:border-emerald-500/40 transition-all hover:-translate-y-1 group flex flex-col justify-between">
|
|
6
|
+
<div>
|
|
7
|
+
<div class="flex items-center justify-between mb-3 text-xs">
|
|
8
|
+
<span class="font-mono text-emerald-400 font-medium px-2.5 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
|
|
9
|
+
{post.category}
|
|
10
|
+
</span>
|
|
11
|
+
<span class="text-slate-400 font-mono">{post.readTime}</span>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<h2 class="text-xl font-bold text-white mb-3 group-hover:text-emerald-400 transition-colors line-clamp-2">
|
|
15
|
+
<a href={`/blog/${post.slug}`}>
|
|
16
|
+
{post.title}
|
|
17
|
+
</a>
|
|
18
|
+
</h2>
|
|
19
|
+
|
|
20
|
+
<p class="text-slate-300 text-sm leading-relaxed mb-6 line-clamp-3">
|
|
21
|
+
{post.summary}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="pt-4 border-t border-slate-800/60 flex items-center justify-between">
|
|
26
|
+
<div class="flex items-center gap-3">
|
|
27
|
+
<img
|
|
28
|
+
src={post.author.avatar}
|
|
29
|
+
alt={post.author.name}
|
|
30
|
+
class="w-7 h-7 rounded-full object-cover ring-1 ring-slate-700"
|
|
31
|
+
/>
|
|
32
|
+
<div class="text-xs">
|
|
33
|
+
<span class="font-medium text-slate-200 block">{post.author.name}</span>
|
|
34
|
+
<span class="text-slate-500">{post.date}</span>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<a
|
|
39
|
+
href={`/blog/${post.slug}`}
|
|
40
|
+
class="text-emerald-400 text-xs font-semibold hover:underline flex items-center gap-1 group-hover:translate-x-0.5 transition-transform"
|
|
41
|
+
>
|
|
42
|
+
Read →
|
|
43
|
+
</a>
|
|
44
|
+
</div>
|
|
45
|
+
</article>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface Author {
|
|
2
|
+
name: string;
|
|
3
|
+
role: string;
|
|
4
|
+
avatar: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Post {
|
|
8
|
+
slug: string;
|
|
9
|
+
title: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
date: string;
|
|
12
|
+
readTime: string;
|
|
13
|
+
category: string;
|
|
14
|
+
author: Author;
|
|
15
|
+
tags: string[];
|
|
16
|
+
content: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const POSTS: Post[] = [
|
|
20
|
+
{
|
|
21
|
+
slug: 'welcome-to-frootkit',
|
|
22
|
+
title: 'Introducing FrootKit: High-Performance Next.js Alternative',
|
|
23
|
+
summary: 'Why we combined Preact SSR, native Tailwind CSS v4, and the Froot runtime for maximum developer velocity and sub-millisecond page loads.',
|
|
24
|
+
date: 'Oct 31, 2026',
|
|
25
|
+
readTime: '4 min read',
|
|
26
|
+
category: 'Architecture',
|
|
27
|
+
author: {
|
|
28
|
+
name: 'Alex Rivera',
|
|
29
|
+
role: 'Staff Systems Architect',
|
|
30
|
+
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&h=100&fit=crop&crop=faces',
|
|
31
|
+
},
|
|
32
|
+
tags: ['FrootKit', 'Preact', 'Performance', 'SSR'],
|
|
33
|
+
content: [
|
|
34
|
+
'Modern web frameworks have grown increasingly heavy. Between massive runtime bundles, complex hydration mismatches, and multi-megabyte node_modules dependencies, developers often spend more time debugging tooling than writing features.',
|
|
35
|
+
'FrootKit takes an unapologetically streamlined approach: sub-millisecond Preact server-side rendering, zero-config Tailwind CSS v4 compilation, and Next.js-style App Router conventions without the heavyweight lock-in.',
|
|
36
|
+
'By decoupling static site generation from bloated polyfills and leveraging monomorphic V8 dispatch trees, FrootKit achieves sub-millisecond render times while keeping memory usage under 20MB.',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
slug: 'tailwind-v4-without-configs',
|
|
41
|
+
title: 'Zero Configuration Tailwind CSS v4 in Practice',
|
|
42
|
+
summary: 'Say goodbye to tailwind.config.js and postcss.config.js. Explore how native CSS @theme variables simplify fullstack design tokens.',
|
|
43
|
+
date: 'Oct 28, 2026',
|
|
44
|
+
readTime: '3 min read',
|
|
45
|
+
category: 'Styling',
|
|
46
|
+
author: {
|
|
47
|
+
name: 'Elena Rostova',
|
|
48
|
+
role: 'Design Systems Lead',
|
|
49
|
+
avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=100&h=100&fit=crop&crop=faces',
|
|
50
|
+
},
|
|
51
|
+
tags: ['TailwindCSS', 'CSS', 'Design'],
|
|
52
|
+
content: [
|
|
53
|
+
'Tailwind CSS v4 revolutionizes how utility styling is processed. Instead of relying on JavaScript configuration files, everything is declared directly inside modern CSS using the @theme directive.',
|
|
54
|
+
'In FrootKit, the Tailwind CLI is integrated natively into the development lifecycle. When you run frootkit dev, stylesheets are recompiled in single-digit milliseconds without requiring external bundlers.',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
slug: 'building-sub-millisecond-apis',
|
|
59
|
+
title: 'Crafting Dynamic Fullstack APIs with Standard Web Standards',
|
|
60
|
+
summary: 'A deep dive into Next.js-style route handlers powered by standard Request and Response interfaces on top of Froot.',
|
|
61
|
+
date: 'Oct 24, 2026',
|
|
62
|
+
readTime: '5 min read',
|
|
63
|
+
category: 'Backend',
|
|
64
|
+
author: {
|
|
65
|
+
name: 'Marcus Chen',
|
|
66
|
+
role: 'Core Runtime Engineer',
|
|
67
|
+
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=faces',
|
|
68
|
+
},
|
|
69
|
+
tags: ['API', 'Web Standards', 'TypeScript'],
|
|
70
|
+
content: [
|
|
71
|
+
'Web standards have matured dramatically. Standard Fetch API primitives like Request, Response, Headers, and URL are now universal across Node.js, Bun, Deno, and browsers.',
|
|
72
|
+
'FrootKit route handlers in src/app/api/**/route.ts take direct advantage of these standards. You export standard HTTP verb functions like GET, POST, PUT, DELETE, and receive native Request objects with zero middleware baggage.',
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-brand-50: #ecfdf5;
|
|
5
|
+
--color-brand-100: #d1fae5;
|
|
6
|
+
--color-brand-200: #a7f3d0;
|
|
7
|
+
--color-brand-300: #6ee7b7;
|
|
8
|
+
--color-brand-400: #34d399;
|
|
9
|
+
--color-brand-500: #10b981;
|
|
10
|
+
--color-brand-600: #059669;
|
|
11
|
+
--color-brand-700: #047857;
|
|
12
|
+
--color-brand-800: #065f46;
|
|
13
|
+
--color-brand-900: #064e3b;
|
|
14
|
+
--color-brand-950: #022c22;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
color-scheme: dark;
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"jsxImportSource": "preact",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — E-Commerce Store
|
|
2
|
+
|
|
3
|
+
High-performance digital storefront built with **FrootKit**.
|
|
4
|
+
|
|
5
|
+
## 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Dynamic product pages: `src/app/products/[id]/page.tsx`
|
|
8
|
+
- Product catalog & Cart drawer: `src/app/cart/page.tsx`
|
|
9
|
+
- Checkout API endpoint: `src/app/api/checkout/route.ts`
|
|
10
|
+
- Products API endpoint: `src/app/api/products/route.ts`
|
|
11
|
+
- Ready for real database integration (Drizzle ORM with SQLite or Prisma)!
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "frootkit dev",
|
|
7
|
+
"build": "frootkit build",
|
|
8
|
+
"start": "frootkit start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"frootkit": "npm:froot-kit@^1.3.1",
|
|
12
|
+
"froot-kit": "^1.3.1",
|
|
13
|
+
"frootjs": "^1.0.6",
|
|
14
|
+
"preact": "^11.1.0",
|
|
15
|
+
"preact-render-to-string": "^6.8.2"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@tailwindcss/cli": "^4.0.0",
|
|
19
|
+
"tailwindcss": "^4.0.0",
|
|
20
|
+
"@types/node": "^22.10.0",
|
|
21
|
+
"tsx": "^4.19.2",
|
|
22
|
+
"typescript": "^5.8.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function POST(req: Request): Promise<Response> {
|
|
2
|
+
const orderId = 'ord_' + Math.random().toString(36).substring(2, 9);
|
|
3
|
+
|
|
4
|
+
return Response.json(
|
|
5
|
+
{
|
|
6
|
+
success: true,
|
|
7
|
+
orderId,
|
|
8
|
+
status: 'pending_payment',
|
|
9
|
+
message: 'Order created successfully. Ready for Stripe / LemonSqueezy integration.',
|
|
10
|
+
createdAt: new Date().toISOString(),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
status: 201,
|
|
14
|
+
headers: {
|
|
15
|
+
'content-type': 'application/json',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PRODUCTS } from '../../../data/products.js';
|
|
2
|
+
|
|
3
|
+
export async function GET(req: Request): Promise<Response> {
|
|
4
|
+
const url = new URL(req.url);
|
|
5
|
+
const category = url.searchParams.get('category');
|
|
6
|
+
const limit = Number(url.searchParams.get('limit') || 20);
|
|
7
|
+
|
|
8
|
+
let results = [...PRODUCTS];
|
|
9
|
+
if (category) {
|
|
10
|
+
results = results.filter((p) => p.category.toLowerCase() === category.toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
results = results.slice(0, limit);
|
|
14
|
+
|
|
15
|
+
return Response.json(
|
|
16
|
+
{
|
|
17
|
+
products: results,
|
|
18
|
+
total: results.length,
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
status: 200,
|
|
23
|
+
headers: {
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Navbar } from '../../components/Navbar.js';
|
|
2
|
+
import { CartDrawer } from '../../components/CartDrawer.js';
|
|
3
|
+
import { Footer } from '../../components/Footer.js';
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
title: 'Shopping Cart — FrootStore',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function CartPage() {
|
|
10
|
+
return (
|
|
11
|
+
<div class="min-h-screen flex flex-col justify-between">
|
|
12
|
+
<Navbar />
|
|
13
|
+
|
|
14
|
+
<main class="max-w-4xl mx-auto px-6 py-12 flex-1 w-full">
|
|
15
|
+
<h1 class="text-3xl font-black text-white mb-8">Your Cart</h1>
|
|
16
|
+
<CartDrawer />
|
|
17
|
+
</main>
|
|
18
|
+
|
|
19
|
+
<Footer />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
|
|
3
|
+
export interface RootLayoutProps {
|
|
4
|
+
children: ComponentChildren;
|
|
5
|
+
metadata?: {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function RootLayout({ children, metadata }: RootLayoutProps) {
|
|
12
|
+
const title = metadata?.title || 'FrootStore — High-Performance Gear';
|
|
13
|
+
const description =
|
|
14
|
+
metadata?.description ||
|
|
15
|
+
'Minimalist hardware, peripherals, and engineering workspace equipment powered by Preact and FrootKit.';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" class="dark scroll-smooth">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charSet="UTF-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
22
|
+
<title>{title}</title>
|
|
23
|
+
<meta name="description" content={description} />
|
|
24
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
25
|
+
</head>
|
|
26
|
+
<body class="bg-[#030712] text-slate-100 min-h-screen antialiased selection:bg-emerald-500/20 selection:text-emerald-300">
|
|
27
|
+
{children}
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PRODUCTS } from '../data/products.js';
|
|
2
|
+
import { Navbar } from '../components/Navbar.js';
|
|
3
|
+
import { ProductCard } from '../components/ProductCard.js';
|
|
4
|
+
import { Footer } from '../components/Footer.js';
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
title: 'FrootStore — Curated Workspace & Developer Hardware',
|
|
8
|
+
description: 'Precision mechanical keyboards, studio audio, and ergonomic setups.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function StoreHomePage() {
|
|
12
|
+
return (
|
|
13
|
+
<div class="min-h-screen flex flex-col justify-between">
|
|
14
|
+
<Navbar />
|
|
15
|
+
|
|
16
|
+
<main class="max-w-6xl mx-auto px-6 py-12 flex-1 w-full">
|
|
17
|
+
{/* Hero Banner */}
|
|
18
|
+
<div class="rounded-3xl border border-slate-800 bg-gradient-to-r from-slate-900 via-slate-950 to-emerald-950/30 p-8 sm:p-12 mb-12 relative overflow-hidden">
|
|
19
|
+
<div class="max-w-2xl relative z-10">
|
|
20
|
+
<span class="inline-block px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-xs font-semibold text-emerald-400 mb-4">
|
|
21
|
+
Autumn 2026 Collection
|
|
22
|
+
</span>
|
|
23
|
+
<h1 class="text-4xl sm:text-5xl font-black text-white tracking-tight mb-4">
|
|
24
|
+
Engineered for Focus. Crafted for Craft.
|
|
25
|
+
</h1>
|
|
26
|
+
<p class="text-slate-300 text-base sm:text-lg mb-8 leading-relaxed">
|
|
27
|
+
Curated minimal workspace hardware designed to elevate your daily development workflow with tactile precision.
|
|
28
|
+
</p>
|
|
29
|
+
<div class="flex items-center gap-4">
|
|
30
|
+
<a
|
|
31
|
+
href="#catalog"
|
|
32
|
+
class="px-6 py-3 rounded-xl bg-emerald-500 text-slate-950 font-bold hover:bg-emerald-400 transition-colors shadow-lg shadow-emerald-500/20 text-sm"
|
|
33
|
+
>
|
|
34
|
+
Browse All Equipment
|
|
35
|
+
</a>
|
|
36
|
+
<span class="text-xs text-slate-400 font-mono">Free shipping over $100</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Catalog Section */}
|
|
42
|
+
<div id="catalog" class="mb-8 flex items-center justify-between">
|
|
43
|
+
<div>
|
|
44
|
+
<h2 class="text-2xl font-bold text-white">All Products</h2>
|
|
45
|
+
<p class="text-xs text-slate-400">Showing {PRODUCTS.length} curated instruments</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
50
|
+
{PRODUCTS.map((product) => (
|
|
51
|
+
<ProductCard key={product.id} product={product} />
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
</main>
|
|
55
|
+
|
|
56
|
+
<Footer />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|