falak-app-duo 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/falak-app-duo +364 -44
- package/package.json +5 -2
- package/templates/default/.env.example +8 -1
- package/templates/default/package.json +4 -0
- package/templates/default/src/data/demo/appointments.js +7 -0
- package/templates/default/src/data/demo/customers.js +7 -0
- package/templates/default/src/data/demo/events.js +7 -0
- package/templates/default/src/data/demo/invoices.js +7 -0
- package/templates/default/src/data/demo/messages.js +7 -0
- package/templates/default/src/data/demo/orders.js +7 -0
- package/templates/default/src/data/demo/payments.js +7 -0
- package/templates/default/src/data/demo/posts.js +7 -0
- package/templates/default/src/data/demo/products.js +7 -0
- package/templates/default/src/data/demo/projects.js +7 -0
- package/templates/default/src/data/demo/tasks.js +7 -0
- package/templates/default/src/data/demo/users.js +9 -0
- package/templates/default/src/data/demoAccounts.js +135 -0
- package/templates/default/src/data/roles.js +25 -0
- package/templates/default/src/i18n/locales/ar.json +26 -4
- package/templates/default/src/i18n/locales/de.json +26 -4
- package/templates/default/src/i18n/locales/en.json +26 -4
- package/templates/default/src/i18n/locales/es.json +26 -4
- package/templates/default/src/i18n/locales/fr.json +26 -4
- package/templates/default/src/i18n/locales/it.json +26 -4
- package/templates/default/src/i18n/locales/ru.json +26 -4
- package/templates/default/src/main.js +11 -0
- package/templates/default/src/services/dataClient.js +22 -0
- package/templates/default/src/services/demoApi.js +377 -0
- package/templates/default/src/stores/auth.js +22 -17
- package/templates/default/src/views/Home.vue +52 -2
- package/templates/default/src/views/Login.vue +26 -1
- package/templates/default/src/views/Profile.vue +13 -0
- package/templates/default/src/views/Register.vue +17 -2
- package/templates/default/vite.config.js +2 -1
package/bin/falak-app-duo
CHANGED
|
@@ -35,6 +35,182 @@ const LOCALE_LABELS = {
|
|
|
35
35
|
|
|
36
36
|
const EXTRA_LOCALE_FILES = ["ar.json", "de.json", "es.json", "fr.json", "it.json", "ru.json"];
|
|
37
37
|
|
|
38
|
+
const ROLE_ORDER = ["master", "super_admin", "admin", "user"];
|
|
39
|
+
const ROLE_META = {
|
|
40
|
+
master: { label: "roles.master.title", badge: "bg-rose-500/10 text-rose-300 ring-rose-400/30" },
|
|
41
|
+
super_admin: { label: "roles.super_admin.title", badge: "bg-amber-500/10 text-amber-300 ring-amber-400/30" },
|
|
42
|
+
admin: { label: "roles.admin.title", badge: "bg-cyan-500/10 text-cyan-300 ring-cyan-400/30" },
|
|
43
|
+
user: { label: "roles.user.title", badge: "bg-emerald-500/10 text-emerald-300 ring-emerald-400/30" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEMO_ENTITIES = [
|
|
47
|
+
"users",
|
|
48
|
+
"products",
|
|
49
|
+
"orders",
|
|
50
|
+
"customers",
|
|
51
|
+
"invoices",
|
|
52
|
+
"payments",
|
|
53
|
+
"events",
|
|
54
|
+
"posts",
|
|
55
|
+
"messages",
|
|
56
|
+
"tasks",
|
|
57
|
+
"projects",
|
|
58
|
+
"appointments",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const DEMO_ROLE_SEED = {
|
|
62
|
+
master: {
|
|
63
|
+
name: "Aya El-Amin",
|
|
64
|
+
email: "master@demo.dev",
|
|
65
|
+
phone: "+201000000001",
|
|
66
|
+
country_code: "EG",
|
|
67
|
+
city: "Cairo",
|
|
68
|
+
channel: "telegram",
|
|
69
|
+
channel_handle: "@aya_master",
|
|
70
|
+
ai_model: "llama-3.3-70b-versatile",
|
|
71
|
+
ai_enabled: true,
|
|
72
|
+
telegram_username: "@aya_master",
|
|
73
|
+
},
|
|
74
|
+
super_admin: {
|
|
75
|
+
name: "Omar El-Sayed",
|
|
76
|
+
email: "superadmin@demo.dev",
|
|
77
|
+
phone: "+966500000002",
|
|
78
|
+
country_code: "SA",
|
|
79
|
+
city: "Riyadh",
|
|
80
|
+
channel: "whatsapp",
|
|
81
|
+
channel_handle: "+966500000002",
|
|
82
|
+
ai_model: "mixtral-8x7b-32768",
|
|
83
|
+
ai_enabled: true,
|
|
84
|
+
telegram_username: "",
|
|
85
|
+
},
|
|
86
|
+
admin: {
|
|
87
|
+
name: "Layla Haddad",
|
|
88
|
+
email: "admin@demo.dev",
|
|
89
|
+
phone: "+971500000003",
|
|
90
|
+
country_code: "AE",
|
|
91
|
+
city: "Dubai",
|
|
92
|
+
channel: "none",
|
|
93
|
+
channel_handle: "",
|
|
94
|
+
ai_model: "llama-3.1-8b-instant",
|
|
95
|
+
ai_enabled: false,
|
|
96
|
+
telegram_username: "",
|
|
97
|
+
},
|
|
98
|
+
user: {
|
|
99
|
+
name: "Karim Nasser",
|
|
100
|
+
email: "user@demo.dev",
|
|
101
|
+
phone: "+120200000004",
|
|
102
|
+
country_code: "US",
|
|
103
|
+
city: "New York",
|
|
104
|
+
channel: "none",
|
|
105
|
+
channel_handle: "",
|
|
106
|
+
ai_model: "gemma2-9b-it",
|
|
107
|
+
ai_enabled: false,
|
|
108
|
+
telegram_username: "",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const DEMO_NOTIFICATIONS = {
|
|
113
|
+
account: { email: true, push: true, sms: false, telegram: false },
|
|
114
|
+
security: { email: true, push: true, sms: true, telegram: false },
|
|
115
|
+
marketing: { email: false, push: false, sms: false, telegram: false },
|
|
116
|
+
updates: { email: true, push: false, sms: false, telegram: false },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function buildRolesModule(roleKeys) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
lines.push("// UserRole support. Generated by the scaffolder: only the roles");
|
|
122
|
+
lines.push("// you picked at scaffold time are kept; an empty ROLES means UserRole");
|
|
123
|
+
lines.push('// support is off. `label` is a vue-i18n key (see i18n/locales/*.json).');
|
|
124
|
+
lines.push("export const ROLES = [");
|
|
125
|
+
for (const key of ROLE_ORDER.filter((k) => roleKeys.includes(k))) {
|
|
126
|
+
lines.push(` { key: "${key}", label: "${ROLE_META[key].label}", badge: "${ROLE_META[key].badge}" },`);
|
|
127
|
+
}
|
|
128
|
+
lines.push("];");
|
|
129
|
+
return lines.join("\n") + "\n";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function demoSettingsFor(roleKey) {
|
|
133
|
+
const seed = roleKey ? DEMO_ROLE_SEED[roleKey] : null;
|
|
134
|
+
return {
|
|
135
|
+
notification_preferences: DEMO_NOTIFICATIONS,
|
|
136
|
+
two_factor_enabled: false,
|
|
137
|
+
ai_agent: {
|
|
138
|
+
enabled: seed ? seed.ai_enabled : false,
|
|
139
|
+
model: seed ? seed.ai_model : "llama-3.3-70b-versatile",
|
|
140
|
+
temperature: 0.7,
|
|
141
|
+
reasoning: false,
|
|
142
|
+
system_prompt: "",
|
|
143
|
+
},
|
|
144
|
+
telegram: { bot_token: "", chat_id: "", username: seed ? seed.telegram_username : "" },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// roleKeys == [] => a single normal demo user (no role on the account).
|
|
149
|
+
function buildDemoAccountsModule(roleKeys) {
|
|
150
|
+
const chosen = ROLE_ORDER.filter((k) => roleKeys.includes(k));
|
|
151
|
+
const accounts = chosen.length
|
|
152
|
+
? chosen.map((key) => {
|
|
153
|
+
const s = DEMO_ROLE_SEED[key];
|
|
154
|
+
return {
|
|
155
|
+
role: key,
|
|
156
|
+
email: s.email,
|
|
157
|
+
password: "password",
|
|
158
|
+
profile: {
|
|
159
|
+
name: s.name,
|
|
160
|
+
email: s.email,
|
|
161
|
+
phone: s.phone,
|
|
162
|
+
country_code: s.country_code,
|
|
163
|
+
city: s.city,
|
|
164
|
+
channel: s.channel,
|
|
165
|
+
channel_handle: s.channel_handle,
|
|
166
|
+
photo: null,
|
|
167
|
+
},
|
|
168
|
+
settings: demoSettingsFor(key),
|
|
169
|
+
};
|
|
170
|
+
})
|
|
171
|
+
: [
|
|
172
|
+
{
|
|
173
|
+
role: null,
|
|
174
|
+
email: "demo@demo.dev",
|
|
175
|
+
password: "password",
|
|
176
|
+
profile: {
|
|
177
|
+
name: "Demo User",
|
|
178
|
+
email: "demo@demo.dev",
|
|
179
|
+
phone: "+201000000009",
|
|
180
|
+
country_code: "EG",
|
|
181
|
+
city: "Cairo",
|
|
182
|
+
channel: "none",
|
|
183
|
+
channel_handle: "",
|
|
184
|
+
photo: null,
|
|
185
|
+
},
|
|
186
|
+
settings: demoSettingsFor(null),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
const lines = [];
|
|
190
|
+
lines.push("// Static demo accounts for testing. Generated by the scaffolder: one");
|
|
191
|
+
lines.push("// account per role you picked, or a single normal demo user when");
|
|
192
|
+
lines.push("// UserRole support is off. Each carries its full profile and default");
|
|
193
|
+
lines.push("// settings so a backend seed (or a first-login init) has everything.");
|
|
194
|
+
lines.push("export const DEMO_ACCOUNTS = [");
|
|
195
|
+
for (const account of accounts) {
|
|
196
|
+
lines.push(" " + JSON.stringify(account) + ",");
|
|
197
|
+
}
|
|
198
|
+
lines.push("];");
|
|
199
|
+
return lines.join("\n") + "\n";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseList(value) {
|
|
203
|
+
return String(value || "")
|
|
204
|
+
.split(",")
|
|
205
|
+
.map((x) => x.trim())
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function keepCanonicalOrder(value, allowed) {
|
|
210
|
+
const picked = parseList(value);
|
|
211
|
+
return allowed.filter((item) => picked.includes(item));
|
|
212
|
+
}
|
|
213
|
+
|
|
38
214
|
function buildI18nModule(locales, defaultLocale) {
|
|
39
215
|
const ordered = ["ar", "en", "es", "de", "fr", "ru", "it"].filter((c) => locales.includes(c));
|
|
40
216
|
const lines = [];
|
|
@@ -82,8 +258,11 @@ program
|
|
|
82
258
|
.name("falak-app")
|
|
83
259
|
.description("Scaffold a Vue 3 app with Ably, Brevo, AES-256 and a Laravel API client baked in.")
|
|
84
260
|
.argument("[project-name]", "Directory to create the project in")
|
|
85
|
-
.option("-y, --yes", "Skip prompts and use defaults (
|
|
261
|
+
.option("-y, --yes", "Skip prompts and use defaults (services + languages only; no roles/demo)")
|
|
86
262
|
.option("--no-rtl", "Exclude Arabic/RTL (English or your chosen LTR languages only)")
|
|
263
|
+
.option("--roles <list>", "Enable UserRole support with the given comma-separated roles (master,super_admin,admin,user)")
|
|
264
|
+
.option("--demo-accounts", "Include static demo accounts (per selected role, or a normal demo user without roles)")
|
|
265
|
+
.option("--demo-data <list>", "Include static demo data for the given comma-separated entities (e.g. products,orders)")
|
|
87
266
|
.parse(process.argv);
|
|
88
267
|
|
|
89
268
|
const [argProjectName] = program.args;
|
|
@@ -120,49 +299,141 @@ async function printBanner() {
|
|
|
120
299
|
async function main() {
|
|
121
300
|
await printBanner();
|
|
122
301
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
302
|
+
const defaults = {
|
|
303
|
+
projectName: argProjectName || "falak-app-duo",
|
|
304
|
+
services: ["ably", "brevo"],
|
|
305
|
+
languages: ["ar"],
|
|
306
|
+
apiBaseUrl: "http://localhost:8000/api",
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const hasFeatureFlags =
|
|
310
|
+
opts.roles !== undefined || opts.demoAccounts !== undefined || opts.demoData !== undefined;
|
|
311
|
+
|
|
312
|
+
// Feature outcomes. Roles/accounts/data only come via explicit flags or
|
|
313
|
+
// interactive prompts — `--yes` keeps the minimal scaffold.
|
|
314
|
+
let roles = opts.yes ? [] : null;
|
|
315
|
+
let demoAccounts = opts.yes ? false : null;
|
|
316
|
+
let demoEntities = opts.yes ? [] : null;
|
|
317
|
+
|
|
318
|
+
let answers;
|
|
319
|
+
if (opts.yes || hasFeatureFlags) {
|
|
320
|
+
answers = { ...defaults };
|
|
321
|
+
} else {
|
|
322
|
+
answers = await prompts(
|
|
323
|
+
[
|
|
324
|
+
{
|
|
325
|
+
type: argProjectName ? null : "text",
|
|
326
|
+
name: "projectName",
|
|
327
|
+
message: "Project name / folder:",
|
|
328
|
+
initial: "falak-app-duo",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: "multiselect",
|
|
332
|
+
name: "services",
|
|
333
|
+
message: "Which services do you want wired up?",
|
|
334
|
+
choices: [
|
|
335
|
+
{ title: "Ably (realtime, via Laravel Echo)", value: "ably", selected: true },
|
|
336
|
+
{ title: "Brevo (email/SMS, via backend)", value: "brevo", selected: true },
|
|
337
|
+
],
|
|
338
|
+
hint: "Space to toggle, Enter to confirm",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
type: "multiselect",
|
|
342
|
+
name: "languages",
|
|
343
|
+
message: "Which languages should the app ship with? (English is always included)",
|
|
344
|
+
choices: LANGUAGE_CHOICES.map((c) =>
|
|
345
|
+
c.value === "ar" && !opts.rtl ? { ...c, selected: false } : c
|
|
346
|
+
),
|
|
347
|
+
hint: "Space to toggle, Enter to confirm",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: "text",
|
|
351
|
+
name: "apiBaseUrl",
|
|
352
|
+
message: "Laravel API base URL:",
|
|
353
|
+
initial: "http://localhost:8000/api",
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
{ onCancel: () => process.exit(1) }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---- UserRole support ----
|
|
361
|
+
if (!opts.yes && hasFeatureFlags) {
|
|
362
|
+
roles = opts.roles !== undefined ? keepCanonicalOrder(opts.roles, ROLE_ORDER) : [];
|
|
363
|
+
demoAccounts = Boolean(opts.demoAccounts);
|
|
364
|
+
demoEntities = opts.demoData !== undefined ? keepCanonicalOrder(opts.demoData, DEMO_ENTITIES) : [];
|
|
365
|
+
} else if (!opts.yes) {
|
|
366
|
+
const wantRoles = (
|
|
367
|
+
await prompts(
|
|
368
|
+
{
|
|
369
|
+
type: "toggle",
|
|
370
|
+
name: "v",
|
|
371
|
+
message: "Register accounts with a role (UserRole support)?",
|
|
372
|
+
initial: true,
|
|
373
|
+
active: "yes",
|
|
374
|
+
inactive: "no",
|
|
375
|
+
},
|
|
376
|
+
{ onCancel: () => process.exit(1) }
|
|
377
|
+
)
|
|
378
|
+
).v;
|
|
379
|
+
if (wantRoles) {
|
|
380
|
+
const { r } = await prompts(
|
|
381
|
+
{
|
|
382
|
+
type: "multiselect",
|
|
383
|
+
name: "r",
|
|
384
|
+
message: "Which roles should auth support?",
|
|
385
|
+
choices: ROLE_ORDER.map((k) => ({ title: k, value: k, selected: true })),
|
|
386
|
+
hint: "Space to toggle, Enter to confirm",
|
|
387
|
+
},
|
|
164
388
|
{ onCancel: () => process.exit(1) }
|
|
165
389
|
);
|
|
390
|
+
roles = r || [];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const wantAccounts = (
|
|
394
|
+
await prompts(
|
|
395
|
+
{
|
|
396
|
+
type: "toggle",
|
|
397
|
+
name: "v",
|
|
398
|
+
message: roles.length
|
|
399
|
+
? "Create demo accounts (one per selected role)?"
|
|
400
|
+
: "Create a normal demo account?",
|
|
401
|
+
initial: true,
|
|
402
|
+
active: "yes",
|
|
403
|
+
inactive: "no",
|
|
404
|
+
},
|
|
405
|
+
{ onCancel: () => process.exit(1) }
|
|
406
|
+
)
|
|
407
|
+
).v;
|
|
408
|
+
demoAccounts = Boolean(wantAccounts);
|
|
409
|
+
|
|
410
|
+
const wantData = (
|
|
411
|
+
await prompts(
|
|
412
|
+
{
|
|
413
|
+
type: "toggle",
|
|
414
|
+
name: "v",
|
|
415
|
+
message: "Create static demo data to test with?",
|
|
416
|
+
initial: true,
|
|
417
|
+
active: "yes",
|
|
418
|
+
inactive: "no",
|
|
419
|
+
},
|
|
420
|
+
{ onCancel: () => process.exit(1) }
|
|
421
|
+
)
|
|
422
|
+
).v;
|
|
423
|
+
if (wantData) {
|
|
424
|
+
const { e } = await prompts(
|
|
425
|
+
{
|
|
426
|
+
type: "multiselect",
|
|
427
|
+
name: "e",
|
|
428
|
+
message: "Which demo-data entities should the app ship with?",
|
|
429
|
+
choices: DEMO_ENTITIES.map((k) => ({ title: k, value: k, selected: true })),
|
|
430
|
+
hint: "Space to toggle, Enter to confirm",
|
|
431
|
+
},
|
|
432
|
+
{ onCancel: () => process.exit(1) }
|
|
433
|
+
);
|
|
434
|
+
demoEntities = e || [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
166
437
|
|
|
167
438
|
// The set of locales the app ships: English is always bundled, plus the
|
|
168
439
|
// languages the user picked. `--no-rtl` forces Arabic out (LTR only).
|
|
@@ -238,14 +509,57 @@ async function main() {
|
|
|
238
509
|
await fs.writeFile(appPath, app);
|
|
239
510
|
}
|
|
240
511
|
|
|
512
|
+
// UserRole support: rewrite src/data/roles.js to the picked role set. The
|
|
513
|
+
// file always stays (Register/Profile import it), just emptied when off.
|
|
514
|
+
await fs.writeFile(path.join(targetDir, "src", "data", "roles.js"), buildRolesModule(roles || []));
|
|
515
|
+
|
|
516
|
+
// Demo accounts: static module with one account per role, or a single
|
|
517
|
+
// normal demo user when roles are off. Removed entirely when not wanted.
|
|
518
|
+
const demoAccountsPath = path.join(targetDir, "src", "data", "demoAccounts.js");
|
|
519
|
+
if (demoAccounts) {
|
|
520
|
+
await fs.writeFile(demoAccountsPath, buildDemoAccountsModule(roles || []));
|
|
521
|
+
} else {
|
|
522
|
+
await fs.remove(demoAccountsPath);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Static demo data: keep only the selected entity modules. The whole demo
|
|
526
|
+
// dir is dropped when demo data wasn't picked (nothing imports it).
|
|
527
|
+
const demoDir = path.join(targetDir, "src", "data", "demo");
|
|
528
|
+
for (const entity of DEMO_ENTITIES) {
|
|
529
|
+
if (!demoEntities || !demoEntities.includes(entity)) {
|
|
530
|
+
await fs.remove(path.join(demoDir, `${entity}.js`));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (!demoEntities || (await fs.readdir(demoDir).catch(() => [])).length === 0) {
|
|
534
|
+
await fs.remove(demoDir);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const demoAccountsSummary = demoAccounts
|
|
538
|
+
? roles && roles.length
|
|
539
|
+
? `${roles.length} (${roles.join(", ")})`
|
|
540
|
+
: "1 (normal demo user)"
|
|
541
|
+
: "none";
|
|
542
|
+
|
|
543
|
+
// Data mode: demo apps are wired fully to the bundled demo data (auth +
|
|
544
|
+
// profile/settings + entity lists) and need no backend; everything else
|
|
545
|
+
// goes straight to the Laravel API.
|
|
546
|
+
const dataMode = demoAccounts || (demoEntities && demoEntities.length) ? "demo" : "live";
|
|
547
|
+
|
|
241
548
|
console.log(
|
|
242
549
|
chalk.gray(` Languages wired: ${locales.map((l) => LOCALE_LABELS[l]).join(", ")}${hasRtl ? " (RTL default)" : ""}`)
|
|
243
550
|
);
|
|
551
|
+
console.log(
|
|
552
|
+
chalk.gray(
|
|
553
|
+
` Roles: ${roles && roles.length ? roles.join(", ") : "none"} | Demo accounts: ${demoAccountsSummary} | Demo data: ${demoEntities && demoEntities.length ? demoEntities.join(", ") : "none"}`
|
|
554
|
+
)
|
|
555
|
+
);
|
|
556
|
+
console.log(chalk.gray(` Data mode: ${dataMode}${dataMode === "demo" ? " (no backend needed)" : " (Laravel API)"}`));
|
|
244
557
|
|
|
245
558
|
// Generate a real .env from .env.example with a fresh encryption key
|
|
246
559
|
const envExamplePath = path.join(targetDir, ".env.example");
|
|
247
560
|
let envContent = await fs.readFile(envExamplePath, "utf-8");
|
|
248
561
|
envContent = envContent
|
|
562
|
+
.replace("__DATA_MODE__", dataMode)
|
|
249
563
|
.replace("__API_BASE_URL__", answers.apiBaseUrl || "http://localhost:8000/api")
|
|
250
564
|
.replace("__AES_KEY__", randomHex(32)) // 256-bit key, hex-encoded
|
|
251
565
|
.replace("__AES_IV__", randomHex(16)); // 128-bit IV, hex-encoded
|
|
@@ -254,11 +568,17 @@ async function main() {
|
|
|
254
568
|
console.log(chalk.green(`\n Scaffolded ./${projectName} successfully.\n`));
|
|
255
569
|
console.log(
|
|
256
570
|
chalk.gray(
|
|
257
|
-
" Fill in VITE_API_KEY, VITE_ABLY_PUBLIC_KEY, Brevo backend routes, and review .env before shipping
|
|
571
|
+
" Fill in VITE_API_KEY, VITE_ABLY_PUBLIC_KEY, Brevo backend routes, and review .env before shipping."
|
|
258
572
|
)
|
|
259
573
|
);
|
|
574
|
+
if (dataMode === "demo") {
|
|
575
|
+
console.log(chalk.gray(" Data mode: demo — the app runs on bundled demo data (no API keys needed)."));
|
|
576
|
+
console.log(chalk.gray(" Flip VITE_DATA_MODE=live in .env to wire the app to your Laravel backend.\n"));
|
|
577
|
+
} else {
|
|
578
|
+
console.log(chalk.gray("\n Data mode: live — the app calls your Laravel backend.\n"));
|
|
579
|
+
}
|
|
260
580
|
|
|
261
|
-
const runChoice = opts.yes
|
|
581
|
+
const runChoice = opts.yes || hasFeatureFlags
|
|
262
582
|
? "install"
|
|
263
583
|
: (
|
|
264
584
|
await prompts(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "falak-app-duo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Vue 3 boilerplate scaffolder — Ably (realtime), Brevo, AES-256, Arabic/RTL, and a Laravel API client (API key + user token auth). No Firebase, no Groq/Paymob on the frontend (Groq is backend-only; Paymob dropped).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
"ably",
|
|
33
33
|
"realtime"
|
|
34
34
|
],
|
|
35
|
-
"license": "MIT"
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
36
39
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
# --- Data source ---
|
|
2
|
+
# demo: the app runs entirely on bundled demo data (auth, profile, settings and
|
|
3
|
+
# entity lists) with no backend required.
|
|
4
|
+
# live: every request goes to the Laravel API below.
|
|
5
|
+
# The scaffolder sets this to "demo" when demo accounts/demo data were selected.
|
|
6
|
+
VITE_DATA_MODE=__DATA_MODE__
|
|
7
|
+
|
|
1
8
|
# --- Laravel API (Falak Bridge or your own backend) ---
|
|
2
|
-
# Base URL of your Laravel API
|
|
9
|
+
# Base URL of your Laravel API (not used in demo mode)
|
|
3
10
|
VITE_API_BASE_URL=__API_BASE_URL__
|
|
4
11
|
# Static API key issued to this client app (sent as X-API-Key on every request)
|
|
5
12
|
VITE_API_KEY=replace-with-your-app-api-key
|
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@primeuix/themes": "^1.2.5",
|
|
12
13
|
"ably": "^2.5.0",
|
|
13
14
|
"axios": "^1.7.7",
|
|
14
15
|
"crypto-js": "^4.2.0",
|
|
15
16
|
"laravel-echo": "^1.16.1",
|
|
16
17
|
"pinia": "^2.2.2",
|
|
18
|
+
"primeicons": "^7.0.0",
|
|
19
|
+
"primevue": "^4.5.5",
|
|
17
20
|
"vue": "^3.5.0",
|
|
18
21
|
"vue-i18n": "^9.14.0",
|
|
19
22
|
"vue-router": "^4.4.5"
|
|
20
23
|
},
|
|
21
24
|
"devDependencies": {
|
|
25
|
+
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
|
22
26
|
"@vitejs/plugin-vue": "^5.1.4",
|
|
23
27
|
"autoprefixer": "^10.4.20",
|
|
24
28
|
"postcss": "^8.4.47",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const appointments = [
|
|
2
|
+
{ id: 1, title: "Design review", attendee: "Aya El-Amin", date: "2026-09-15", time: "10:00", status: "confirmed" },
|
|
3
|
+
{ id: 2, title: "Stakeholder sync", attendee: "Nour Farouk", date: "2026-09-15", time: "14:30", status: "confirmed" },
|
|
4
|
+
{ id: 3, title: "Intro call", attendee: "Layla Haddad", date: "2026-09-16", time: "09:15", status: "pending" },
|
|
5
|
+
{ id: 4, title: "User interview", attendee: "Omar El-Sayed", date: "2026-09-17", time: "11:45", status: "confirmed" },
|
|
6
|
+
{ id: 5, title: "Sprint retro", attendee: "Karim Nasser", date: "2026-09-18", time: "16:00", status: "cancelled" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const customers = [
|
|
2
|
+
{ id: 1, name: "Aya El-Amin", email: "aya@example.com", segment: "enterprise", orders: 14, lifetime_value: 3200, created_at: "2025-11-02" },
|
|
3
|
+
{ id: 2, name: "Omar El-Sayed", email: "omar@example.com", segment: "retail", orders: 6, lifetime_value: 880, created_at: "2026-01-24" },
|
|
4
|
+
{ id: 3, name: "Layla Haddad", email: "layla@example.com", segment: "retail", orders: 2, lifetime_value: 428, created_at: "2026-03-18" },
|
|
5
|
+
{ id: 4, name: "Karim Nasser", email: "karim@example.com", segment: "wholesale", orders: 9, lifetime_value: 5410, created_at: "2025-09-15" },
|
|
6
|
+
{ id: 5, name: "Nour Farouk", email: "nour@example.com", segment: "enterprise", orders: 21, lifetime_value: 9800, created_at: "2025-07-30" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const events = [
|
|
2
|
+
{ id: 1, title: "Product Launch 2026", date: "2026-10-12", venue: "Cairo Marriott", capacity: 300, booked: 214, status: "published" },
|
|
3
|
+
{ id: 2, title: "Design Meetup", date: "2026-10-20", venue: "Riyadh Hub", capacity: 120, booked: 98, status: "published" },
|
|
4
|
+
{ id: 3, title: "Founders Breakfast", date: "2026-11-02", venue: "Dubai Mall Lounge", capacity: 40, booked: 40, status: "sold_out" },
|
|
5
|
+
{ id: 4, title: "CS Workshop", date: "2026-11-15", venue: "Online", capacity: 500, booked: 156, status: "draft" },
|
|
6
|
+
{ id: 5, title: "Annual Gala", date: "2026-12-05", venue: "InterContinental", capacity: 800, booked: 512, status: "published" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const invoices = [
|
|
2
|
+
{ id: 2001, number: "INV-2001", customer: "Aya El-Amin", amount: 1240, due_date: "2026-09-30", status: "sent", issued_at: "2026-09-01" },
|
|
3
|
+
{ id: 2002, number: "INV-2002", customer: "Karim Nasser", amount: 3410, due_date: "2026-10-05", status: "partial", issued_at: "2026-09-04" },
|
|
4
|
+
{ id: 2003, number: "INV-2003", customer: "Layla Haddad", amount: 428, due_date: "2026-09-20", status: "overdue", issued_at: "2026-08-05" },
|
|
5
|
+
{ id: 2004, number: "INV-2004", customer: "Nour Farouk", amount: 2280, due_date: "2026-10-12", status: "sent", issued_at: "2026-09-09" },
|
|
6
|
+
{ id: 2005, number: "INV-2005", customer: "Omar El-Sayed", amount: 640, due_date: "2026-10-01", status: "paid", issued_at: "2026-08-28" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const messages = [
|
|
2
|
+
{ id: 1, sender: "Aya El-Amin", channel: "email", preview: "Meeting rescheduled to 3pm…", sent_at: "2026-09-10 09:14", read: true },
|
|
3
|
+
{ id: 2, sender: "Support Bot", channel: "chat", preview: "Your ticket #2918 was resolved", sent_at: "2026-09-10 10:02", read: true },
|
|
4
|
+
{ id: 3, sender: "Omar El-Sayed", channel: "sms", preview: "Can you review the PR when you have a second?", sent_at: "2026-09-11 12:47", read: false },
|
|
5
|
+
{ id: 4, sender: "Finance Team", channel: "email", preview: "October payroll timeline attached", sent_at: "2026-09-11 14:30", read: false },
|
|
6
|
+
{ id: 5, sender: "Layla Haddad", channel: "telegram", preview: "Lunch on Thursday?", sent_at: "2026-09-12 08:05", read: false },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const orders = [
|
|
2
|
+
{ id: 1001, number: "ORD-1001", customer: "Aya El-Amin", status: "paid", items: 3, total: 184, placed_at: "2026-09-01" },
|
|
3
|
+
{ id: 1002, number: "ORD-1002", customer: "Omar El-Sayed", status: "pending", items: 1, total: 89, placed_at: "2026-09-03" },
|
|
4
|
+
{ id: 1003, number: "ORD-1003", customer: "Layla Haddad", status: "shipped", items: 2, total: 428, placed_at: "2026-09-05" },
|
|
5
|
+
{ id: 1004, number: "ORD-1004", customer: "Karim Nasser", status: "cancelled", items: 5, total: 612, placed_at: "2026-09-08" },
|
|
6
|
+
{ id: 1005, number: "ORD-1005", customer: "Nour Farouk", status: "refunded", items: 1, total: 74, placed_at: "2026-09-10" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const payments = [
|
|
2
|
+
{ id: 1, method: "visa", amount: 184, status: "succeeded", reference: "PAY-8821", paid_at: "2026-09-01" },
|
|
3
|
+
{ id: 2, method: "paypal", amount: 428, status: "succeeded", reference: "PAY-8843", paid_at: "2026-09-05" },
|
|
4
|
+
{ id: 3, method: "mastercard", amount: 612, status: "failed", reference: "PAY-8850", paid_at: "2026-09-08" },
|
|
5
|
+
{ id: 4, method: "bank_transfer", amount: 1240, status: "processing", reference: "PAY-8877", paid_at: "2026-09-09" },
|
|
6
|
+
{ id: 5, method: "cash", amount: 74, status: "refunded", reference: "PAY-8892", paid_at: "2026-09-12" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const posts = [
|
|
2
|
+
{ id: 1, title: "Welcome to the new dashboard", author: "Aya El-Amin", status: "published", views: 1240, published_at: "2026-08-01" },
|
|
3
|
+
{ id: 2, title: "How we cut load times by 60%", author: "Omar El-Sayed", status: "published", views: 860, published_at: "2026-08-14" },
|
|
4
|
+
{ id: 3, title: "Roadmap: Q4 2026", author: "Nour Farouk", status: "draft", views: 0, published_at: null },
|
|
5
|
+
{ id: 4, title: "Better onboarding in 5 steps", author: "Layla Haddad", status: "review", views: 0, published_at: null },
|
|
6
|
+
{ id: 5, title: "Security hardening guide", author: "Karim Nasser", status: "published", views: 2230, published_at: "2026-09-02" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const products = [
|
|
2
|
+
{ id: 1, name: "Aurora Lamp", sku: "AUR-001", category: "Home", price: 89, stock: 42, status: "in_stock" },
|
|
3
|
+
{ id: 2, name: "Nimbus Headphones", sku: "NIM-014", category: "Audio", price: 149, stock: 12, status: "in_stock" },
|
|
4
|
+
{ id: 3, name: "Terrace Chair", sku: "TER-022", category: "Furniture", price: 210, stock: 0, status: "out_of_stock" },
|
|
5
|
+
{ id: 4, name: "Lumen Desk", sku: "LUM-008", category: "Furniture", price: 340, stock: 7, status: "in_stock" },
|
|
6
|
+
{ id: 5, name: "Drift Backpack", sku: "DRI-031", category: "Travel", price: 74, stock: 96, status: "low" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const projects = [
|
|
2
|
+
{ id: 1, name: "Atlas Rebrand", owner: "Aya El-Amin", progress: 65, status: "active", deadline: "2026-11-01" },
|
|
3
|
+
{ id: 2, name: "Mobile App v2", owner: "Omar El-Sayed", progress: 28, status: "active", deadline: "2026-12-15" },
|
|
4
|
+
{ id: 3, name: "API V2 Migration", owner: "Karim Nasser", progress: 100, status: "done", deadline: "2026-09-01" },
|
|
5
|
+
{ id: 4, name: "Marketing Site", owner: "Layla Haddad", progress: 0, status: "on_hold", deadline: "2027-01-20" },
|
|
6
|
+
{ id: 5, name: "Automation Playbook", owner: "Nour Farouk", progress: 12, status: "active", deadline: "2026-10-30" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const tasks = [
|
|
2
|
+
{ id: 1, title: "Finalize landing page copy", assignee: "Aya El-Amin", priority: "high", status: "in_progress", due_date: "2026-09-18" },
|
|
3
|
+
{ id: 2, title: "Fix checkout bug #412", assignee: "Omar El-Sayed", priority: "critical", status: "todo", due_date: "2026-09-16" },
|
|
4
|
+
{ id: 3, title: "Update pricing page", assignee: "Layla Haddad", priority: "medium", status: "done", due_date: "2026-09-12" },
|
|
5
|
+
{ id: 4, title: "Migrate database indexes", assignee: "Karim Nasser", priority: "high", status: "todo", due_date: "2026-09-22" },
|
|
6
|
+
{ id: 5, title: "Prepare Q4 roadmap", assignee: "Nour Farouk", priority: "medium", status: "in_progress", due_date: "2026-09-30" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Static demo data — users. Import in your own code, e.g.
|
|
2
|
+
// `import { users } from "@/data/demo/users";`
|
|
3
|
+
export const users = [
|
|
4
|
+
{ id: 1, name: "Aya El-Amin", email: "aya@example.com", role: "admin", status: "active", joined_at: "2026-01-12" },
|
|
5
|
+
{ id: 2, name: "Omar El-Sayed", email: "omar@example.com", role: "user", status: "active", joined_at: "2026-02-03" },
|
|
6
|
+
{ id: 3, name: "Layla Haddad", email: "layla@example.com", role: "user", status: "pending", joined_at: "2026-03-18" },
|
|
7
|
+
{ id: 4, name: "Karim Nasser", email: "karim@example.com", role: "user", status: "suspended", joined_at: "2026-04-09" },
|
|
8
|
+
{ id: 5, name: "Nour Farouk", email: "nour@example.com", role: "master", status: "active", joined_at: "2026-05-27" },
|
|
9
|
+
];
|