create-frootkit 1.2.8 → 1.2.10
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/addons/database.d.ts +13 -5
- package/dist/addons/database.d.ts.map +1 -1
- package/dist/addons/database.js +496 -94
- package/dist/addons/database.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +5 -1
- package/dist/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/templates/auth/src/app/api/auth/route.ts +17 -12
- package/templates/auth/src/app/dashboard/layout.tsx +14 -9
- package/templates/auth/src/app/dashboard/page.tsx +55 -24
- package/templates/auth/src/app/layout.tsx +3 -3
- package/templates/auth/src/app/login/page.tsx +5 -1
- package/templates/auth/src/app/page.tsx +103 -45
- package/templates/auth/src/app/signup/page.tsx +5 -1
- package/templates/auth/src/components/AuthForm.tsx +34 -19
- package/templates/auth/src/components/DashboardNav.tsx +97 -56
- package/templates/auth/src/components/MetricCard.tsx +13 -6
- package/templates/auth/src/components/Navbar.tsx +13 -12
- package/templates/auth/src/lib/auth.ts +123 -0
- package/templates/auth/src/styles.css +27 -5
- package/templates/blog/src/app/api/posts/route.ts +10 -34
- package/templates/blog/src/app/blog/[slug]/page.tsx +22 -21
- package/templates/blog/src/app/layout.tsx +4 -4
- package/templates/blog/src/app/page.tsx +19 -17
- package/templates/blog/src/components/Footer.tsx +6 -6
- package/templates/blog/src/components/Navbar.tsx +17 -15
- package/templates/blog/src/components/Newsletter.tsx +11 -9
- package/templates/blog/src/components/PostCard.tsx +29 -27
- package/templates/blog/src/data/posts.ts +48 -0
- package/templates/blog/src/styles.css +14 -5
- package/templates/ecommerce/src/app/api/products/route.ts +10 -26
- package/templates/ecommerce/src/app/cart/page.tsx +3 -3
- package/templates/ecommerce/src/app/layout.tsx +2 -2
- package/templates/ecommerce/src/app/page.tsx +41 -18
- package/templates/ecommerce/src/app/products/[id]/page.tsx +37 -28
- package/templates/ecommerce/src/components/CartDrawer.tsx +20 -17
- package/templates/ecommerce/src/components/Footer.tsx +6 -6
- package/templates/ecommerce/src/components/Navbar.tsx +21 -14
- package/templates/ecommerce/src/components/ProductCard.tsx +22 -15
- package/templates/ecommerce/src/data/products.ts +55 -11
- package/templates/ecommerce/src/styles.css +29 -5
- package/templates/empty/src/app/layout.tsx +5 -5
- package/templates/empty/src/app/page.tsx +10 -9
- package/templates/empty/src/styles.css +13 -15
- package/templates/starter/src/app/layout.tsx +4 -4
- package/templates/starter/src/app/page.tsx +41 -33
- package/templates/starter/src/components/FeatureCard.tsx +5 -5
- package/templates/starter/src/components/Footer.tsx +6 -6
- package/templates/starter/src/components/Header.tsx +18 -14
- package/templates/starter/src/components/TerminalPreview.tsx +30 -30
- package/templates/starter/src/styles.css +14 -5
|
@@ -1,44 +1,67 @@
|
|
|
1
|
-
import { PRODUCTS } from '../data/products.js';
|
|
1
|
+
import { getProducts, PRODUCTS } from '../data/products.js';
|
|
2
2
|
import { Navbar } from '../components/Navbar.js';
|
|
3
3
|
import { ProductCard } from '../components/ProductCard.js';
|
|
4
4
|
import { Footer } from '../components/Footer.js';
|
|
5
5
|
|
|
6
6
|
export const metadata = {
|
|
7
|
-
title: 'FrootStore —
|
|
7
|
+
title: 'FrootStore — Tactile Instruments for Developers',
|
|
8
8
|
description: 'Precision mechanical keyboards, studio audio, and ergonomic setups.',
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export default function StoreHomePage() {
|
|
11
|
+
export default async function StoreHomePage() {
|
|
12
|
+
const products = await getProducts();
|
|
13
|
+
const categories = ['All Products', 'Keyboards', 'Audio', 'Peripherals', 'Desk Setup', 'Accessories'];
|
|
14
|
+
|
|
12
15
|
return (
|
|
13
|
-
<div class="min-h-screen bg-[#
|
|
16
|
+
<div class="min-h-screen bg-[#090a0f] text-zinc-100 flex flex-col justify-between selection:bg-white/20 selection:text-white">
|
|
14
17
|
<Navbar />
|
|
15
18
|
|
|
16
|
-
<main class="max-w-6xl mx-auto px-6 py-12 flex-1 w-full">
|
|
17
|
-
{/* Collection
|
|
18
|
-
<div class="mb-12 border-b border-[
|
|
19
|
-
<div class="
|
|
20
|
-
|
|
19
|
+
<main class="max-w-6xl mx-auto px-6 py-12 sm:py-16 flex-1 w-full">
|
|
20
|
+
{/* Curated Collection Hero */}
|
|
21
|
+
<div class="mb-12 border-b border-white/[0.08] pb-10">
|
|
22
|
+
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/[0.04] border border-white/[0.08] text-zinc-300 text-xs font-medium mb-4">
|
|
23
|
+
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
24
|
+
<span>Curated Hardware Collection</span>
|
|
21
25
|
</div>
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
<h1 class="text-3xl sm:text-5xl font-semibold text-white tracking-[-0.03em] leading-tight mb-4">
|
|
28
|
+
Tactile Instruments for Focused Engineering.
|
|
24
29
|
</h1>
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
|
|
31
|
+
<p class="text-zinc-400 text-base max-w-2xl leading-relaxed">
|
|
32
|
+
Minimal workspace instruments, CNC aluminum mechanical keyboards, and planar audio drivers designed to eliminate friction from extended development sessions.
|
|
27
33
|
</p>
|
|
34
|
+
|
|
35
|
+
{/* Category Filter Pills */}
|
|
36
|
+
<div class="flex flex-wrap items-center gap-2 mt-8">
|
|
37
|
+
{categories.map((c, i) => (
|
|
38
|
+
<a
|
|
39
|
+
key={c}
|
|
40
|
+
href="/#catalog"
|
|
41
|
+
class={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
|
42
|
+
i === 0
|
|
43
|
+
? 'bg-white text-zinc-950 shadow-sm'
|
|
44
|
+
: 'bg-white/[0.04] border border-white/[0.08] text-zinc-400 hover:text-white hover:bg-white/[0.08]'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{c}
|
|
48
|
+
</a>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
28
51
|
</div>
|
|
29
52
|
|
|
30
53
|
{/* Catalog Section Header */}
|
|
31
54
|
<div id="catalog" class="mb-6 flex items-center justify-between">
|
|
32
|
-
<h2 class="text-xs
|
|
33
|
-
|
|
55
|
+
<h2 class="text-xs uppercase tracking-wider text-zinc-400 font-semibold">
|
|
56
|
+
Featured Instruments
|
|
34
57
|
</h2>
|
|
35
|
-
<span class="text-xs
|
|
36
|
-
{
|
|
58
|
+
<span class="text-xs text-zinc-500 tabular-nums">
|
|
59
|
+
{products.length} products available
|
|
37
60
|
</span>
|
|
38
61
|
</div>
|
|
39
62
|
|
|
40
63
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
41
|
-
{
|
|
64
|
+
{products.map((product) => (
|
|
42
65
|
<ProductCard key={product.id} product={product} />
|
|
43
66
|
))}
|
|
44
67
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PRODUCTS } from '../../../data/products.js';
|
|
1
|
+
import { getProduct, PRODUCTS } from '../../../data/products.js';
|
|
2
2
|
import { Navbar } from '../../../components/Navbar.js';
|
|
3
3
|
import { Footer } from '../../../components/Footer.js';
|
|
4
4
|
|
|
@@ -6,78 +6,87 @@ export async function generateStaticParams() {
|
|
|
6
6
|
return PRODUCTS.map((p) => ({ id: p.id }));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export default function ProductDetailPage({ params }: { params?: { id?: string } }) {
|
|
9
|
+
export default async function ProductDetailPage({ params }: { params?: { id?: string } }) {
|
|
10
10
|
const id = params?.id || 'nordic-mechanical-keyboard';
|
|
11
|
-
const product =
|
|
11
|
+
const product = (await getProduct(id)) || PRODUCTS[0]!;
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<div class="min-h-screen bg-[#
|
|
14
|
+
<div class="min-h-screen bg-[#090a0f] text-zinc-100 flex flex-col justify-between selection:bg-white/20 selection:text-white">
|
|
15
15
|
<Navbar />
|
|
16
16
|
|
|
17
17
|
<main class="max-w-6xl mx-auto px-6 py-12 flex-1 w-full">
|
|
18
18
|
<a
|
|
19
19
|
href="/"
|
|
20
|
-
class="inline-flex items-center gap-2 text-xs
|
|
20
|
+
class="inline-flex items-center gap-2 text-xs text-zinc-400 hover:text-white mb-8 transition-colors"
|
|
21
21
|
>
|
|
22
|
-
|
|
22
|
+
<span>←</span>
|
|
23
|
+
<span>Back to catalog</span>
|
|
23
24
|
</a>
|
|
24
25
|
|
|
25
|
-
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
|
|
26
|
-
|
|
26
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
|
|
27
|
+
{/* Product Gallery Image */}
|
|
28
|
+
<div class="rounded-2xl border border-white/[0.08] overflow-hidden bg-[#0f1117] aspect-[4/3] shadow-2xl relative">
|
|
27
29
|
<img
|
|
28
30
|
src={product.image}
|
|
29
31
|
alt={product.name}
|
|
30
32
|
class="w-full h-full object-cover"
|
|
31
33
|
/>
|
|
34
|
+
<div class="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-transparent pointer-events-none" />
|
|
32
35
|
</div>
|
|
33
36
|
|
|
37
|
+
{/* Details & Purchase */}
|
|
34
38
|
<div>
|
|
35
|
-
<div class="flex items-center gap-3 mb-3 text-xs
|
|
36
|
-
<span class="px-2.5 py-0.5 rounded bg-
|
|
39
|
+
<div class="flex items-center gap-3 mb-3 text-xs">
|
|
40
|
+
<span class="px-2.5 py-0.5 rounded-full bg-white/[0.04] text-zinc-300 border border-white/[0.08] font-medium text-[11px] uppercase tracking-wider">
|
|
37
41
|
{product.category}
|
|
38
42
|
</span>
|
|
39
|
-
<span class="text-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
<span class="text-zinc-600">•</span>
|
|
44
|
+
<div class="flex items-center gap-1.5 text-zinc-400 font-medium">
|
|
45
|
+
<span class="text-amber-400">★</span>
|
|
46
|
+
<span class="text-zinc-200 tabular-nums">{product.rating.toFixed(1)}</span>
|
|
47
|
+
<span class="text-zinc-500 tabular-nums">({product.reviewsCount} verified reviews)</span>
|
|
48
|
+
</div>
|
|
43
49
|
</div>
|
|
44
50
|
|
|
45
|
-
<h1 class="text-3xl sm:text-4xl font-
|
|
51
|
+
<h1 class="text-3xl sm:text-4xl font-semibold text-white tracking-[-0.025em] mb-4">
|
|
46
52
|
{product.name}
|
|
47
53
|
</h1>
|
|
48
54
|
|
|
49
|
-
<div class="text-3xl font-
|
|
55
|
+
<div class="text-3xl font-semibold text-white mb-6 tabular-nums">
|
|
50
56
|
${product.price.toFixed(2)}
|
|
51
57
|
</div>
|
|
52
58
|
|
|
53
|
-
<p class="text-
|
|
59
|
+
<p class="text-zinc-300 text-sm sm:text-base leading-relaxed mb-8">
|
|
54
60
|
{product.description}
|
|
55
61
|
</p>
|
|
56
62
|
|
|
57
|
-
<div class="border-t border-[
|
|
58
|
-
<h2 class="text-xs
|
|
59
|
-
|
|
63
|
+
<div class="border-t border-white/[0.08] pt-6 mb-8">
|
|
64
|
+
<h2 class="text-xs uppercase tracking-wider font-semibold text-zinc-400 mb-4">
|
|
65
|
+
Hardware Specifications
|
|
60
66
|
</h2>
|
|
61
|
-
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-
|
|
67
|
+
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs text-zinc-300">
|
|
62
68
|
{product.features.map((f) => (
|
|
63
69
|
<li key={f} class="flex items-center gap-2">
|
|
64
|
-
<span class="
|
|
70
|
+
<span class="w-1.5 h-1.5 rounded-full bg-zinc-400 shrink-0" />
|
|
65
71
|
<span>{f}</span>
|
|
66
72
|
</li>
|
|
67
73
|
))}
|
|
68
74
|
</ul>
|
|
69
75
|
</div>
|
|
70
76
|
|
|
71
|
-
<div class="flex items-center gap-4">
|
|
77
|
+
<div class="flex flex-col sm:flex-row sm:items-center gap-4 pt-2">
|
|
72
78
|
<a
|
|
73
79
|
href="/cart"
|
|
74
|
-
class="flex-1 py-3 rounded-
|
|
80
|
+
class="flex-1 py-3 px-6 rounded-xl bg-white text-zinc-950 font-medium text-center text-sm hover:bg-zinc-200 transition-all shadow-md flex items-center justify-center gap-2"
|
|
75
81
|
>
|
|
76
|
-
Add to Cart
|
|
82
|
+
<span>Add to Cart</span>
|
|
83
|
+
<span class="text-zinc-500">•</span>
|
|
84
|
+
<span class="tabular-nums font-semibold">${product.price.toFixed(2)}</span>
|
|
77
85
|
</a>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
<div class="flex items-center gap-2 text-xs text-zinc-400 px-3">
|
|
87
|
+
<span class="w-2 h-2 rounded-full bg-emerald-400" />
|
|
88
|
+
<span>{product.inStock ? 'In Stock · Ships in 24h' : 'Backorder'}</span>
|
|
89
|
+
</div>
|
|
81
90
|
</div>
|
|
82
91
|
</div>
|
|
83
92
|
</div>
|
|
@@ -11,52 +11,55 @@ export function CartDrawer() {
|
|
|
11
11
|
const total = subtotal + shipping;
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<div class="rounded-xl border border-[
|
|
15
|
-
<h2 class="text-
|
|
14
|
+
<div class="rounded-xl border border-white/[0.08] bg-[#0f1117] p-6 sm:p-8 shadow-xl">
|
|
15
|
+
<h2 class="text-base font-semibold text-white mb-6 flex items-center justify-between border-b border-white/[0.08] pb-4">
|
|
16
16
|
<span>Order Summary</span>
|
|
17
|
-
<span class="text-xs
|
|
17
|
+
<span class="text-xs text-zinc-400 font-medium px-2 py-0.5 rounded-full bg-white/[0.04] border border-white/[0.06] tabular-nums">
|
|
18
|
+
{cartItems.length} items
|
|
19
|
+
</span>
|
|
18
20
|
</h2>
|
|
19
21
|
|
|
20
22
|
<div class="space-y-4 mb-6">
|
|
21
23
|
{cartItems.map(({ product, quantity }) => (
|
|
22
|
-
<div key={product.id} class="flex items-center gap-4 pb-4 border-b border-[
|
|
24
|
+
<div key={product.id} class="flex items-center gap-4 pb-4 border-b border-white/[0.06]">
|
|
23
25
|
<img
|
|
24
26
|
src={product.image}
|
|
25
27
|
alt={product.name}
|
|
26
|
-
class="w-14 h-14 rounded-lg object-cover bg-
|
|
28
|
+
class="w-14 h-14 rounded-lg object-cover bg-zinc-950/40 border border-white/[0.08]"
|
|
27
29
|
/>
|
|
28
30
|
<div class="flex-1 min-w-0">
|
|
29
|
-
<h4 class="text-sm font-
|
|
30
|
-
<div class="text-xs text-
|
|
31
|
+
<h4 class="text-xs sm:text-sm font-medium text-zinc-100 truncate">{product.name}</h4>
|
|
32
|
+
<div class="text-xs text-zinc-400 mt-0.5 tabular-nums">Qty: {quantity} × ${product.price.toFixed(2)}</div>
|
|
31
33
|
</div>
|
|
32
|
-
<div class="font-
|
|
34
|
+
<div class="font-medium text-xs sm:text-sm text-white tabular-nums">
|
|
33
35
|
${(product.price * quantity).toFixed(2)}
|
|
34
36
|
</div>
|
|
35
37
|
</div>
|
|
36
38
|
))}
|
|
37
39
|
</div>
|
|
38
40
|
|
|
39
|
-
<div class="space-y-2.5 text-xs text-
|
|
41
|
+
<div class="space-y-2.5 text-xs text-zinc-300 mb-6">
|
|
40
42
|
<div class="flex justify-between">
|
|
41
|
-
<span class="text-
|
|
42
|
-
<span class="text-white font-
|
|
43
|
+
<span class="text-zinc-400">Subtotal</span>
|
|
44
|
+
<span class="text-white font-medium tabular-nums">${subtotal.toFixed(2)}</span>
|
|
43
45
|
</div>
|
|
44
46
|
<div class="flex justify-between">
|
|
45
|
-
<span class="text-
|
|
46
|
-
<span class="text-emerald-400 font-
|
|
47
|
+
<span class="text-zinc-400">Standard Shipping</span>
|
|
48
|
+
<span class="text-emerald-400 font-medium">{shipping === 0 ? 'FREE' : `$${shipping.toFixed(2)}`}</span>
|
|
47
49
|
</div>
|
|
48
|
-
<div class="pt-3 border-t border-[
|
|
50
|
+
<div class="pt-3 border-t border-white/[0.08] flex justify-between text-sm text-white font-semibold">
|
|
49
51
|
<span>Total</span>
|
|
50
|
-
<span class="text-
|
|
52
|
+
<span class="text-base text-white tabular-nums">${total.toFixed(2)}</span>
|
|
51
53
|
</div>
|
|
52
54
|
</div>
|
|
53
55
|
|
|
54
56
|
<form action="/api/checkout" method="POST">
|
|
55
57
|
<button
|
|
56
58
|
type="submit"
|
|
57
|
-
class="w-full py-3 rounded-lg bg-
|
|
59
|
+
class="w-full py-3 rounded-lg bg-white text-zinc-950 font-medium text-xs hover:bg-zinc-200 transition-all shadow-sm flex items-center justify-center gap-2 group"
|
|
58
60
|
>
|
|
59
|
-
Proceed to Checkout
|
|
61
|
+
<span>Proceed to Checkout</span>
|
|
62
|
+
<span class="text-zinc-500 group-hover:translate-x-0.5 transition-transform">→</span>
|
|
60
63
|
</button>
|
|
61
64
|
</form>
|
|
62
65
|
</div>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export function Footer() {
|
|
2
2
|
return (
|
|
3
|
-
<footer class="border-t border-[
|
|
4
|
-
<div class="max-w-6xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4
|
|
3
|
+
<footer class="border-t border-white/[0.08] bg-[#090a0f] py-8 text-xs text-zinc-500">
|
|
4
|
+
<div class="max-w-6xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
5
5
|
<div>
|
|
6
|
-
FrootStore © {new Date().getFullYear()} OrbitNorth LLC.
|
|
6
|
+
FrootStore © {new Date().getFullYear()} OrbitNorth LLC. Built with FrootKit.
|
|
7
7
|
</div>
|
|
8
8
|
<div class="flex items-center gap-6">
|
|
9
|
-
<a href="https://github.com/orbitnorth/frootkit" class="hover:text-
|
|
10
|
-
<a href="https://frootjs.org" class="hover:text-
|
|
11
|
-
<a href="/api/products" class="hover:text-
|
|
9
|
+
<a href="https://github.com/orbitnorth/frootkit" class="hover:text-zinc-300 transition-colors">GitHub</a>
|
|
10
|
+
<a href="https://frootjs.org" class="hover:text-zinc-300 transition-colors">Documentation</a>
|
|
11
|
+
<a href="/api/products" class="hover:text-zinc-300 transition-colors font-mono text-[11px]">/api/products</a>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
</footer>
|
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
export function Navbar() {
|
|
2
2
|
return (
|
|
3
|
-
<header class="border-b border-[
|
|
4
|
-
<div class="max-w-6xl mx-auto px-6 h-
|
|
5
|
-
<a href="/" class="flex items-center gap-
|
|
6
|
-
<div class="w-
|
|
7
|
-
<svg
|
|
8
|
-
<path
|
|
3
|
+
<header class="border-b border-white/[0.08] bg-[#090a0f]/80 backdrop-blur-md sticky top-0 z-50">
|
|
4
|
+
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
|
|
5
|
+
<a href="/" class="flex items-center gap-2.5 font-semibold text-zinc-100 text-sm tracking-tight group">
|
|
6
|
+
<div class="w-7 h-7 rounded-md bg-white/[0.06] border border-white/[0.1] flex items-center justify-center text-zinc-200 group-hover:border-white/[0.2] transition-colors shrink-0">
|
|
7
|
+
<svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
8
|
+
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
|
9
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
10
|
+
<path d="M16 10a4 4 0 0 1-8 0" />
|
|
9
11
|
</svg>
|
|
10
12
|
</div>
|
|
11
|
-
<span>Froot<span class="text-
|
|
13
|
+
<span>Froot<span class="text-zinc-400 font-normal">Store</span></span>
|
|
12
14
|
</a>
|
|
13
15
|
|
|
14
|
-
<nav class="flex items-center gap-
|
|
15
|
-
<a href="
|
|
16
|
+
<nav class="flex items-center gap-5 text-xs font-medium">
|
|
17
|
+
<a href="/#catalog" class="text-zinc-400 hover:text-zinc-200 transition-colors">
|
|
16
18
|
Catalog
|
|
17
19
|
</a>
|
|
18
|
-
<a
|
|
20
|
+
<a
|
|
21
|
+
href="/api/products"
|
|
22
|
+
class="text-zinc-400 hover:text-zinc-200 font-mono text-[11px] px-2 py-0.5 rounded bg-white/[0.03] border border-white/[0.06] transition-colors"
|
|
23
|
+
>
|
|
19
24
|
/api/products
|
|
20
25
|
</a>
|
|
21
26
|
<a
|
|
22
27
|
href="/cart"
|
|
23
|
-
class="flex items-center gap-2 px-3
|
|
28
|
+
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/[0.05] border border-white/[0.08] text-zinc-200 hover:text-white hover:bg-white/[0.08] transition-all text-xs font-medium"
|
|
24
29
|
>
|
|
25
|
-
<svg
|
|
26
|
-
<path
|
|
30
|
+
<svg style="width: 13px; height: 13px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
31
|
+
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
|
32
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
33
|
+
<path d="M16 10a4 4 0 0 1-8 0" />
|
|
27
34
|
</svg>
|
|
28
35
|
<span>Cart</span>
|
|
29
|
-
<span class="px-1.5 py-0.2 rounded bg-
|
|
36
|
+
<span class="px-1.5 py-0.2 rounded-full bg-white text-zinc-950 font-bold text-[10px] tabular-nums">
|
|
30
37
|
2
|
|
31
38
|
</span>
|
|
32
39
|
</a>
|
|
@@ -2,49 +2,56 @@ import type { Product } from '../data/products.js';
|
|
|
2
2
|
|
|
3
3
|
export function ProductCard({ product }: { product: Product }) {
|
|
4
4
|
return (
|
|
5
|
-
<div class="rounded-xl border border-[
|
|
6
|
-
|
|
5
|
+
<div class="group rounded-xl border border-white/[0.08] bg-[#0f1117] overflow-hidden hover:border-white/[0.18] transition-all duration-200 flex flex-col justify-between shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05)]">
|
|
6
|
+
{/* Product Image Frame */}
|
|
7
|
+
<a href={`/products/${product.id}`} class="block relative aspect-[4/3] overflow-hidden bg-zinc-950/60">
|
|
7
8
|
<img
|
|
8
9
|
src={product.image}
|
|
9
10
|
alt={product.name}
|
|
10
|
-
class="w-full h-full object-cover transition-transform duration-
|
|
11
|
+
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 ease-out"
|
|
11
12
|
loading="lazy"
|
|
12
13
|
/>
|
|
13
|
-
<
|
|
14
|
+
<div class="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent pointer-events-none" />
|
|
15
|
+
<span class="absolute top-3 left-3 px-2.5 py-0.5 rounded-full bg-[#090a0f]/85 backdrop-blur-md text-[10px] uppercase tracking-wider font-semibold text-zinc-300 border border-white/[0.08]">
|
|
14
16
|
{product.category}
|
|
15
17
|
</span>
|
|
16
18
|
</a>
|
|
17
19
|
|
|
20
|
+
{/* Product Details */}
|
|
18
21
|
<div class="p-5 flex-1 flex flex-col justify-between">
|
|
19
22
|
<div>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<span
|
|
23
|
-
<span class="text-
|
|
23
|
+
{/* Subtle star & reviews */}
|
|
24
|
+
<div class="flex items-center gap-1.5 mb-2 text-xs text-zinc-400 font-medium">
|
|
25
|
+
<span class="text-amber-400 text-xs">★</span>
|
|
26
|
+
<span class="text-zinc-200 tabular-nums">{product.rating.toFixed(1)}</span>
|
|
27
|
+
<span class="text-zinc-600">•</span>
|
|
28
|
+
<span class="text-zinc-500 tabular-nums">{product.reviewsCount} reviews</span>
|
|
24
29
|
</div>
|
|
25
30
|
|
|
26
|
-
<h3 class="font-
|
|
31
|
+
<h3 class="font-semibold text-zinc-100 text-sm mb-2 group-hover:text-white transition-colors line-clamp-1">
|
|
27
32
|
<a href={`/products/${product.id}`}>
|
|
28
33
|
{product.name}
|
|
29
34
|
</a>
|
|
30
35
|
</h3>
|
|
31
36
|
|
|
32
|
-
<p class="text-
|
|
37
|
+
<p class="text-zinc-400 text-xs leading-relaxed mb-4 line-clamp-2">
|
|
33
38
|
{product.description}
|
|
34
39
|
</p>
|
|
35
40
|
</div>
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
{/* Price & Action Row */}
|
|
43
|
+
<div class="pt-4 border-t border-white/[0.06] flex items-center justify-between">
|
|
38
44
|
<div>
|
|
39
|
-
<span class="text-[
|
|
40
|
-
<span class="text-
|
|
45
|
+
<span class="text-[10px] text-zinc-500 block uppercase tracking-wider font-medium">Price</span>
|
|
46
|
+
<span class="text-base font-semibold text-white tabular-nums">${product.price.toFixed(2)}</span>
|
|
41
47
|
</div>
|
|
42
48
|
|
|
43
49
|
<a
|
|
44
50
|
href={`/products/${product.id}`}
|
|
45
|
-
class="px-3.5 py-1.5 rounded-lg bg-
|
|
51
|
+
class="px-3.5 py-1.5 rounded-lg bg-white text-zinc-950 font-medium text-xs hover:bg-zinc-200 transition-all shadow-sm flex items-center gap-1.5 group-hover:shadow"
|
|
46
52
|
>
|
|
47
|
-
|
|
53
|
+
<span>View</span>
|
|
54
|
+
<span class="text-zinc-500">→</span>
|
|
48
55
|
</a>
|
|
49
56
|
</div>
|
|
50
57
|
</div>
|
|
@@ -19,9 +19,9 @@ export const PRODUCTS: Product[] = [
|
|
|
19
19
|
category: 'Keyboards',
|
|
20
20
|
rating: 4.9,
|
|
21
21
|
reviewsCount: 128,
|
|
22
|
-
image: 'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=
|
|
23
|
-
description: 'Aircraft-grade anodized aluminum
|
|
24
|
-
features: ['Hot-swappable switches', 'Bluetooth 5.3 & 2.4GHz', 'CNC Aluminum
|
|
22
|
+
image: 'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=800&auto=format&fit=crop&q=80',
|
|
23
|
+
description: 'Aircraft-grade CNC anodized aluminum chassis with custom factory-lubed linear switches and sound-dampening silicone base.',
|
|
24
|
+
features: ['Hot-swappable switches', 'Bluetooth 5.3 & 2.4GHz wireless', 'CNC Aluminum Unibody', 'PBT Dye-Sub Keycaps'],
|
|
25
25
|
inStock: true,
|
|
26
26
|
},
|
|
27
27
|
{
|
|
@@ -31,9 +31,9 @@ export const PRODUCTS: Product[] = [
|
|
|
31
31
|
category: 'Audio',
|
|
32
32
|
rating: 4.8,
|
|
33
33
|
reviewsCount: 94,
|
|
34
|
-
image: 'https://images.unsplash.com/photo-
|
|
34
|
+
image: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=800&auto=format&fit=crop&q=80',
|
|
35
35
|
description: 'Precision 50mm dynamic drivers engineered for ultra-flat frequency response, critical mixing, and executive focus.',
|
|
36
|
-
features: ['Planar Magnetic
|
|
36
|
+
features: ['Planar Magnetic Drivers', 'Memory Foam Cushions', 'Detachable Braided Cable', 'Hard Travel Case'],
|
|
37
37
|
inStock: true,
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -43,9 +43,9 @@ export const PRODUCTS: Product[] = [
|
|
|
43
43
|
category: 'Peripherals',
|
|
44
44
|
rating: 4.7,
|
|
45
45
|
reviewsCount: 210,
|
|
46
|
-
image: 'https://images.unsplash.com/photo-
|
|
46
|
+
image: 'https://images.unsplash.com/photo-1615663245857-ac93bb7c39e7?w=800&auto=format&fit=crop&q=80',
|
|
47
47
|
description: 'Sculpted ergonomic contour designed to eliminate wrist fatigue during extended development sessions.',
|
|
48
|
-
features: ['26,000 DPI Optical Sensor', '70-day Battery Life', 'Infinite Scroll Wheel', '
|
|
48
|
+
features: ['26,000 DPI Optical Sensor', '70-day Battery Life', 'Infinite Magnetic Scroll Wheel', 'Silent Click Switches'],
|
|
49
49
|
inStock: true,
|
|
50
50
|
},
|
|
51
51
|
{
|
|
@@ -55,8 +55,8 @@ export const PRODUCTS: Product[] = [
|
|
|
55
55
|
category: 'Desk Setup',
|
|
56
56
|
rating: 5.0,
|
|
57
57
|
reviewsCount: 62,
|
|
58
|
-
image: 'https://images.unsplash.com/photo-
|
|
59
|
-
description: 'Handcrafted from single-origin American walnut with cork feet and an integrated aluminum tray for accessories.',
|
|
58
|
+
image: 'https://images.unsplash.com/photo-1593062096033-9a26b09da705?w=800&auto=format&fit=crop&q=80',
|
|
59
|
+
description: 'Handcrafted from single-origin American walnut with cork feet and an integrated aluminum tray for desk accessories.',
|
|
60
60
|
features: ['Sustainable American Walnut', 'Holds up to 60 lbs', 'Integrated Cable Troughs', 'Hand-finished Matte Oil'],
|
|
61
61
|
inStock: true,
|
|
62
62
|
},
|
|
@@ -67,7 +67,7 @@ export const PRODUCTS: Product[] = [
|
|
|
67
67
|
category: 'Desk Setup',
|
|
68
68
|
rating: 4.6,
|
|
69
69
|
reviewsCount: 175,
|
|
70
|
-
image: 'https://images.unsplash.com/photo-
|
|
70
|
+
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=800&auto=format&fit=crop&q=80',
|
|
71
71
|
description: 'Water-resistant, stain-proof premium desk surface that provides effortless mouse tracking and protects your workspace.',
|
|
72
72
|
features: ['Water-resistant PU Leather', 'Anti-slip Suede Backing', '90cm x 45cm Large Size', 'Easy to Clean'],
|
|
73
73
|
inStock: true,
|
|
@@ -79,9 +79,53 @@ export const PRODUCTS: Product[] = [
|
|
|
79
79
|
category: 'Accessories',
|
|
80
80
|
rating: 4.9,
|
|
81
81
|
reviewsCount: 340,
|
|
82
|
-
image: 'https://images.unsplash.com/photo-
|
|
82
|
+
image: 'https://images.unsplash.com/photo-1622434641406-a158123450f9?w=800&auto=format&fit=crop&q=80',
|
|
83
83
|
description: 'Ultra-durable Kevlar-reinforced charging cable supporting up to 240W Power Delivery and 40Gbps Thunderbolt data transfer.',
|
|
84
84
|
features: ['240W Power Delivery', 'E-Marker Smart Chip', 'Braided Kevlar Weave', '2-meter Length'],
|
|
85
85
|
inStock: true,
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns all products, querying the database if configured or falling back to static PRODUCTS.
|
|
91
|
+
*/
|
|
92
|
+
export async function getProducts(): Promise<Product[]> {
|
|
93
|
+
try {
|
|
94
|
+
const { db } = await import('../db/index.js');
|
|
95
|
+
const { products } = await import('../db/schema.js');
|
|
96
|
+
const rows = await db.select().from(products);
|
|
97
|
+
if (rows && rows.length > 0) {
|
|
98
|
+
return rows.map((r: any) => ({
|
|
99
|
+
...r,
|
|
100
|
+
features: typeof r.features === 'string' ? JSON.parse(r.features) : r.features,
|
|
101
|
+
inStock: Boolean(r.inStock),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Database not attached, fallback
|
|
106
|
+
}
|
|
107
|
+
return PRODUCTS;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns a single product by ID from the database or static store.
|
|
112
|
+
*/
|
|
113
|
+
export async function getProduct(id: string): Promise<Product | undefined> {
|
|
114
|
+
try {
|
|
115
|
+
const { db } = await import('../db/index.js');
|
|
116
|
+
const { products } = await import('../db/schema.js');
|
|
117
|
+
const { eq } = await import('drizzle-orm');
|
|
118
|
+
const rows = await db.select().from(products).where(eq(products.id, id));
|
|
119
|
+
if (rows && rows.length > 0) {
|
|
120
|
+
const r = rows[0];
|
|
121
|
+
return {
|
|
122
|
+
...r,
|
|
123
|
+
features: typeof r.features === 'string' ? JSON.parse(r.features) : r.features,
|
|
124
|
+
inStock: Boolean(r.inStock),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Database not attached, fallback
|
|
129
|
+
}
|
|
130
|
+
return PRODUCTS.find((p) => p.id === id);
|
|
131
|
+
}
|
|
@@ -4,28 +4,52 @@
|
|
|
4
4
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
5
5
|
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
6
6
|
|
|
7
|
-
--color-brand-primary: #
|
|
8
|
-
--color-brand-hover: #
|
|
7
|
+
--color-brand-primary: #f4f4f5;
|
|
8
|
+
--color-brand-hover: #ffffff;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
:root {
|
|
12
12
|
color-scheme: dark;
|
|
13
|
+
--bg-canvas: #090a0f;
|
|
14
|
+
--bg-surface: #0f1117;
|
|
15
|
+
--bg-elevated: #151822;
|
|
16
|
+
--border-subtle: rgba(255, 255, 255, 0.08);
|
|
17
|
+
--border-strong: rgba(255, 255, 255, 0.16);
|
|
18
|
+
--text-primary: #f4f4f5;
|
|
19
|
+
--text-secondary: #9496a1;
|
|
20
|
+
--text-muted: #5c6370;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
html {
|
|
16
|
-
background-color:
|
|
17
|
-
color:
|
|
24
|
+
background-color: var(--bg-canvas);
|
|
25
|
+
color: var(--text-primary);
|
|
18
26
|
font-family: var(--font-sans);
|
|
19
27
|
scroll-behavior: smooth;
|
|
20
28
|
-webkit-font-smoothing: antialiased;
|
|
29
|
+
-moz-osx-font-smoothing: grayscale;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
/* SVG safety
|
|
32
|
+
/* Strict SVG safety to prevent flex ballooning */
|
|
24
33
|
svg {
|
|
25
34
|
flex-shrink: 0;
|
|
26
35
|
max-width: 100%;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/* Stripe/Linear refined product card */
|
|
39
|
+
.product-card {
|
|
40
|
+
background-color: var(--bg-surface);
|
|
41
|
+
border: 1px solid var(--border-subtle);
|
|
42
|
+
border-radius: 0.75rem;
|
|
43
|
+
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.04);
|
|
44
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.product-card:hover {
|
|
48
|
+
border-color: var(--border-strong);
|
|
49
|
+
transform: translateY(-2px);
|
|
50
|
+
box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.5), inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
/* Accessibility: Mandatory Reduced Motion */
|
|
30
54
|
@media (prefers-reduced-motion: reduce) {
|
|
31
55
|
*, *::before, *::after {
|