create-frootkit 1.1.3 → 1.2.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/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 +475 -14
- 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 +3 -5
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +47 -403
- 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 +16 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +32 -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,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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { PRODUCTS } from '../../../data/products.js';
|
|
2
|
+
import { Navbar } from '../../../components/Navbar.js';
|
|
3
|
+
import { Footer } from '../../../components/Footer.js';
|
|
4
|
+
|
|
5
|
+
export async function generateStaticParams() {
|
|
6
|
+
return PRODUCTS.map((p) => ({ id: p.id }));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function ProductDetailPage({ params }: { params?: { id?: string } }) {
|
|
10
|
+
const id = params?.id || 'nordic-mechanical-keyboard';
|
|
11
|
+
const product = PRODUCTS.find((p) => p.id === id) || PRODUCTS[0]!;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div class="min-h-screen flex flex-col justify-between">
|
|
15
|
+
<Navbar />
|
|
16
|
+
|
|
17
|
+
<main class="max-w-6xl 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 catalog
|
|
23
|
+
</a>
|
|
24
|
+
|
|
25
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
|
|
26
|
+
<div class="rounded-3xl border border-slate-800 overflow-hidden bg-slate-950 aspect-[4/3]">
|
|
27
|
+
<img
|
|
28
|
+
src={product.image}
|
|
29
|
+
alt={product.name}
|
|
30
|
+
class="w-full h-full object-cover"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div>
|
|
35
|
+
<div class="flex items-center gap-2 mb-3">
|
|
36
|
+
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs font-mono font-medium">
|
|
37
|
+
{product.category}
|
|
38
|
+
</span>
|
|
39
|
+
<span class="text-amber-400 text-xs font-bold">
|
|
40
|
+
★ {product.rating} ({product.reviewsCount} reviews)
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<h1 class="text-3xl sm:text-4xl font-black text-white tracking-tight mb-4">
|
|
45
|
+
{product.name}
|
|
46
|
+
</h1>
|
|
47
|
+
|
|
48
|
+
<div class="text-3xl font-black text-emerald-400 mb-6 font-mono">
|
|
49
|
+
${product.price.toFixed(2)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<p class="text-slate-300 text-base leading-relaxed mb-8">
|
|
53
|
+
{product.description}
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<div class="border-t border-slate-800 pt-6 mb-8">
|
|
57
|
+
<h3 class="text-xs uppercase font-mono font-bold text-slate-400 tracking-wider mb-4">
|
|
58
|
+
Key Specifications
|
|
59
|
+
</h3>
|
|
60
|
+
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm text-slate-300">
|
|
61
|
+
{product.features.map((f) => (
|
|
62
|
+
<li key={f} class="flex items-center gap-2">
|
|
63
|
+
<span class="text-emerald-400 font-bold">✓</span>
|
|
64
|
+
<span>{f}</span>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="flex items-center gap-4">
|
|
71
|
+
<a
|
|
72
|
+
href="/cart"
|
|
73
|
+
class="flex-1 py-3.5 rounded-xl bg-emerald-500 text-slate-950 font-bold text-center text-sm hover:bg-emerald-400 transition-colors shadow-lg shadow-emerald-500/20"
|
|
74
|
+
>
|
|
75
|
+
Add to Cart • ${product.price.toFixed(2)}
|
|
76
|
+
</a>
|
|
77
|
+
<span class="text-xs text-slate-500 font-mono">
|
|
78
|
+
{product.inStock ? 'In Stock (Ready to Ship)' : 'Backorder'}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</main>
|
|
84
|
+
|
|
85
|
+
<Footer />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { PRODUCTS } from '../data/products.js';
|
|
2
|
+
|
|
3
|
+
export function CartDrawer() {
|
|
4
|
+
const cartItems = [
|
|
5
|
+
{ product: PRODUCTS[0]!, quantity: 1 },
|
|
6
|
+
{ product: PRODUCTS[2]!, quantity: 1 },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const subtotal = cartItems.reduce((acc, item) => acc + item.product.price * item.quantity, 0);
|
|
10
|
+
const shipping = subtotal > 100 ? 0 : 15;
|
|
11
|
+
const total = subtotal + shipping;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div class="rounded-2xl border border-slate-800 bg-slate-900/60 p-6 backdrop-blur-md">
|
|
15
|
+
<h2 class="text-lg font-bold text-white mb-4 flex items-center justify-between">
|
|
16
|
+
<span>Order Summary</span>
|
|
17
|
+
<span class="text-xs font-mono text-emerald-400 font-semibold">{cartItems.length} items</span>
|
|
18
|
+
</h2>
|
|
19
|
+
|
|
20
|
+
<div class="space-y-4 mb-6">
|
|
21
|
+
{cartItems.map(({ product, quantity }) => (
|
|
22
|
+
<div key={product.id} class="flex items-center gap-3 pb-3 border-b border-slate-800/60">
|
|
23
|
+
<img
|
|
24
|
+
src={product.image}
|
|
25
|
+
alt={product.name}
|
|
26
|
+
class="w-12 h-12 rounded-lg object-cover bg-slate-950"
|
|
27
|
+
/>
|
|
28
|
+
<div class="flex-1 min-w-0">
|
|
29
|
+
<h4 class="text-xs font-semibold text-white truncate">{product.name}</h4>
|
|
30
|
+
<div class="text-[11px] text-slate-400">Qty: {quantity} × ${product.price}</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="font-bold text-sm text-white font-mono">
|
|
33
|
+
${(product.price * quantity).toFixed(2)}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="space-y-2 text-xs text-slate-400 mb-6 font-mono">
|
|
40
|
+
<div class="flex justify-between">
|
|
41
|
+
<span>Subtotal</span>
|
|
42
|
+
<span class="text-white font-bold">${subtotal.toFixed(2)}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="flex justify-between">
|
|
45
|
+
<span>Estimated Shipping</span>
|
|
46
|
+
<span class="text-emerald-400 font-semibold">{shipping === 0 ? 'FREE' : `$${shipping.toFixed(2)}`}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="pt-3 border-t border-slate-800 flex justify-between text-sm text-white font-bold">
|
|
49
|
+
<span>Total</span>
|
|
50
|
+
<span class="text-emerald-400 text-base font-black">${total.toFixed(2)}</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<form action="/api/checkout" method="POST">
|
|
55
|
+
<button
|
|
56
|
+
type="submit"
|
|
57
|
+
class="w-full py-3 rounded-xl bg-emerald-500 text-slate-950 font-bold text-sm hover:bg-emerald-400 transition-colors shadow-lg shadow-emerald-500/20"
|
|
58
|
+
>
|
|
59
|
+
Proceed to Checkout →
|
|
60
|
+
</button>
|
|
61
|
+
</form>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -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-6xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
5
|
+
<div>
|
|
6
|
+
FrootStore © {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/products" class="hover:text-slate-400 transition-colors">Products API</a>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</footer>
|
|
15
|
+
);
|
|
16
|
+
}
|