create-frootkit 1.2.9 → 1.2.11
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 +540 -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/lib/auth.ts +123 -0
- package/templates/blog/src/app/api/posts/route.ts +10 -34
- package/templates/blog/src/app/blog/[slug]/page.tsx +3 -3
- package/templates/blog/src/app/page.tsx +4 -3
- package/templates/blog/src/data/posts.ts +48 -0
- package/templates/ecommerce/src/app/api/products/route.ts +10 -26
- package/templates/ecommerce/src/app/page.tsx +5 -4
- package/templates/ecommerce/src/app/products/[id]/page.tsx +3 -3
- package/templates/ecommerce/src/data/products.ts +49 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/addons/database.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/addons/database.ts"],"names":[],"mappings":"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"}
|
package/dist/scaffold.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAKA,OAAO,
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAKA,OAAO,EAA4C,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAOzE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;IAC7C,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAiFvF"}
|
package/dist/scaffold.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { generateFrootKitTemplate, DEFAULT_GITIGNORE } from './template.js';
|
|
6
|
-
import { applyDatabaseAddon } from './addons/database.js';
|
|
6
|
+
import { applyDatabaseAddon, seedPhysicalDatabase } from './addons/database.js';
|
|
7
7
|
import { applyLinterAddon } from './addons/linter.js';
|
|
8
8
|
import { applyAgentAddon } from './addons/agent.js';
|
|
9
9
|
import { applyDockerAddon } from './addons/docker.js';
|
|
@@ -37,6 +37,10 @@ export async function scaffoldProject(options) {
|
|
|
37
37
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
38
38
|
await fs.writeFile(fullPath, files[relPath], 'utf8');
|
|
39
39
|
}
|
|
40
|
+
// Pre-seed physical SQLite database with complete schemas and actual data
|
|
41
|
+
if (db !== 'none') {
|
|
42
|
+
seedPhysicalDatabase(targetDir, db, template);
|
|
43
|
+
}
|
|
40
44
|
writeSpinner.stop(`Created ${filePaths.length} project files`);
|
|
41
45
|
let gitInitialized = false;
|
|
42
46
|
if (gitInit) {
|
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAuB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -5,8 +5,12 @@ import {
|
|
|
5
5
|
parseCookies,
|
|
6
6
|
createSessionCookie,
|
|
7
7
|
clearSessionCookie,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
findUserByEmail,
|
|
9
|
+
findUserById,
|
|
10
|
+
saveUser,
|
|
11
|
+
findSession,
|
|
12
|
+
saveSession,
|
|
13
|
+
deleteSession,
|
|
10
14
|
type User,
|
|
11
15
|
} from '../../../lib/auth.js';
|
|
12
16
|
|
|
@@ -21,7 +25,7 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
21
25
|
);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
const session =
|
|
28
|
+
const session = await findSession(sessionToken);
|
|
25
29
|
if (!session) {
|
|
26
30
|
return Response.json(
|
|
27
31
|
{ authenticated: false, user: null },
|
|
@@ -29,8 +33,8 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
29
33
|
);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
36
|
+
const user = await findUserById(session.userId);
|
|
37
|
+
if (!user) {
|
|
34
38
|
return Response.json(
|
|
35
39
|
{ authenticated: false, user: null },
|
|
36
40
|
{ status: 401 }
|
|
@@ -38,7 +42,7 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
return Response.json(
|
|
41
|
-
{ authenticated: true, user
|
|
45
|
+
{ authenticated: true, user },
|
|
42
46
|
{ status: 200 }
|
|
43
47
|
);
|
|
44
48
|
}
|
|
@@ -51,7 +55,7 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
51
55
|
const cookies = parseCookies(req.headers.get('cookie'));
|
|
52
56
|
const token = cookies['froot_session'];
|
|
53
57
|
if (token) {
|
|
54
|
-
|
|
58
|
+
await deleteSession(token);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
return new Response(null, {
|
|
@@ -87,7 +91,8 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
if (action === 'signup') {
|
|
90
|
-
|
|
94
|
+
const existing = await findUserByEmail(email);
|
|
95
|
+
if (existing) {
|
|
91
96
|
return Response.json({ error: 'User already exists' }, { status: 409 });
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -100,10 +105,10 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
100
105
|
createdAt: new Date().toISOString(),
|
|
101
106
|
};
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
await saveUser(user, passwordHash);
|
|
104
109
|
|
|
105
110
|
const token = createSessionToken();
|
|
106
|
-
|
|
111
|
+
await saveSession({
|
|
107
112
|
id: 'sess_' + Math.random().toString(36).substring(2, 9),
|
|
108
113
|
token,
|
|
109
114
|
userId: user.id,
|
|
@@ -120,7 +125,7 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
// Default: Login
|
|
123
|
-
const record =
|
|
128
|
+
const record = await findUserByEmail(email);
|
|
124
129
|
if (!record) {
|
|
125
130
|
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
|
|
126
131
|
}
|
|
@@ -131,7 +136,7 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
const token = createSessionToken();
|
|
134
|
-
|
|
139
|
+
await saveSession({
|
|
135
140
|
id: 'sess_' + Math.random().toString(36).substring(2, 9),
|
|
136
141
|
token,
|
|
137
142
|
userId: record.user.id,
|
|
@@ -129,6 +129,129 @@ export function clearSessionCookie(): string {
|
|
|
129
129
|
return 'froot_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0';
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Finds user record by email from DB (with fallback to in-memory).
|
|
134
|
+
*/
|
|
135
|
+
export async function findUserByEmail(email: string): Promise<{ user: User; passwordHash: string } | null> {
|
|
136
|
+
try {
|
|
137
|
+
const { db } = await import('../db/index.js');
|
|
138
|
+
const { users } = await import('../db/schema.js');
|
|
139
|
+
const { eq } = await import('drizzle-orm');
|
|
140
|
+
const rows = await db.select().from(users).where(eq(users.email, email));
|
|
141
|
+
if (rows && rows.length > 0) {
|
|
142
|
+
const u = rows[0];
|
|
143
|
+
return {
|
|
144
|
+
user: {
|
|
145
|
+
id: u.id,
|
|
146
|
+
email: u.email,
|
|
147
|
+
name: u.name,
|
|
148
|
+
role: u.role as 'user' | 'admin',
|
|
149
|
+
createdAt: u.createdAt,
|
|
150
|
+
},
|
|
151
|
+
passwordHash: u.passwordHash,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
return USERS_STORE.get(email) || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Finds user record by ID.
|
|
160
|
+
*/
|
|
161
|
+
export async function findUserById(id: string): Promise<User | null> {
|
|
162
|
+
try {
|
|
163
|
+
const { db } = await import('../db/index.js');
|
|
164
|
+
const { users } = await import('../db/schema.js');
|
|
165
|
+
const { eq } = await import('drizzle-orm');
|
|
166
|
+
const rows = await db.select().from(users).where(eq(users.id, id));
|
|
167
|
+
if (rows && rows.length > 0) {
|
|
168
|
+
const u = rows[0];
|
|
169
|
+
return {
|
|
170
|
+
id: u.id,
|
|
171
|
+
email: u.email,
|
|
172
|
+
name: u.name,
|
|
173
|
+
role: u.role as 'user' | 'admin',
|
|
174
|
+
createdAt: u.createdAt,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
} catch {}
|
|
178
|
+
for (const entry of USERS_STORE.values()) {
|
|
179
|
+
if (entry.user.id === id) return entry.user;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Saves a new user to DB and in-memory store.
|
|
186
|
+
*/
|
|
187
|
+
export async function saveUser(user: User, passwordHash: string): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
const { db } = await import('../db/index.js');
|
|
190
|
+
const { users } = await import('../db/schema.js');
|
|
191
|
+
await db.insert(users).values({
|
|
192
|
+
id: user.id,
|
|
193
|
+
email: user.email,
|
|
194
|
+
name: user.name,
|
|
195
|
+
role: user.role,
|
|
196
|
+
passwordHash,
|
|
197
|
+
createdAt: user.createdAt,
|
|
198
|
+
});
|
|
199
|
+
} catch {}
|
|
200
|
+
USERS_STORE.set(user.email, { user, passwordHash });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Saves a session token to DB and in-memory store.
|
|
205
|
+
*/
|
|
206
|
+
export async function saveSession(session: Session): Promise<void> {
|
|
207
|
+
try {
|
|
208
|
+
const { db } = await import('../db/index.js');
|
|
209
|
+
const { sessions } = await import('../db/schema.js');
|
|
210
|
+
await db.insert(sessions).values({
|
|
211
|
+
id: session.id,
|
|
212
|
+
token: session.token,
|
|
213
|
+
userId: session.userId,
|
|
214
|
+
expiresAt: session.expiresAt,
|
|
215
|
+
});
|
|
216
|
+
} catch {}
|
|
217
|
+
SESSIONS_STORE.set(session.token, session);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Finds session by token.
|
|
222
|
+
*/
|
|
223
|
+
export async function findSession(token: string): Promise<Session | null> {
|
|
224
|
+
try {
|
|
225
|
+
const { db } = await import('../db/index.js');
|
|
226
|
+
const { sessions } = await import('../db/schema.js');
|
|
227
|
+
const { eq } = await import('drizzle-orm');
|
|
228
|
+
const rows = await db.select().from(sessions).where(eq(sessions.token, token));
|
|
229
|
+
if (rows && rows.length > 0) {
|
|
230
|
+
const s = rows[0];
|
|
231
|
+
return {
|
|
232
|
+
id: s.id,
|
|
233
|
+
token: s.token,
|
|
234
|
+
userId: s.userId,
|
|
235
|
+
expiresAt: s.expiresAt,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
} catch {}
|
|
239
|
+
return SESSIONS_STORE.get(token) || null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Deletes a session by token.
|
|
244
|
+
*/
|
|
245
|
+
export async function deleteSession(token: string): Promise<void> {
|
|
246
|
+
try {
|
|
247
|
+
const { db } = await import('../db/index.js');
|
|
248
|
+
const { sessions } = await import('../db/schema.js');
|
|
249
|
+
const { eq } = await import('drizzle-orm');
|
|
250
|
+
await db.delete(sessions).where(eq(sessions.token, token));
|
|
251
|
+
} catch {}
|
|
252
|
+
SESSIONS_STORE.delete(token);
|
|
253
|
+
}
|
|
254
|
+
|
|
132
255
|
// Pre-populate demo user: admin@frootkit.dev / password123
|
|
133
256
|
(async () => {
|
|
134
257
|
const demoHash = await hashPassword('password123');
|
|
@@ -1,36 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export async function GET(
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(),
|
|
1
|
+
import { getPosts } from '../../../data/posts.js';
|
|
2
|
+
|
|
3
|
+
export async function GET(): Promise<Response> {
|
|
4
|
+
const posts = await getPosts();
|
|
5
|
+
return Response.json(posts, {
|
|
6
|
+
status: 200,
|
|
7
|
+
headers: {
|
|
8
|
+
'content-type': 'application/json',
|
|
9
|
+
'cache-control': 'public, max-age=60',
|
|
28
10
|
},
|
|
29
|
-
|
|
30
|
-
status: 200,
|
|
31
|
-
headers: {
|
|
32
|
-
'content-type': 'application/json',
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
);
|
|
11
|
+
});
|
|
36
12
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { POSTS } from '../../../data/posts.js';
|
|
1
|
+
import { getPostBySlug, POSTS } from '../../../data/posts.js';
|
|
2
2
|
import { Navbar } from '../../../components/Navbar.js';
|
|
3
3
|
import { Footer } from '../../../components/Footer.js';
|
|
4
4
|
|
|
@@ -6,9 +6,9 @@ export async function generateStaticParams() {
|
|
|
6
6
|
return POSTS.map((post) => ({ slug: post.slug }));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export default function BlogPostPage({ params }: { params?: { slug?: string } }) {
|
|
9
|
+
export default async function BlogPostPage({ params }: { params?: { slug?: string } }) {
|
|
10
10
|
const slug = params?.slug || 'welcome-to-frootkit';
|
|
11
|
-
const post =
|
|
11
|
+
const post = (await getPostBySlug(slug)) || POSTS[0]!;
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
14
|
<div class="min-h-screen bg-[#08090a] text-zinc-100 flex flex-col justify-between selection:bg-indigo-500/20 selection:text-indigo-300">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { POSTS } from '../data/posts.js';
|
|
1
|
+
import { getPosts, POSTS } from '../data/posts.js';
|
|
2
2
|
import { Navbar } from '../components/Navbar.js';
|
|
3
3
|
import { PostCard } from '../components/PostCard.js';
|
|
4
4
|
import { Newsletter } from '../components/Newsletter.js';
|
|
@@ -9,8 +9,9 @@ export const metadata = {
|
|
|
9
9
|
description: 'Deep dives into Preact SSR, fullstack TypeScript, and native styling.',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export default function BlogHomePage() {
|
|
13
|
-
const
|
|
12
|
+
export default async function BlogHomePage() {
|
|
13
|
+
const allPosts = await getPosts();
|
|
14
|
+
const [featuredPost, ...recentPosts] = allPosts;
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
17
|
<div class="min-h-screen bg-[#08090a] text-zinc-100 flex flex-col justify-between selection:bg-indigo-500/20 selection:text-indigo-300">
|
|
@@ -73,3 +73,51 @@ export const POSTS: Post[] = [
|
|
|
73
73
|
],
|
|
74
74
|
},
|
|
75
75
|
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns all articles, querying the database if configured or falling back to static POSTS.
|
|
79
|
+
*/
|
|
80
|
+
export async function getPosts(): Promise<Post[]> {
|
|
81
|
+
try {
|
|
82
|
+
const { db } = await import('../db/index.js');
|
|
83
|
+
const { posts, authors } = await import('../db/schema.js');
|
|
84
|
+
const dbPosts = await db.select().from(posts);
|
|
85
|
+
if (dbPosts && dbPosts.length > 0) {
|
|
86
|
+
const allAuthors = await db.select().from(authors);
|
|
87
|
+
const authorMap = new Map(allAuthors.map((a: any) => [a.id, a]));
|
|
88
|
+
return dbPosts.map((p: any) => {
|
|
89
|
+
const auth = authorMap.get(p.authorId) || {
|
|
90
|
+
name: 'Alex Rivera',
|
|
91
|
+
role: 'Staff Systems Architect',
|
|
92
|
+
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&h=100&fit=crop&crop=faces',
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
slug: p.slug,
|
|
96
|
+
title: p.title,
|
|
97
|
+
summary: p.summary,
|
|
98
|
+
date: 'Recent',
|
|
99
|
+
readTime: p.readTime,
|
|
100
|
+
category: p.category,
|
|
101
|
+
author: {
|
|
102
|
+
name: auth.name,
|
|
103
|
+
role: auth.role,
|
|
104
|
+
avatar: auth.avatar,
|
|
105
|
+
},
|
|
106
|
+
tags: ['FrootKit', 'Architecture', 'TypeScript'],
|
|
107
|
+
content: typeof p.content === 'string' ? JSON.parse(p.content) : p.content,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Database not attached, fallback
|
|
113
|
+
}
|
|
114
|
+
return POSTS;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns a single article by slug.
|
|
119
|
+
*/
|
|
120
|
+
export async function getPostBySlug(slug: string): Promise<Post | undefined> {
|
|
121
|
+
const allPosts = await getPosts();
|
|
122
|
+
return allPosts.find((p) => p.slug === slug);
|
|
123
|
+
}
|
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export async function GET(
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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(),
|
|
1
|
+
import { getProducts } from '../../../data/products.js';
|
|
2
|
+
|
|
3
|
+
export async function GET(): Promise<Response> {
|
|
4
|
+
const products = await getProducts();
|
|
5
|
+
return Response.json(products, {
|
|
6
|
+
status: 200,
|
|
7
|
+
headers: {
|
|
8
|
+
'content-type': 'application/json',
|
|
9
|
+
'cache-control': 'public, max-age=60',
|
|
20
10
|
},
|
|
21
|
-
|
|
22
|
-
status: 200,
|
|
23
|
-
headers: {
|
|
24
|
-
'content-type': 'application/json',
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
);
|
|
11
|
+
});
|
|
28
12
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -8,7 +8,8 @@ export const metadata = {
|
|
|
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();
|
|
12
13
|
const categories = ['All Products', 'Keyboards', 'Audio', 'Peripherals', 'Desk Setup', 'Accessories'];
|
|
13
14
|
|
|
14
15
|
return (
|
|
@@ -55,12 +56,12 @@ export default function StoreHomePage() {
|
|
|
55
56
|
Featured Instruments
|
|
56
57
|
</h2>
|
|
57
58
|
<span class="text-xs text-zinc-500 tabular-nums">
|
|
58
|
-
{
|
|
59
|
+
{products.length} products available
|
|
59
60
|
</span>
|
|
60
61
|
</div>
|
|
61
62
|
|
|
62
63
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
63
|
-
{
|
|
64
|
+
{products.map((product) => (
|
|
64
65
|
<ProductCard key={product.id} product={product} />
|
|
65
66
|
))}
|
|
66
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,9 +6,9 @@ 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
14
|
<div class="min-h-screen bg-[#090a0f] text-zinc-100 flex flex-col justify-between selection:bg-white/20 selection:text-white">
|
|
@@ -19,8 +19,7 @@ export const PRODUCTS: Product[] = [
|
|
|
19
19
|
category: 'Keyboards',
|
|
20
20
|
rating: 4.9,
|
|
21
21
|
reviewsCount: 128,
|
|
22
|
-
|
|
23
|
-
image: 'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=800&auto=format&fit=crop&q=80',
|
|
22
|
+
image: 'https://images.unsplash.com/photo-1595225476474-87563907a212?w=800&auto=format&fit=crop&q=80',
|
|
24
23
|
description: 'Aircraft-grade CNC anodized aluminum chassis with custom factory-lubed linear switches and sound-dampening silicone base.',
|
|
25
24
|
features: ['Hot-swappable switches', 'Bluetooth 5.3 & 2.4GHz wireless', 'CNC Aluminum Unibody', 'PBT Dye-Sub Keycaps'],
|
|
26
25
|
inStock: true,
|
|
@@ -32,8 +31,7 @@ export const PRODUCTS: Product[] = [
|
|
|
32
31
|
category: 'Audio',
|
|
33
32
|
rating: 4.8,
|
|
34
33
|
reviewsCount: 94,
|
|
35
|
-
|
|
36
|
-
image: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=800&auto=format&fit=crop&q=80',
|
|
34
|
+
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800&auto=format&fit=crop&q=80',
|
|
37
35
|
description: 'Precision 50mm dynamic drivers engineered for ultra-flat frequency response, critical mixing, and executive focus.',
|
|
38
36
|
features: ['Planar Magnetic Drivers', 'Memory Foam Cushions', 'Detachable Braided Cable', 'Hard Travel Case'],
|
|
39
37
|
inStock: true,
|
|
@@ -45,7 +43,6 @@ export const PRODUCTS: Product[] = [
|
|
|
45
43
|
category: 'Peripherals',
|
|
46
44
|
rating: 4.7,
|
|
47
45
|
reviewsCount: 210,
|
|
48
|
-
// Matte dark ergonomic mouse on neutral slate background
|
|
49
46
|
image: 'https://images.unsplash.com/photo-1615663245857-ac93bb7c39e7?w=800&auto=format&fit=crop&q=80',
|
|
50
47
|
description: 'Sculpted ergonomic contour designed to eliminate wrist fatigue during extended development sessions.',
|
|
51
48
|
features: ['26,000 DPI Optical Sensor', '70-day Battery Life', 'Infinite Magnetic Scroll Wheel', 'Silent Click Switches'],
|
|
@@ -58,8 +55,7 @@ export const PRODUCTS: Product[] = [
|
|
|
58
55
|
category: 'Desk Setup',
|
|
59
56
|
rating: 5.0,
|
|
60
57
|
reviewsCount: 62,
|
|
61
|
-
|
|
62
|
-
image: 'https://images.unsplash.com/photo-1593062096033-9a26b09da705?w=800&auto=format&fit=crop&q=80',
|
|
58
|
+
image: 'https://images.unsplash.com/photo-1586775490184-b79f0621891f?w=800&auto=format&fit=crop&q=80',
|
|
63
59
|
description: 'Handcrafted from single-origin American walnut with cork feet and an integrated aluminum tray for desk accessories.',
|
|
64
60
|
features: ['Sustainable American Walnut', 'Holds up to 60 lbs', 'Integrated Cable Troughs', 'Hand-finished Matte Oil'],
|
|
65
61
|
inStock: true,
|
|
@@ -71,8 +67,7 @@ export const PRODUCTS: Product[] = [
|
|
|
71
67
|
category: 'Desk Setup',
|
|
72
68
|
rating: 4.6,
|
|
73
69
|
reviewsCount: 175,
|
|
74
|
-
|
|
75
|
-
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=800&auto=format&fit=crop&q=80',
|
|
70
|
+
image: 'https://images.unsplash.com/photo-1581291518857-4e27b48ff24e?w=800&auto=format&fit=crop&q=80',
|
|
76
71
|
description: 'Water-resistant, stain-proof premium desk surface that provides effortless mouse tracking and protects your workspace.',
|
|
77
72
|
features: ['Water-resistant PU Leather', 'Anti-slip Suede Backing', '90cm x 45cm Large Size', 'Easy to Clean'],
|
|
78
73
|
inStock: true,
|
|
@@ -84,10 +79,53 @@ export const PRODUCTS: Product[] = [
|
|
|
84
79
|
category: 'Accessories',
|
|
85
80
|
rating: 4.9,
|
|
86
81
|
reviewsCount: 340,
|
|
87
|
-
|
|
88
|
-
image: 'https://images.unsplash.com/photo-1622434641406-a158123450f9?w=800&auto=format&fit=crop&q=80',
|
|
82
|
+
image: 'https://images.unsplash.com/photo-1583863788434-e58a36330cf0?w=800&auto=format&fit=crop&q=80',
|
|
89
83
|
description: 'Ultra-durable Kevlar-reinforced charging cable supporting up to 240W Power Delivery and 40Gbps Thunderbolt data transfer.',
|
|
90
84
|
features: ['240W Power Delivery', 'E-Marker Smart Chip', 'Braided Kevlar Weave', '2-meter Length'],
|
|
91
85
|
inStock: true,
|
|
92
86
|
},
|
|
93
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
|
+
}
|