bestraw 1.0.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.
Files changed (66) hide show
  1. package/index.mjs +436 -0
  2. package/package.json +17 -0
  3. package/templates/.env.example +51 -0
  4. package/templates/Caddyfile +21 -0
  5. package/templates/docker-compose.yml +80 -0
  6. package/templates/web/Dockerfile +19 -0
  7. package/templates/web/next-env.d.ts +6 -0
  8. package/templates/web/next.config.ts +10 -0
  9. package/templates/web/node_modules/.bin/next +17 -0
  10. package/templates/web/node_modules/.bin/tsc +17 -0
  11. package/templates/web/node_modules/.bin/tsserver +17 -0
  12. package/templates/web/package.json +28 -0
  13. package/templates/web/postcss.config.mjs +8 -0
  14. package/templates/web/public/images/.gitkeep +0 -0
  15. package/templates/web/src/app/[locale]/auth/page.tsx +222 -0
  16. package/templates/web/src/app/[locale]/blog/[slug]/page.tsx +104 -0
  17. package/templates/web/src/app/[locale]/blog/page.tsx +90 -0
  18. package/templates/web/src/app/[locale]/error.tsx +41 -0
  19. package/templates/web/src/app/[locale]/info/page.tsx +186 -0
  20. package/templates/web/src/app/[locale]/layout.tsx +86 -0
  21. package/templates/web/src/app/[locale]/loyalty/page.tsx +135 -0
  22. package/templates/web/src/app/[locale]/menu/page.tsx +69 -0
  23. package/templates/web/src/app/[locale]/order/cart/page.tsx +199 -0
  24. package/templates/web/src/app/[locale]/order/checkout/page.tsx +489 -0
  25. package/templates/web/src/app/[locale]/order/confirmation/[id]/page.tsx +159 -0
  26. package/templates/web/src/app/[locale]/order/page.tsx +207 -0
  27. package/templates/web/src/app/[locale]/page.tsx +119 -0
  28. package/templates/web/src/app/globals.css +11 -0
  29. package/templates/web/src/app/robots.ts +14 -0
  30. package/templates/web/src/app/sitemap.ts +56 -0
  31. package/templates/web/src/bestraw.config.ts +9 -0
  32. package/templates/web/src/components/auth/OtpForm.tsx +98 -0
  33. package/templates/web/src/components/blog/ArticleCard.tsx +67 -0
  34. package/templates/web/src/components/blog/ArticleContent.tsx +14 -0
  35. package/templates/web/src/components/cart/CartDrawer.tsx +152 -0
  36. package/templates/web/src/components/cart/CartItem.tsx +111 -0
  37. package/templates/web/src/components/checkout/StripePaymentForm.tsx +54 -0
  38. package/templates/web/src/components/layout/Footer.tsx +40 -0
  39. package/templates/web/src/components/layout/Header.tsx +240 -0
  40. package/templates/web/src/components/layout/LocaleSwitcher.tsx +34 -0
  41. package/templates/web/src/components/loyalty/PointsBalance.tsx +96 -0
  42. package/templates/web/src/components/loyalty/RewardCard.tsx +73 -0
  43. package/templates/web/src/components/loyalty/TransactionHistory.tsx +108 -0
  44. package/templates/web/src/components/menu/CategorySection.tsx +42 -0
  45. package/templates/web/src/components/menu/MealCard.tsx +55 -0
  46. package/templates/web/src/components/menu/MealDetailModal.tsx +355 -0
  47. package/templates/web/src/components/menu/MenuContent.tsx +216 -0
  48. package/templates/web/src/components/order/MealOrderCard.tsx +220 -0
  49. package/templates/web/src/components/order/OrderStatusTracker.tsx +138 -0
  50. package/templates/web/src/components/order/PaymentStatus.tsx +62 -0
  51. package/templates/web/src/components/ui/Button.tsx +40 -0
  52. package/templates/web/src/components/ui/ErrorAlert.tsx +15 -0
  53. package/templates/web/src/i18n/config.ts +3 -0
  54. package/templates/web/src/i18n/request.ts +13 -0
  55. package/templates/web/src/i18n/routing.ts +10 -0
  56. package/templates/web/src/lib/client.ts +5 -0
  57. package/templates/web/src/lib/errors.ts +31 -0
  58. package/templates/web/src/lib/features.ts +10 -0
  59. package/templates/web/src/lib/hooks/useCustomerClient.ts +28 -0
  60. package/templates/web/src/lib/hooks/useMenu.ts +46 -0
  61. package/templates/web/src/messages/en.json +283 -0
  62. package/templates/web/src/messages/fr.json +283 -0
  63. package/templates/web/src/middleware.ts +8 -0
  64. package/templates/web/src/providers/CartProvider.tsx +162 -0
  65. package/templates/web/src/providers/StripeProvider.tsx +21 -0
  66. package/templates/web/tsconfig.json +27 -0
