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.
- package/index.mjs +436 -0
- package/package.json +17 -0
- package/templates/.env.example +51 -0
- package/templates/Caddyfile +21 -0
- package/templates/docker-compose.yml +80 -0
- package/templates/web/Dockerfile +19 -0
- package/templates/web/next-env.d.ts +6 -0
- package/templates/web/next.config.ts +10 -0
- package/templates/web/node_modules/.bin/next +17 -0
- package/templates/web/node_modules/.bin/tsc +17 -0
- package/templates/web/node_modules/.bin/tsserver +17 -0
- package/templates/web/package.json +28 -0
- package/templates/web/postcss.config.mjs +8 -0
- package/templates/web/public/images/.gitkeep +0 -0
- package/templates/web/src/app/[locale]/auth/page.tsx +222 -0
- package/templates/web/src/app/[locale]/blog/[slug]/page.tsx +104 -0
- package/templates/web/src/app/[locale]/blog/page.tsx +90 -0
- package/templates/web/src/app/[locale]/error.tsx +41 -0
- package/templates/web/src/app/[locale]/info/page.tsx +186 -0
- package/templates/web/src/app/[locale]/layout.tsx +86 -0
- package/templates/web/src/app/[locale]/loyalty/page.tsx +135 -0
- package/templates/web/src/app/[locale]/menu/page.tsx +69 -0
- package/templates/web/src/app/[locale]/order/cart/page.tsx +199 -0
- package/templates/web/src/app/[locale]/order/checkout/page.tsx +489 -0
- package/templates/web/src/app/[locale]/order/confirmation/[id]/page.tsx +159 -0
- package/templates/web/src/app/[locale]/order/page.tsx +207 -0
- package/templates/web/src/app/[locale]/page.tsx +119 -0
- package/templates/web/src/app/globals.css +11 -0
- package/templates/web/src/app/robots.ts +14 -0
- package/templates/web/src/app/sitemap.ts +56 -0
- package/templates/web/src/bestraw.config.ts +9 -0
- package/templates/web/src/components/auth/OtpForm.tsx +98 -0
- package/templates/web/src/components/blog/ArticleCard.tsx +67 -0
- package/templates/web/src/components/blog/ArticleContent.tsx +14 -0
- package/templates/web/src/components/cart/CartDrawer.tsx +152 -0
- package/templates/web/src/components/cart/CartItem.tsx +111 -0
- package/templates/web/src/components/checkout/StripePaymentForm.tsx +54 -0
- package/templates/web/src/components/layout/Footer.tsx +40 -0
- package/templates/web/src/components/layout/Header.tsx +240 -0
- package/templates/web/src/components/layout/LocaleSwitcher.tsx +34 -0
- package/templates/web/src/components/loyalty/PointsBalance.tsx +96 -0
- package/templates/web/src/components/loyalty/RewardCard.tsx +73 -0
- package/templates/web/src/components/loyalty/TransactionHistory.tsx +108 -0
- package/templates/web/src/components/menu/CategorySection.tsx +42 -0
- package/templates/web/src/components/menu/MealCard.tsx +55 -0
- package/templates/web/src/components/menu/MealDetailModal.tsx +355 -0
- package/templates/web/src/components/menu/MenuContent.tsx +216 -0
- package/templates/web/src/components/order/MealOrderCard.tsx +220 -0
- package/templates/web/src/components/order/OrderStatusTracker.tsx +138 -0
- package/templates/web/src/components/order/PaymentStatus.tsx +62 -0
- package/templates/web/src/components/ui/Button.tsx +40 -0
- package/templates/web/src/components/ui/ErrorAlert.tsx +15 -0
- package/templates/web/src/i18n/config.ts +3 -0
- package/templates/web/src/i18n/request.ts +13 -0
- package/templates/web/src/i18n/routing.ts +10 -0
- package/templates/web/src/lib/client.ts +5 -0
- package/templates/web/src/lib/errors.ts +31 -0
- package/templates/web/src/lib/features.ts +10 -0
- package/templates/web/src/lib/hooks/useCustomerClient.ts +28 -0
- package/templates/web/src/lib/hooks/useMenu.ts +46 -0
- package/templates/web/src/messages/en.json +283 -0
- package/templates/web/src/messages/fr.json +283 -0
- package/templates/web/src/middleware.ts +8 -0
- package/templates/web/src/providers/CartProvider.tsx +162 -0
- package/templates/web/src/providers/StripeProvider.tsx +21 -0
- 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
|