@ma7moudsalama/falak-app 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/README.md +378 -0
- package/bin/falak.js +157 -0
- package/index.js +5 -0
- package/lib/scaffold.js +23 -0
- package/package.json +46 -0
- package/template/_env.example +34 -0
- package/template/_gitignore +8 -0
- package/template/firebase-rules.json +36 -0
- package/template/index.html +21 -0
- package/template/package.json +36 -0
- package/template/postcss.config.js +6 -0
- package/template/public/favicon.svg +5 -0
- package/template/src/App.vue +95 -0
- package/template/src/assets/main.css +100 -0
- package/template/src/components/layout/AppLayout.vue +163 -0
- package/template/src/composables/useAuth.js +393 -0
- package/template/src/composables/useCrypto.js +153 -0
- package/template/src/composables/useDatabase.js +341 -0
- package/template/src/composables/useGroq.js +237 -0
- package/template/src/composables/usePaymob.js +392 -0
- package/template/src/firebase/index.js +87 -0
- package/template/src/i18n/index.js +66 -0
- package/template/src/i18n/locales/ar.json +121 -0
- package/template/src/i18n/locales/en.json +121 -0
- package/template/src/main.js +59 -0
- package/template/src/router/index.js +127 -0
- package/template/src/stores/auth.js +14 -0
- package/template/src/views/AdminView.vue +67 -0
- package/template/src/views/DashboardView.vue +253 -0
- package/template/src/views/HomeView.vue +13 -0
- package/template/src/views/NotFoundView.vue +8 -0
- package/template/src/views/ProfileView.vue +134 -0
- package/template/src/views/auth/ForgotView.vue +57 -0
- package/template/src/views/auth/LoginView.vue +169 -0
- package/template/src/views/auth/RegisterView.vue +103 -0
- package/template/tailwind.config.js +41 -0
- package/template/vite.config.js +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# فَلَك · Falak App
|
|
2
|
+
|
|
3
|
+
> **Production-ready Vue 3 boilerplate** with Firebase, PrimeVue, Tailwind CSS, i18n (Arabic + English), Groq AI, Paymob payments, offline IndexedDB sync, and AES encryption — all wired up and ready to build on.
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.com/package/falak-app)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Scaffold a new project
|
|
14
|
+
npx falak-app create my-project
|
|
15
|
+
|
|
16
|
+
# Or install globally
|
|
17
|
+
npm i -g falak-app
|
|
18
|
+
falak create my-project
|
|
19
|
+
|
|
20
|
+
# Then
|
|
21
|
+
cd my-project
|
|
22
|
+
cp .env.example .env # fill in your keys
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What's Inside
|
|
29
|
+
|
|
30
|
+
| Layer | Package | Status |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Framework | Vue 3 + Vite | ✅ Configured |
|
|
33
|
+
| UI Library | PrimeVue 4 (Aura theme) | ✅ Configured |
|
|
34
|
+
| Styling | Tailwind CSS 3 | ✅ Configured |
|
|
35
|
+
| State | Pinia | ✅ Configured |
|
|
36
|
+
| Routing | Vue Router 4 | ✅ Configured + guards |
|
|
37
|
+
| Translation | Vue I18n 10 (EN + AR/RTL) | ✅ Configured |
|
|
38
|
+
| Auth | Firebase Auth (Email, Google, Facebook) | ✅ Composable ready |
|
|
39
|
+
| Database | Firebase Realtime DB | ✅ Composable ready |
|
|
40
|
+
| Offline | IndexedDB via `idb` | ✅ Auto-sync |
|
|
41
|
+
| Encryption | AES-256 via `crypto-js` | ✅ Composable ready |
|
|
42
|
+
| AI | Groq SDK (LLaMA 3.3) | ✅ Composable ready |
|
|
43
|
+
| Payments | Paymob | ✅ Composable + subscriptions |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Project Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/
|
|
51
|
+
├── assets/
|
|
52
|
+
│ └── main.css ← Tailwind + custom utilities
|
|
53
|
+
├── components/
|
|
54
|
+
│ └── layout/
|
|
55
|
+
│ └── AppLayout.vue ← Sidebar + topbar layout shell
|
|
56
|
+
├── composables/
|
|
57
|
+
│ ├── useAuth.js ← Firebase auth + roles + sessions
|
|
58
|
+
│ ├── useDatabase.js ← RTDB + IndexedDB offline manager
|
|
59
|
+
│ ├── useCrypto.js ← AES encrypt/decrypt
|
|
60
|
+
│ ├── useGroq.js ← Groq AI chat + streaming
|
|
61
|
+
│ └── usePaymob.js ← Paymob payments + subscriptions
|
|
62
|
+
├── firebase/
|
|
63
|
+
│ └── index.js ← Firebase app init + all services
|
|
64
|
+
├── i18n/
|
|
65
|
+
│ ├── index.js ← Vue I18n setup + locale switcher
|
|
66
|
+
│ └── locales/
|
|
67
|
+
│ ├── en.json
|
|
68
|
+
│ └── ar.json
|
|
69
|
+
├── router/
|
|
70
|
+
│ └── index.js ← Routes + auth/role guards
|
|
71
|
+
├── stores/
|
|
72
|
+
│ └── auth.js ← Pinia wrapper for useAuth
|
|
73
|
+
├── views/
|
|
74
|
+
│ ├── auth/
|
|
75
|
+
│ │ ├── LoginView.vue
|
|
76
|
+
│ │ ├── RegisterView.vue
|
|
77
|
+
│ │ └── ForgotView.vue
|
|
78
|
+
│ ├── HomeView.vue
|
|
79
|
+
│ ├── DashboardView.vue ← Live data + AI + subscription demo
|
|
80
|
+
│ ├── ProfileView.vue
|
|
81
|
+
│ ├── AdminView.vue
|
|
82
|
+
│ └── NotFoundView.vue
|
|
83
|
+
├── App.vue
|
|
84
|
+
└── main.js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
### 1. Firebase
|
|
92
|
+
|
|
93
|
+
Edit `src/firebase/index.js` or set your `.env`:
|
|
94
|
+
|
|
95
|
+
```env
|
|
96
|
+
VITE_FIREBASE_API_KEY=...
|
|
97
|
+
VITE_FIREBASE_AUTH_DOMAIN=...
|
|
98
|
+
VITE_FIREBASE_DATABASE_URL=...
|
|
99
|
+
VITE_FIREBASE_PROJECT_ID=...
|
|
100
|
+
VITE_FIREBASE_STORAGE_BUCKET=...
|
|
101
|
+
VITE_FIREBASE_MESSAGING_SENDER_ID=...
|
|
102
|
+
VITE_FIREBASE_APP_ID=...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Enable in Firebase Console:
|
|
106
|
+
- Authentication → Email/Password, Google, Facebook
|
|
107
|
+
- Realtime Database → create database
|
|
108
|
+
- Upload `firebase-rules.json` as your security rules
|
|
109
|
+
|
|
110
|
+
### 2. Groq AI
|
|
111
|
+
|
|
112
|
+
```env
|
|
113
|
+
VITE_GROQ_API_KEY=gsk_...
|
|
114
|
+
VITE_GROQ_MODEL=llama-3.3-70b-versatile
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Get your key at [console.groq.com](https://console.groq.com).
|
|
118
|
+
|
|
119
|
+
### 3. Paymob
|
|
120
|
+
|
|
121
|
+
```env
|
|
122
|
+
VITE_PAYMOB_API_KEY=...
|
|
123
|
+
VITE_PAYMOB_INTEGRATION_ID=...
|
|
124
|
+
VITE_PAYMOB_IFRAME_ID=...
|
|
125
|
+
VITE_PAYMOB_HMAC_SECRET=...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Get credentials from [accept.paymob.com](https://accept.paymob.com).
|
|
129
|
+
|
|
130
|
+
### 4. Encryption
|
|
131
|
+
|
|
132
|
+
```env
|
|
133
|
+
VITE_ENCRYPTION_KEY=your_strong_random_secret
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Composables API
|
|
139
|
+
|
|
140
|
+
### `useAuth()`
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
import { useAuth } from '@/composables/useAuth.js'
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
currentUser, // Ref<FirebaseUser | null>
|
|
147
|
+
userProfile, // Ref<Object> — RTDB profile
|
|
148
|
+
userRole, // Ref<string> — live from RTDB
|
|
149
|
+
isAuthenticated, // ComputedRef<bool>
|
|
150
|
+
isAdmin, // ComputedRef<bool>
|
|
151
|
+
isSuperAdmin, // ComputedRef<bool>
|
|
152
|
+
isAuthReady, // Ref<bool> — true after first auth check
|
|
153
|
+
isLoading, // Ref<bool>
|
|
154
|
+
authError, // Ref<string | null>
|
|
155
|
+
|
|
156
|
+
register, // ({ email, password, displayName }) → { success, user }
|
|
157
|
+
login, // ({ email, password }) → { success, user }
|
|
158
|
+
logout, // → { success }
|
|
159
|
+
resetPassword, // (email) → { success }
|
|
160
|
+
changePassword, // ({ currentPassword, newPassword }) → { success }
|
|
161
|
+
loginWithGoogle, // (useRedirect?) → { success, user }
|
|
162
|
+
loginWithFacebook,// (useRedirect?) → { success, user }
|
|
163
|
+
updateUserProfile,// (data) → { success }
|
|
164
|
+
setUserRole, // (uid, role) → { success } ← admin only
|
|
165
|
+
deleteAccount, // → { success }
|
|
166
|
+
hasPermission, // (permission) → bool
|
|
167
|
+
hasRole, // (...roles) → bool
|
|
168
|
+
ROLES // { SUPER_ADMIN, ADMIN, USER, GUEST }
|
|
169
|
+
} = useAuth()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**User roles** are stored in `/users/{uid}/role` in RTDB and listened to in real-time.
|
|
173
|
+
**Session data** (online, lastSeen) is written to `/sessions/{uid}` with `onDisconnect` handling.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `useDatabase(options?)`
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { useDatabase } from '@/composables/useDatabase.js'
|
|
181
|
+
|
|
182
|
+
const db = useDatabase({
|
|
183
|
+
encryptedPaths: ['payments/*'], // paths to auto-encrypt
|
|
184
|
+
encryptFields: ['ssn', 'card'] // field names to encrypt
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// One-time read (offline-aware)
|
|
188
|
+
const user = await db.get('users/uid123')
|
|
189
|
+
|
|
190
|
+
// Write (blocked offline with error)
|
|
191
|
+
await db.set('todos/1', { text: 'Buy milk', done: false })
|
|
192
|
+
await db.update('todos/1', { done: true })
|
|
193
|
+
await db.push('todos', { text: 'New item' }) // → { success, key }
|
|
194
|
+
await db.remove('todos/1')
|
|
195
|
+
|
|
196
|
+
// Real-time listener (reactive ref + IDB sync)
|
|
197
|
+
const { data, unsubscribe } = db.listen('todos')
|
|
198
|
+
// data.value updates live
|
|
199
|
+
|
|
200
|
+
// Real-time child events (efficient for large lists)
|
|
201
|
+
const { items, unsubscribe } = db.listenList('messages')
|
|
202
|
+
// items.value = { key1: {...}, key2: {...} }
|
|
203
|
+
|
|
204
|
+
// Offline-only reads
|
|
205
|
+
const cached = await db.getOffline('todos')
|
|
206
|
+
const all = await db.getAllOffline('todos')
|
|
207
|
+
|
|
208
|
+
// Network status
|
|
209
|
+
db.isOnline // Ref<bool>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Offline behavior:**
|
|
213
|
+
- All write operations return `{ success: false, error: 'You are offline. Data is read-only.' }` when offline.
|
|
214
|
+
- Read operations return IndexedDB cached data.
|
|
215
|
+
- Remote changes are auto-synced to IDB when online.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### `useCrypto()`
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
import { useCrypto } from '@/composables/useCrypto.js'
|
|
223
|
+
|
|
224
|
+
const { encrypt, decrypt, encryptObject, decryptObject,
|
|
225
|
+
encryptFields, decryptFields, hash, hmac, randomToken } = useCrypto()
|
|
226
|
+
|
|
227
|
+
const cipher = encrypt('sensitive data')
|
|
228
|
+
const plain = decrypt(cipher)
|
|
229
|
+
|
|
230
|
+
const encObj = encryptObject({ ssn: '123-45-6789', name: 'Max' })
|
|
231
|
+
const obj = decryptObject(encObj)
|
|
232
|
+
|
|
233
|
+
// Encrypt only specific fields
|
|
234
|
+
const safe = encryptFields(userData, ['ssn', 'bankAccount'])
|
|
235
|
+
const back = decryptFields(safe, ['ssn', 'bankAccount'])
|
|
236
|
+
|
|
237
|
+
// SHA256 hash
|
|
238
|
+
const hashed = hash('mypassword')
|
|
239
|
+
|
|
240
|
+
// HMAC (used by Paymob webhook verification)
|
|
241
|
+
const sig = hmac('message', 'secret')
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### `useGroq(systemPrompt?)`
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
import { useGroq } from '@/composables/useGroq.js'
|
|
250
|
+
|
|
251
|
+
const groq = useGroq('You are a helpful assistant.')
|
|
252
|
+
|
|
253
|
+
// Single prompt
|
|
254
|
+
const reply = await groq.chat('What is Vue.js?')
|
|
255
|
+
|
|
256
|
+
// Streaming
|
|
257
|
+
await groq.streamChat('Tell me a story', (chunk, full) => {
|
|
258
|
+
output.value = full
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Multi-turn conversation
|
|
262
|
+
await groq.sendConversation('Hello!')
|
|
263
|
+
await groq.sendConversation('What did I just say?')
|
|
264
|
+
|
|
265
|
+
// Streaming multi-turn
|
|
266
|
+
await groq.streamConversation('Continue...', (chunk) => { ... })
|
|
267
|
+
|
|
268
|
+
// Audio transcription (Whisper)
|
|
269
|
+
const text = await groq.transcribe(audioBlob, 'ar')
|
|
270
|
+
|
|
271
|
+
groq.clearConversation()
|
|
272
|
+
groq.setSystemPrompt('New system prompt')
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### `usePaymob()` + `useSubscription()`
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
import { usePaymob, useSubscription, PLANS } from '@/composables/usePaymob.js'
|
|
281
|
+
|
|
282
|
+
// ── Payments ──
|
|
283
|
+
const paymob = usePaymob()
|
|
284
|
+
|
|
285
|
+
// Full payment flow → opens iframe URL
|
|
286
|
+
const result = await paymob.initPayment({
|
|
287
|
+
amountCents: 14900, // 149.00 EGP
|
|
288
|
+
currency: 'EGP',
|
|
289
|
+
billingData: { ... }
|
|
290
|
+
})
|
|
291
|
+
// result.iframeUrl → embed or open
|
|
292
|
+
|
|
293
|
+
// Open in new tab
|
|
294
|
+
await paymob.openPaymentTab({ amountCents: 14900, ... })
|
|
295
|
+
|
|
296
|
+
// Verify Paymob webhook HMAC
|
|
297
|
+
const valid = paymob.verifyHmac(callbackData)
|
|
298
|
+
|
|
299
|
+
// ── Subscriptions ──
|
|
300
|
+
const sub = useSubscription()
|
|
301
|
+
|
|
302
|
+
// Initialize free plan for new users
|
|
303
|
+
await sub.initFreeplan(userId)
|
|
304
|
+
|
|
305
|
+
// Activate after successful payment
|
|
306
|
+
await sub.activateSubscription(userId, 'PRO', { paymobOrderId: 12345, amountCents: 14900 })
|
|
307
|
+
|
|
308
|
+
// Real-time subscription listener
|
|
309
|
+
const { data } = sub.listenSubscription(userId)
|
|
310
|
+
// data.value = { planId, status, expiresAt, features, ... }
|
|
311
|
+
|
|
312
|
+
// Feature gating
|
|
313
|
+
const canUseAPI = await sub.hasFeature(userId, 'api_access')
|
|
314
|
+
const active = await sub.isActive(userId)
|
|
315
|
+
|
|
316
|
+
await sub.cancelSubscription(userId)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Subscription data in RTDB** (`/subscriptions/{uid}`):
|
|
320
|
+
```json
|
|
321
|
+
{
|
|
322
|
+
"planId": "pro",
|
|
323
|
+
"planName": "Pro",
|
|
324
|
+
"status": "active",
|
|
325
|
+
"features": ["read", "write", "advanced_features", "api_access"],
|
|
326
|
+
"startedAt": 1700000000000,
|
|
327
|
+
"expiresAt": 1702592000000,
|
|
328
|
+
"autoRenew": true,
|
|
329
|
+
"paymentHistory": [{ "paidAt": 1700000000000, "amountCents": 14900 }]
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## i18n
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
import { setLocale } from '@/i18n/index.js'
|
|
339
|
+
|
|
340
|
+
setLocale('ar') // switches to Arabic + sets dir="rtl"
|
|
341
|
+
setLocale('en') // switches to English + sets dir="ltr"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
In templates:
|
|
345
|
+
```vue
|
|
346
|
+
<p>{{ $t('auth.email') }}</p>
|
|
347
|
+
<p>{{ $d(date, 'short') }}</p>
|
|
348
|
+
<p>{{ $n(149, 'egp') }}</p>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
To add a new language, add its locale file to `src/i18n/locales/` and register it in `src/i18n/index.js`.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Security Rules
|
|
356
|
+
|
|
357
|
+
Upload `firebase-rules.json` to your Firebase Realtime Database rules:
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
Firebase Console → Realtime Database → Rules → paste contents
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Scripts
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npm run dev # Start dev server (http://localhost:5173)
|
|
369
|
+
npm run build # Production build → dist/
|
|
370
|
+
npm run preview # Preview production build
|
|
371
|
+
npm run lint # Lint + auto-fix
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## License
|
|
377
|
+
|
|
378
|
+
MIT © Falak App
|
package/bin/falak.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import inquirer from 'inquirer'
|
|
6
|
+
import ora from 'ora'
|
|
7
|
+
import fs from 'fs-extra'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import { fileURLToPath } from 'url'
|
|
10
|
+
import { execSync } from 'child_process'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = path.dirname(__filename)
|
|
14
|
+
|
|
15
|
+
const TEMPLATE_DIR = path.join(__dirname, '..', 'template')
|
|
16
|
+
|
|
17
|
+
const banner = `
|
|
18
|
+
${chalk.cyan('███████╗ █████╗ ██╗ █████╗ ██╗ ██╗')}
|
|
19
|
+
${chalk.cyan('██╔════╝██╔══██╗██║ ██╔══██╗██║ ██╔╝')}
|
|
20
|
+
${chalk.cyan('█████╗ ███████║██║ ███████║█████╔╝ ')}
|
|
21
|
+
${chalk.cyan('██╔══╝ ██╔══██║██║ ██╔══██║██╔═██╗ ')}
|
|
22
|
+
${chalk.cyan('██║ ██║ ██║███████╗██║ ██║██║ ██╗')}
|
|
23
|
+
${chalk.cyan('╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝')}
|
|
24
|
+
${chalk.gray('Vue 3 Production Boilerplate — فلك')}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.name('falak-app')
|
|
29
|
+
.description('Create a new Falak Vue 3 project')
|
|
30
|
+
.version('1.0.0')
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('create [project-name]')
|
|
34
|
+
.alias('c')
|
|
35
|
+
.description('Scaffold a new Falak project')
|
|
36
|
+
.option('--no-install', 'Skip npm install')
|
|
37
|
+
.action(async (projectName, options) => {
|
|
38
|
+
console.log(banner)
|
|
39
|
+
|
|
40
|
+
const answers = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: 'input',
|
|
43
|
+
name: 'name',
|
|
44
|
+
message: 'Project name:',
|
|
45
|
+
default: projectName || 'my-falak-app',
|
|
46
|
+
when: !projectName,
|
|
47
|
+
validate: (v) => /^[a-z0-9-_]+$/.test(v) || 'Use lowercase letters, numbers, hyphens only'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'input',
|
|
51
|
+
name: 'displayName',
|
|
52
|
+
message: 'App display name:',
|
|
53
|
+
default: (a) => (a.name || projectName || 'My App')
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'confirm',
|
|
57
|
+
name: 'arabic',
|
|
58
|
+
message: 'Enable Arabic (RTL) locale by default?',
|
|
59
|
+
default: false
|
|
60
|
+
}
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const name = projectName || answers.name
|
|
64
|
+
const targetDir = path.resolve(process.cwd(), name)
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(targetDir)) {
|
|
67
|
+
const { overwrite } = await inquirer.prompt([{
|
|
68
|
+
type: 'confirm',
|
|
69
|
+
name: 'overwrite',
|
|
70
|
+
message: `Directory "${name}" already exists. Overwrite?`,
|
|
71
|
+
default: false
|
|
72
|
+
}])
|
|
73
|
+
if (!overwrite) {
|
|
74
|
+
console.log(chalk.red('Aborted.'))
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
fs.removeSync(targetDir)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const spinner = ora('Scaffolding project...').start()
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Copy template
|
|
84
|
+
fs.copySync(TEMPLATE_DIR, targetDir)
|
|
85
|
+
|
|
86
|
+
// Update package.json with project name
|
|
87
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
88
|
+
const pkg = fs.readJsonSync(pkgPath)
|
|
89
|
+
pkg.name = name
|
|
90
|
+
fs.writeJsonSync(pkgPath, pkg, { spaces: 2 })
|
|
91
|
+
|
|
92
|
+
// Update index.html title
|
|
93
|
+
const htmlPath = path.join(targetDir, 'index.html')
|
|
94
|
+
let html = fs.readFileSync(htmlPath, 'utf-8')
|
|
95
|
+
html = html.replace('__APP_NAME__', answers.displayName || name)
|
|
96
|
+
fs.writeFileSync(htmlPath, html)
|
|
97
|
+
|
|
98
|
+
// Rename _gitignore → .gitignore
|
|
99
|
+
const gitignoreSrc = path.join(targetDir, '_gitignore')
|
|
100
|
+
if (fs.existsSync(gitignoreSrc)) {
|
|
101
|
+
fs.moveSync(gitignoreSrc, path.join(targetDir, '.gitignore'))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Rename _env.example → .env.example
|
|
105
|
+
const envSrc = path.join(targetDir, '_env.example')
|
|
106
|
+
if (fs.existsSync(envSrc)) {
|
|
107
|
+
fs.moveSync(envSrc, path.join(targetDir, '.env.example'))
|
|
108
|
+
fs.copySync(path.join(targetDir, '.env.example'), path.join(targetDir, '.env'))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
spinner.succeed(chalk.green('Project scaffolded successfully!'))
|
|
112
|
+
|
|
113
|
+
if (options.install !== false) {
|
|
114
|
+
const installSpinner = ora('Installing dependencies...').start()
|
|
115
|
+
try {
|
|
116
|
+
execSync('npm install', { cwd: targetDir, stdio: 'ignore' })
|
|
117
|
+
installSpinner.succeed(chalk.green('Dependencies installed!'))
|
|
118
|
+
} catch {
|
|
119
|
+
installSpinner.warn(chalk.yellow('Auto-install failed. Run "npm install" manually.'))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`
|
|
124
|
+
${chalk.bold('✅ Falak project ready!')}
|
|
125
|
+
|
|
126
|
+
${chalk.cyan('Next steps:')}
|
|
127
|
+
${chalk.gray('$')} cd ${name}
|
|
128
|
+
${chalk.gray('$')} cp .env.example .env ${chalk.gray('# fill in your keys')}
|
|
129
|
+
${chalk.gray('$')} npm run dev
|
|
130
|
+
|
|
131
|
+
${chalk.cyan('Configure your services in:')}
|
|
132
|
+
${chalk.yellow('src/firebase/index.js')} — Firebase credentials
|
|
133
|
+
${chalk.yellow('src/config/groq.js')} — Groq API key
|
|
134
|
+
${chalk.yellow('src/config/paymob.js')} — Paymob credentials
|
|
135
|
+
${chalk.yellow('src/i18n/index.js')} — Add languages
|
|
136
|
+
|
|
137
|
+
${chalk.gray('Docs: https://github.com/falak-app/falak-app')}
|
|
138
|
+
`)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
spinner.fail(chalk.red('Scaffolding failed'))
|
|
141
|
+
console.error(err)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Default: if no sub-command, run create
|
|
147
|
+
program
|
|
148
|
+
.argument('[project-name]')
|
|
149
|
+
.action(async (projectName) => {
|
|
150
|
+
if (projectName) {
|
|
151
|
+
program.parse(['node', 'falak', 'create', projectName])
|
|
152
|
+
} else {
|
|
153
|
+
program.parse(['node', 'falak', 'create'])
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
program.parse()
|
package/index.js
ADDED
package/lib/scaffold.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const TEMPLATE = path.join(__dirname, '..', 'template')
|
|
7
|
+
|
|
8
|
+
export async function scaffold(targetDir, options = {}) {
|
|
9
|
+
fs.copySync(TEMPLATE, targetDir)
|
|
10
|
+
if (options.name) {
|
|
11
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
12
|
+
const pkg = fs.readJsonSync(pkgPath)
|
|
13
|
+
pkg.name = options.name
|
|
14
|
+
fs.writeJsonSync(pkgPath, pkg, { spaces: 2 })
|
|
15
|
+
}
|
|
16
|
+
const gitignoreSrc = path.join(targetDir, '_gitignore')
|
|
17
|
+
if (fs.existsSync(gitignoreSrc)) fs.moveSync(gitignoreSrc, path.join(targetDir, '.gitignore'))
|
|
18
|
+
const envSrc = path.join(targetDir, '_env.example')
|
|
19
|
+
if (fs.existsSync(envSrc)) {
|
|
20
|
+
fs.moveSync(envSrc, path.join(targetDir, '.env.example'))
|
|
21
|
+
fs.copySync(path.join(targetDir, '.env.example'), path.join(targetDir, '.env'))
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ma7moudsalama/falak-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready Vue 3 boilerplate with Firebase, PrimeVue, Tailwind, i18n, Groq AI, Paymob & more",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vue",
|
|
7
|
+
"vue3",
|
|
8
|
+
"boilerplate",
|
|
9
|
+
"firebase",
|
|
10
|
+
"primevue",
|
|
11
|
+
"tailwind",
|
|
12
|
+
"groq",
|
|
13
|
+
"paymob",
|
|
14
|
+
"starter",
|
|
15
|
+
"template"
|
|
16
|
+
],
|
|
17
|
+
"author": "MA7MOUD SALAMA <mahmoudsalamacoder@gmail.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"bin": {
|
|
20
|
+
"falak-app": "./bin/falak.js",
|
|
21
|
+
"falak": "./bin/falak.js"
|
|
22
|
+
},
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"template",
|
|
27
|
+
"lib",
|
|
28
|
+
"index.js",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.0.0",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"inquirer": "^9.2.15",
|
|
36
|
+
"ora": "^8.0.1"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/ma7moudsalamacoder/falak-app"
|
|
44
|
+
},
|
|
45
|
+
"type": "module"
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────
|
|
2
|
+
# Falak App — Environment Variables
|
|
3
|
+
# Copy this file to .env and fill in your values
|
|
4
|
+
# ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
# Firebase Configuration
|
|
7
|
+
VITE_FIREBASE_API_KEY=your_firebase_api_key
|
|
8
|
+
VITE_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
|
|
9
|
+
VITE_FIREBASE_DATABASE_URL=https://your_project-default-rtdb.firebaseio.com
|
|
10
|
+
VITE_FIREBASE_PROJECT_ID=your_project_id
|
|
11
|
+
VITE_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
|
|
12
|
+
VITE_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
|
|
13
|
+
VITE_FIREBASE_APP_ID=your_app_id
|
|
14
|
+
VITE_FIREBASE_MEASUREMENT_ID=G-XXXXXXXXXX
|
|
15
|
+
|
|
16
|
+
# Firebase OAuth Providers (optional)
|
|
17
|
+
VITE_FACEBOOK_APP_ID=your_facebook_app_id
|
|
18
|
+
|
|
19
|
+
# Encryption — change this to a strong random secret
|
|
20
|
+
VITE_ENCRYPTION_KEY=falak_super_secret_key_change_me_in_production
|
|
21
|
+
|
|
22
|
+
# Groq AI
|
|
23
|
+
VITE_GROQ_API_KEY=your_groq_api_key
|
|
24
|
+
VITE_GROQ_MODEL=llama-3.3-70b-versatile
|
|
25
|
+
|
|
26
|
+
# Paymob
|
|
27
|
+
VITE_PAYMOB_API_KEY=your_paymob_api_key
|
|
28
|
+
VITE_PAYMOB_INTEGRATION_ID=your_integration_id
|
|
29
|
+
VITE_PAYMOB_IFRAME_ID=your_iframe_id
|
|
30
|
+
VITE_PAYMOB_HMAC_SECRET=your_hmac_secret
|
|
31
|
+
|
|
32
|
+
# App
|
|
33
|
+
VITE_APP_NAME=Falak App
|
|
34
|
+
VITE_APP_ENV=development
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": {
|
|
3
|
+
".read": false,
|
|
4
|
+
".write": false,
|
|
5
|
+
|
|
6
|
+
"users": {
|
|
7
|
+
"$uid": {
|
|
8
|
+
".read": "$uid === auth.uid || root.child('users').child(auth.uid).child('role').val() === 'admin' || root.child('users').child(auth.uid).child('role').val() === 'super_admin'",
|
|
9
|
+
".write": "$uid === auth.uid || root.child('users').child(auth.uid).child('role').val() === 'super_admin'",
|
|
10
|
+
|
|
11
|
+
"role": {
|
|
12
|
+
".write": "root.child('users').child(auth.uid).child('role').val() === 'super_admin' || root.child('users').child(auth.uid).child('role').val() === 'admin'"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"sessions": {
|
|
18
|
+
"$uid": {
|
|
19
|
+
".read": "$uid === auth.uid",
|
|
20
|
+
".write": "$uid === auth.uid"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"subscriptions": {
|
|
25
|
+
"$uid": {
|
|
26
|
+
".read": "$uid === auth.uid || root.child('users').child(auth.uid).child('role').val() === 'admin' || root.child('users').child(auth.uid).child('role').val() === 'super_admin'",
|
|
27
|
+
".write": "$uid === auth.uid || root.child('users').child(auth.uid).child('role').val() === 'admin' || root.child('users').child(auth.uid).child('role').val() === 'super_admin'"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"demo_feed": {
|
|
32
|
+
".read": "auth !== null",
|
|
33
|
+
".write": "auth !== null"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|