package/index.mjs ADDED
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { resolve, dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createInterface } from 'node:readline';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const TEMPLATES_DIR = join(__dirname, 'templates');
11
+
12
+ function ask(rl, question, defaultValue) {
13
+ return new Promise((resolve) => {
14
+ const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
15
+ rl.question(prompt, (answer) => {
16
+ resolve(answer.trim() || defaultValue || '');
17
+ });
18
+ });
19
+ }
20
+
21
+ function generateSecret() {
22
+ return randomBytes(16).toString('base64');
23
+ }
24
+
25
+ function copyDir(src, dest, exclude = []) {
26
+ cpSync(src, dest, {
27
+ recursive: true,
28
+ filter: (source) => {
29
+ const name = source.split('/').pop();
30
+ return !exclude.includes(name);
31
+ },
32
+ });
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Incremental mode: --add=feature1,feature2
37
+ // ---------------------------------------------------------------------------
38
+
39
+ async function incrementalAdd(projectDir, features) {
40
+ const configPath = join(projectDir, 'web', 'src', 'bestraw.config.ts');
41
+ const envPath = join(projectDir, '.env');
42
+
43
+ if (!existsSync(configPath)) {
44
+ console.error(`\nErreur: ${configPath} introuvable. Ce n'est pas un projet BeStraw.\n`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // Read current config
49
+ const configContent = readFileSync(configPath, 'utf-8');
50
+ const current = {
51
+ ordering: /ordering:\s*true/.test(configContent),
52
+ loyalty: /loyalty:\s*true/.test(configContent),
53
+ payments: /payments:\s*true/.test(configContent),
54
+ blog: /blog:\s*true/.test(configContent),
55
+ authPhone: /phone:\s*true/.test(configContent),
56
+ authEmail: /email:\s*true/.test(configContent),
57
+ };
58
+
59
+ const added = [];
60
+ const validFeatures = ['ordering', 'loyalty', 'payments', 'blog', 'auth-phone', 'auth-email'];
61
+
62
+ for (const f of features) {
63
+ if (!validFeatures.includes(f)) {
64
+ console.error(` Feature inconnue: ${f}`);
65
+ console.error(` Disponibles: ${validFeatures.join(', ')}`);
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ // Apply requested features
71
+ if (features.includes('ordering') && !current.ordering) {
72
+ current.ordering = true;
73
+ current.payments = true; // auto-enable payments with ordering
74
+ added.push('ordering', 'payments');
75
+ }
76
+ if (features.includes('loyalty') && !current.loyalty) {
77
+ current.loyalty = true;
78
+ added.push('loyalty');
79
+ }
80
+ if (features.includes('payments') && !current.payments) {
81
+ current.payments = true;
82
+ added.push('payments');
83
+ }
84
+ if (features.includes('auth-phone') && !current.authPhone) {
85
+ current.authPhone = true;
86
+ added.push('auth-phone');
87
+ }
88
+ if (features.includes('auth-email') && !current.authEmail) {
89
+ current.authEmail = true;
90
+ added.push('auth-email');
91
+ }
92
+ if (features.includes('blog') && !current.blog) {
93
+ current.blog = true;
94
+ added.push('blog');
95
+ }
96
+
97
+ // Auto-enable auth if ordering or loyalty requires it
98
+ if ((current.ordering || current.loyalty) && !current.authPhone && !current.authEmail) {
99
+ current.authPhone = true;
100
+ added.push('auth-phone (auto)');
101
+ }
102
+
103
+ if (added.length === 0) {
104
+ console.log('\n Toutes les features demandees sont deja activees.\n');
105
+ return;
106
+ }
107
+
108
+ // Ask provider questions if needed
109
+ let smsProvider = 'console';
110
+ let emailProvider = 'console';
111
+ const needsQuestions = features.includes('auth-phone') || features.includes('auth-email') ||
112
+ ((current.ordering || current.loyalty) && !current.authPhone && !current.authEmail);
113
+
114
+ if (needsQuestions) {
115
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
116
+
117
+ if (current.authPhone && added.includes('auth-phone')) {
118
+ smsProvider = await ask(rl, ' Fournisseur SMS (console/twilio)', 'console');
119
+ }
120
+ if (current.authEmail && added.includes('auth-email')) {
121
+ emailProvider = await ask(rl, ' Fournisseur email (console/smtp/sendgrid)', 'console');
122
+ }
123
+
124
+ rl.close();
125
+ }
126
+
127
+ // Write updated config
128
+ const newConfig = generateConfig(current);
129
+ writeFileSync(configPath, newConfig);
130
+
131
+ // Update .env
132
+ if (existsSync(envPath)) {
133
+ let env = readFileSync(envPath, 'utf-8');
134
+ const updates = {
135
+ FEATURE_ORDERING: current.ordering,
136
+ FEATURE_PAYMENTS: current.payments,
137
+ FEATURE_LOYALTY: current.loyalty,
138
+ AUTH_PHONE: current.authPhone,
139
+ AUTH_EMAIL: current.authEmail,
140
+ };
141
+
142
+ for (const [key, value] of Object.entries(updates)) {
143
+ const regex = new RegExp(`^${key}=.*$`, 'm');
144
+ if (regex.test(env)) {
145
+ env = env.replace(regex, `${key}=${value}`);
146
+ } else {
147
+ env += `\n${key}=${value}`;
148
+ }
149
+ }
150
+
151
+ // Add provider vars if missing
152
+ if (current.authPhone && !env.includes('SMS_PROVIDER=')) {
153
+ env += `\n\n# --- SMS ---\nSMS_PROVIDER=${smsProvider}\nSMS_ACCOUNT_SID=\nSMS_AUTH_TOKEN=\nSMS_FROM=`;
154
+ }
155
+ if (current.authEmail && !env.includes('EMAIL_PROVIDER=')) {
156
+ env += `\n\n# --- Email ---\nEMAIL_PROVIDER=${emailProvider}\nEMAIL_FROM=noreply@localhost\nSMTP_HOST=\nSMTP_PORT=587\nSMTP_USER=\nSMTP_PASS=\nSENDGRID_API_KEY=`;
157
+ }
158
+
159
+ writeFileSync(envPath, env);
160
+ }
161
+
162
+ console.log(`\n Features ajoutees: ${added.join(', ')}`);
163
+ console.log(` Config mis a jour: web/src/bestraw.config.ts`);
164
+ if (existsSync(envPath)) console.log(` Env mis a jour: .env`);
165
+ console.log();
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Config generation
170
+ // ---------------------------------------------------------------------------
171
+
172
+ function generateConfig({ ordering, loyalty, payments, blog, authPhone, authEmail }) {
173
+ return `export const config = {
174
+ features: {
175
+ ordering: ${ordering},
176
+ loyalty: ${loyalty},
177
+ payments: ${payments},
178
+ blog: ${blog || false},
179
+ auth: { phone: ${authPhone}, email: ${authEmail} },
180
+ },
181
+ } as const;
182
+ `;
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Main: new project creation
187
+ // ---------------------------------------------------------------------------
188
+
189
+ async function main() {
190
+ const projectName = process.argv[2];
191
+
192
+ if (!projectName) {
193
+ console.error('\nUsage: npx bestraw <nom-du-projet>\n');
194
+ console.error('Options:');
195
+ console.error(' --yes, -y Utiliser les valeurs par defaut');
196
+ console.error(' --add=f1,f2,... Ajouter des features a un projet existant');
197
+ console.error(' Features: ordering, loyalty, payments, blog, auth-phone, auth-email\n');
198
+ process.exit(1);
199
+ }
200
+
201
+ const projectDir = resolve(process.cwd(), projectName);
202
+
203
+ // Check for incremental mode
204
+ const addFlag = process.argv.find((a) => a.startsWith('--add='));
205
+ if (addFlag) {
206
+ const features = addFlag.replace('--add=', '').split(',').map((f) => f.trim()).filter(Boolean);
207
+ if (features.length === 0) {
208
+ console.error('\nErreur: specifiez les features a ajouter (--add=ordering,loyalty)\n');
209
+ process.exit(1);
210
+ }
211
+ if (!existsSync(projectDir)) {
212
+ console.error(`\nErreur: le dossier "${projectName}" n'existe pas.\n`);
213
+ process.exit(1);
214
+ }
215
+ await incrementalAdd(projectDir, features);
216
+ return;
217
+ }
218
+
219
+ if (existsSync(projectDir)) {
220
+ console.error(`\nErreur: le dossier "${projectName}" existe deja.`);
221
+ console.error(`Pour ajouter des features: npx bestraw ${projectName} --add=ordering,loyalty\n`);
222
+ process.exit(1);
223
+ }
224
+
225
+ const useDefaults = process.argv.includes('--yes') || process.argv.includes('-y');
226
+
227
+ let restaurantName = projectName;
228
+ let domain = `${projectName}.com`;
229
+ let ordering = false;
230
+ let payments = false;
231
+ let loyalty = false;
232
+ let kitchen = false;
233
+ let authPhone = true;
234
+ let authEmail = false;
235
+ let stripeKey = '';
236
+ let smsProvider = 'console';
237
+ let smsAccountSid = '';
238
+ let smsAuthToken = '';
239
+ let smsFrom = '';
240
+ let emailProvider = 'console';
241
+ let emailFrom = '';
242
+ let smtpHost = '';
243
+ let smtpPort = '587';
244
+ let smtpUser = '';
245
+ let smtpPass = '';
246
+ let sendgridApiKey = '';
247
+
248
+ if (useDefaults) {
249
+ console.log(`\n BeStraw - Plateforme Restaurant`);
250
+ console.log(` Utilisation des valeurs par defaut\n`);
251
+ } else {
252
+ console.log(`\n BeStraw - Plateforme Restaurant\n`);
253
+
254
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
255
+
256
+ restaurantName = await ask(rl, ' Nom du restaurant', projectName);
257
+ domain = await ask(rl, ' Domaine', `${projectName}.com`);
258
+
259
+ console.log('\n Fonctionnalites (o/n):');
260
+ ordering = (await ask(rl, ' Commandes en ligne', 'n')).toLowerCase() === 'o';
261
+ payments = ordering ? true : (await ask(rl, ' Paiement Stripe', 'n')).toLowerCase() === 'o';
262
+ loyalty = (await ask(rl, ' Programme de fidelite', 'n')).toLowerCase() === 'o';
263
+ kitchen = (await ask(rl, ' Ecran cuisine (KDS)', 'n')).toLowerCase() === 'o';
264
+
265
+ // Auth methods
266
+ if (ordering || loyalty) {
267
+ console.log('\n Authentification (au moins une methode requise):');
268
+ authPhone = (await ask(rl, ' Auth par telephone', 'o')).toLowerCase() === 'o';
269
+ authEmail = (await ask(rl, ' Auth par email', 'n')).toLowerCase() === 'o';
270
+ // Ensure at least one auth method
271
+ if (!authPhone && !authEmail) {
272
+ console.log(' → Auth telephone active par defaut');
273
+ authPhone = true;
274
+ }
275
+ } else {
276
+ authPhone = false;
277
+ authEmail = false;
278
+ }
279
+
280
+ // SMS provider
281
+ if (authPhone) {
282
+ console.log('\n Configuration SMS:');
283
+ smsProvider = await ask(rl, ' Fournisseur (console/twilio)', 'console');
284
+ if (smsProvider === 'twilio') {
285
+ smsAccountSid = await ask(rl, ' Twilio Account SID', '');
286
+ smsAuthToken = await ask(rl, ' Twilio Auth Token', '');
287
+ smsFrom = await ask(rl, ' Numero expediteur (+33...)', '');
288
+ }
289
+ }
290
+
291
+ // Email provider
292
+ if (authEmail) {
293
+ console.log('\n Configuration email:');
294
+ emailProvider = await ask(rl, ' Fournisseur (console/smtp/sendgrid)', 'console');
295
+ if (emailProvider === 'smtp') {
296
+ emailFrom = await ask(rl, ' Email expediteur', `noreply@${domain}`);
297
+ smtpHost = await ask(rl, ' Serveur SMTP', '');
298
+ smtpPort = await ask(rl, ' Port SMTP', '587');
299
+ smtpUser = await ask(rl, ' Utilisateur SMTP', '');
300
+ smtpPass = await ask(rl, ' Mot de passe SMTP', '');
301
+ } else if (emailProvider === 'sendgrid') {
302
+ emailFrom = await ask(rl, ' Email expediteur', `noreply@${domain}`);
303
+ sendgridApiKey = await ask(rl, ' Cle API SendGrid', '');
304
+ }
305
+ }
306
+
307
+ if (payments) {
308
+ stripeKey = await ask(rl, '\n Cle Stripe secret (sk_...)', '');
309
+ }
310
+
311
+ rl.close();
312
+ }
313
+
314
+ // Create project directory
315
+ mkdirSync(projectDir, { recursive: true });
316
+
317
+ // Copy docker-compose.yml
318
+ cpSync(join(TEMPLATES_DIR, 'docker-compose.yml'), join(projectDir, 'docker-compose.yml'));
319
+
320
+ // Copy Caddyfile
321
+ cpSync(join(TEMPLATES_DIR, 'Caddyfile'), join(projectDir, 'Caddyfile'));
322
+
323
+ // Copy web template
324
+ copyDir(join(TEMPLATES_DIR, 'web'), join(projectDir, 'web'), ['node_modules', '.next', 'dist']);
325
+
326
+ // Update web package.json — replace workspace:* with latest published version
327
+ const webPkgPath = join(projectDir, 'web', 'package.json');
328
+ const webPkg = JSON.parse(readFileSync(webPkgPath, 'utf-8'));
329
+ if (webPkg.dependencies?.['bestraw-sdk'] === 'workspace:*') {
330
+ webPkg.dependencies['bestraw-sdk'] = 'latest';
331
+ }
332
+ writeFileSync(webPkgPath, JSON.stringify(webPkg, null, 2) + '\n');
333
+
334
+ // Generate bestraw.config.ts
335
+ const configContent = generateConfig({ ordering, loyalty, payments, authPhone, authEmail });
336
+ writeFileSync(join(projectDir, 'web', 'src', 'bestraw.config.ts'), configContent);
337
+
338
+ // Generate .env
339
+ const env = `# BeStraw — ${restaurantName}
340
+ # Genere le ${new Date().toISOString().split('T')[0]}
341
+
342
+ # --- General ---
343
+ VERSION=latest
344
+ PUBLIC_URL=https://${domain}
345
+ RESTAURANT_NAME=${restaurantName}
346
+
347
+ # --- Database ---
348
+ DB_NAME=bestraw
349
+ DB_USER=strapi
350
+ DB_PASSWORD=${generateSecret()}
351
+
352
+ # --- Strapi Secrets ---
353
+ APP_KEYS=${generateSecret()},${generateSecret()},${generateSecret()},${generateSecret()}
354
+ API_TOKEN_SALT=${generateSecret()}
355
+ ADMIN_JWT_SECRET=${generateSecret()}
356
+ JWT_SECRET=${generateSecret()}
357
+ TRANSFER_TOKEN_SALT=${generateSecret()}
358
+
359
+ # --- Ports ---
360
+ API_PORT=1337
361
+ WEB_PORT=3000
362
+ KITCHEN_PORT=3001
363
+
364
+ # --- Features ---
365
+ FEATURE_ORDERING=${ordering}
366
+ FEATURE_LOYALTY=${loyalty}
367
+ FEATURE_KITCHEN=${kitchen}
368
+ FEATURE_PAYMENTS=${payments}
369
+
370
+ # --- Auth ---
371
+ AUTH_PHONE=${authPhone}
372
+ AUTH_EMAIL=${authEmail}
373
+
374
+ # --- SMS ---
375
+ SMS_PROVIDER=${smsProvider}
376
+ SMS_ACCOUNT_SID=${smsAccountSid}
377
+ SMS_AUTH_TOKEN=${smsAuthToken}
378
+ SMS_FROM=${smsFrom}
379
+
380
+ # --- Email ---
381
+ EMAIL_PROVIDER=${emailProvider}
382
+ EMAIL_FROM=${emailFrom || `noreply@${domain}`}
383
+ SMTP_HOST=${smtpHost}
384
+ SMTP_PORT=${smtpPort}
385
+ SMTP_USER=${smtpUser}
386
+ SMTP_PASS=${smtpPass}
387
+ SENDGRID_API_KEY=${sendgridApiKey}
388
+
389
+ # --- Stripe ---
390
+ STRIPE_SECRET_KEY=${stripeKey}
391
+ STRIPE_PUBLISHABLE_KEY=
392
+ STRIPE_WEBHOOK_SECRET=
393
+
394
+ # --- Domain ---
395
+ DOMAIN=${domain}
396
+ `;
397
+
398
+ writeFileSync(join(projectDir, '.env'), env);
399
+
400
+ // Copy .env.example
401
+ cpSync(join(TEMPLATES_DIR, '.env.example'), join(projectDir, '.env.example'));
402
+
403
+ // Create directories
404
+ mkdirSync(join(projectDir, 'backups'), { recursive: true });
405
+
406
+ console.log(`
407
+ Projet cree dans ./${projectName}/
408
+
409
+ Features: ${[
410
+ ordering && 'commandes',
411
+ payments && 'paiement',
412
+ loyalty && 'fidelite',
413
+ kitchen && 'cuisine',
414
+ authPhone && 'auth-tel',
415
+ authEmail && 'auth-email',
416
+ ].filter(Boolean).join(', ') || 'aucune'}
417
+
418
+ Pour commencer:
419
+
420
+ cd ${projectName}
421
+ docker login ghcr.io # Auth pour pull l'image API
422
+ docker compose up -d # Lancer le backend (API + DB)
423
+ cd web && npm install && npm run dev # Lancer le frontend
424
+
425
+ Admin Strapi: http://localhost:1337/admin
426
+ Site web: http://localhost:3000
427
+
428
+ En production:
429
+ docker compose --profile production up -d
430
+ `);
431
+ }
432
+
433
+ main().catch((err) => {
434
+ console.error(err);
435
+ process.exit(1);
436
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "bestraw",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a BeStraw restaurant project",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "bestraw": "./index.mjs"
9
+ },
10
+ "files": [
11
+ "index.mjs",
12
+ "templates/"
13
+ ],
14
+ "scripts": {
15
+ "build": "echo 'No build needed'"
16
+ }
17
+ }
@@ -0,0 +1,51 @@
1
+ # ============================================================
2
+ # BeStraw — Configuration
3
+ # ============================================================
4
+
5
+ # --- General ---
6
+ VERSION=latest
7
+ PUBLIC_URL=http://localhost:1337
8
+
9
+ # --- Database ---
10
+ DB_NAME=bestraw
11
+ DB_USER=strapi
12
+ DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD
13
+
14
+ # --- Strapi Secrets (generate with: openssl rand -base64 16) ---
15
+ APP_KEYS=key1,key2,key3,key4
16
+ API_TOKEN_SALT=CHANGE_ME
17
+ ADMIN_JWT_SECRET=CHANGE_ME
18
+ JWT_SECRET=CHANGE_ME
19
+ TRANSFER_TOKEN_SALT=CHANGE_ME
20
+
21
+ # --- Ports ---
22
+ API_PORT=1337
23
+ WEB_PORT=3000
24
+ KITCHEN_PORT=3001
25
+
26
+ # --- Web frontend ---
27
+ NEXT_PUBLIC_API_URL=http://localhost:1337
28
+ API_URL=http://localhost:1337
29
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
30
+
31
+ # --- Kitchen display ---
32
+ NEXT_PUBLIC_API_TOKEN=
33
+
34
+ # --- Features (true/false) ---
35
+ FEATURE_ORDERING=false
36
+ FEATURE_LOYALTY=false
37
+ FEATURE_KITCHEN=false
38
+ FEATURE_PAYMENTS=false
39
+
40
+ # --- Stripe (si FEATURE_PAYMENTS=true) ---
41
+ STRIPE_SECRET_KEY=
42
+ STRIPE_PUBLISHABLE_KEY=
43
+ STRIPE_WEBHOOK_SECRET=
44
+
45
+ # --- SMS OTP (si FEATURE_ORDERING=true) ---
46
+ SMS_PROVIDER=
47
+ SMS_API_KEY=
48
+ SMS_API_SECRET=
49
+
50
+ # --- Domain (production avec Caddy) ---
51
+ DOMAIN=mon-restaurant.com
@@ -0,0 +1,21 @@
1
+ {$DOMAIN:localhost} {
2
+ handle /admin* {
3
+ reverse_proxy api:{$API_PORT:1337}
4
+ }
5
+
6
+ handle /api* {
7
+ reverse_proxy api:{$API_PORT:1337}
8
+ }
9
+
10
+ handle /uploads* {
11
+ reverse_proxy api:{$API_PORT:1337}
12
+ }
13
+
14
+ handle /kitchen* {
15
+ reverse_proxy kitchen:80
16
+ }
17
+
18
+ handle {
19
+ reverse_proxy web:{$WEB_PORT:3000}
20
+ }
21
+ }
@@ -0,0 +1,80 @@
1
+ services:
2
+ db:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_DB: ${DB_NAME:-bestraw}
6
+ POSTGRES_USER: ${DB_USER:-strapi}
7
+ POSTGRES_PASSWORD: ${DB_PASSWORD}
8
+ volumes:
9
+ - pgdata:/var/lib/postgresql/data
10
+ restart: unless-stopped
11
+
12
+ api:
13
+ image: ghcr.io/lbrzr/bestraw-api:${VERSION:-latest}
14
+ depends_on: [db]
15
+ environment:
16
+ DATABASE_CLIENT: postgres
17
+ DATABASE_HOST: db
18
+ DATABASE_PORT: 5432
19
+ DATABASE_NAME: ${DB_NAME:-bestraw}
20
+ DATABASE_USERNAME: ${DB_USER:-strapi}
21
+ DATABASE_PASSWORD: ${DB_PASSWORD}
22
+ ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
23
+ APP_KEYS: ${APP_KEYS}
24
+ API_TOKEN_SALT: ${API_TOKEN_SALT}
25
+ JWT_SECRET: ${JWT_SECRET}
26
+ TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT}
27
+ FEATURE_ORDERING: ${FEATURE_ORDERING:-false}
28
+ FEATURE_LOYALTY: ${FEATURE_LOYALTY:-false}
29
+ FEATURE_KITCHEN: ${FEATURE_KITCHEN:-false}
30
+ FEATURE_PAYMENTS: ${FEATURE_PAYMENTS:-false}
31
+ STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
32
+ STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY:-}
33
+ STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
34
+ SMS_PROVIDER: ${SMS_PROVIDER:-}
35
+ SMS_API_KEY: ${SMS_API_KEY:-}
36
+ SMS_API_SECRET: ${SMS_API_SECRET:-}
37
+ volumes:
38
+ - uploads:/opt/app/public/uploads
39
+ ports:
40
+ - "${API_PORT:-1337}:1337"
41
+ restart: unless-stopped
42
+
43
+ kitchen:
44
+ image: ghcr.io/lbrzr/bestraw-kitchen:${VERSION:-latest}
45
+ depends_on: [api]
46
+ environment:
47
+ VITE_API_URL: ${PUBLIC_URL:-http://localhost:1337}
48
+ ports:
49
+ - "${KITCHEN_PORT:-3001}:80"
50
+ profiles: ["kitchen"]
51
+ restart: unless-stopped
52
+
53
+ web:
54
+ build: ./web
55
+ depends_on: [api]
56
+ environment:
57
+ API_URL: http://api:1337
58
+ NEXT_PUBLIC_API_URL: ${PUBLIC_URL:-http://localhost:1337}
59
+ NEXT_PUBLIC_STRIPE_KEY: ${STRIPE_PUBLISHABLE_KEY:-}
60
+ ports:
61
+ - "${WEB_PORT:-3000}:3000"
62
+ profiles: ["production"]
63
+ restart: unless-stopped
64
+
65
+ caddy:
66
+ image: caddy:2-alpine
67
+ ports:
68
+ - "80:80"
69
+ - "443:443"
70
+ volumes:
71
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
72
+ - caddy_data:/data
73
+ depends_on: [api]
74
+ profiles: ["production"]
75
+ restart: unless-stopped
76
+
77
+ volumes:
78
+ pgdata:
79
+ uploads:
80
+ caddy_data:
@@ -0,0 +1,19 @@
1
+ FROM node:20-alpine AS deps
2
+ WORKDIR /app
3
+ COPY package.json pnpm-lock.yaml* ./
4
+ RUN corepack enable && pnpm install --frozen-lockfile
5
+
6
+ FROM node:20-alpine AS builder
7
+ WORKDIR /app
8
+ COPY --from=deps /app/node_modules ./node_modules
9
+ COPY . .
10
+ RUN corepack enable && pnpm build
11
+
12
+ FROM node:20-alpine AS runner
13
+ WORKDIR /app
14
+ ENV NODE_ENV=production
15
+ COPY --from=builder /app/.next/standalone ./
16
+ COPY --from=builder /app/.next/static ./.next/static
17
+ COPY --from=builder /app/public ./public
18
+ EXPOSE 3000
19
+ CMD ["node", "server.js"]
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ /// <reference path="./.next/types/routes.d.ts" />
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,10 @@
1
+ import type { NextConfig } from 'next';
2
+ import createNextIntlPlugin from 'next-intl/plugin';
3
+
4
+ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
5
+
6
+ const nextConfig: NextConfig = {
7
+ output: 'standalone',
8
+ };
9
+
10
+ export default withNextIntl(nextConfig);
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
6
+ esac
7
+
8
+ if [ -z "$NODE_PATH" ]; then
9
+ export NODE_PATH="/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/dist/bin/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/dist/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/node_modules"
10
+ else
11
+ export NODE_PATH="/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/dist/bin/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/dist/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/next@15.5.14_@opentelemetry+api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/node_modules:$NODE_PATH"
12
+ fi
13
+ if [ -x "$basedir/node" ]; then
14
+ exec "$basedir/node" "$basedir/../next/dist/bin/next" "$@"
15
+ else
16
+ exec node "$basedir/../next/dist/bin/next" "$@"
17
+ fi
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
6
+ esac
7
+
8
+ if [ -z "$NODE_PATH" ]; then
9
+ export NODE_PATH="/home/runner/work/bestraw/bestraw/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/node_modules"
10
+ else
11
+ export NODE_PATH="/home/runner/work/bestraw/bestraw/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/bestraw/bestraw/node_modules/.pnpm/node_modules:$NODE_PATH"
12
+ fi
13
+ if [ -x "$basedir/node" ]; then
14
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
15
+ else
16
+ exec node "$basedir/../typescript/bin/tsc" "$@"
17
+ fi