@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.
Files changed (37) hide show
  1. package/README.md +378 -0
  2. package/bin/falak.js +157 -0
  3. package/index.js +5 -0
  4. package/lib/scaffold.js +23 -0
  5. package/package.json +46 -0
  6. package/template/_env.example +34 -0
  7. package/template/_gitignore +8 -0
  8. package/template/firebase-rules.json +36 -0
  9. package/template/index.html +21 -0
  10. package/template/package.json +36 -0
  11. package/template/postcss.config.js +6 -0
  12. package/template/public/favicon.svg +5 -0
  13. package/template/src/App.vue +95 -0
  14. package/template/src/assets/main.css +100 -0
  15. package/template/src/components/layout/AppLayout.vue +163 -0
  16. package/template/src/composables/useAuth.js +393 -0
  17. package/template/src/composables/useCrypto.js +153 -0
  18. package/template/src/composables/useDatabase.js +341 -0
  19. package/template/src/composables/useGroq.js +237 -0
  20. package/template/src/composables/usePaymob.js +392 -0
  21. package/template/src/firebase/index.js +87 -0
  22. package/template/src/i18n/index.js +66 -0
  23. package/template/src/i18n/locales/ar.json +121 -0
  24. package/template/src/i18n/locales/en.json +121 -0
  25. package/template/src/main.js +59 -0
  26. package/template/src/router/index.js +127 -0
  27. package/template/src/stores/auth.js +14 -0
  28. package/template/src/views/AdminView.vue +67 -0
  29. package/template/src/views/DashboardView.vue +253 -0
  30. package/template/src/views/HomeView.vue +13 -0
  31. package/template/src/views/NotFoundView.vue +8 -0
  32. package/template/src/views/ProfileView.vue +134 -0
  33. package/template/src/views/auth/ForgotView.vue +57 -0
  34. package/template/src/views/auth/LoginView.vue +169 -0
  35. package/template/src/views/auth/RegisterView.vue +103 -0
  36. package/template/tailwind.config.js +41 -0
  37. package/template/vite.config.js +29 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * useDatabase — Realtime Database Manager with Offline Support
3
+ * ─────────────────────────────────────────────────────────────
4
+ * Features:
5
+ * • Reads/writes to Firebase Realtime Database
6
+ * • Mirrors data in IndexedDB for offline access
7
+ * • Detects online/offline status — blocks writes when offline
8
+ * • Listens for remote changes and syncs to IndexedDB automatically
9
+ * • Optional field-level encryption via useCrypto
10
+ *
11
+ * Usage:
12
+ * const db = useDatabase()
13
+ * const todos = await db.listen('todos') // reactive ref + live sync
14
+ * await db.set('todos/1', { title: 'Buy milk' })
15
+ * await db.update('todos/1', { done: true })
16
+ * await db.remove('todos/1')
17
+ * const item = await db.get('todos/1')
18
+ */
19
+
20
+ import { ref, readonly, onUnmounted } from 'vue'
21
+ import {
22
+ ref as dbRef,
23
+ get,
24
+ set,
25
+ update,
26
+ remove,
27
+ push,
28
+ onValue,
29
+ onChildAdded,
30
+ onChildChanged,
31
+ onChildRemoved,
32
+ serverTimestamp,
33
+ query,
34
+ orderByChild,
35
+ equalTo,
36
+ limitToLast,
37
+ limitToFirst
38
+ } from 'firebase/database'
39
+ import { openDB } from 'idb'
40
+ import { rtdb } from '@/firebase/index.js'
41
+ import { useCrypto } from './useCrypto.js'
42
+
43
+ // ── IndexedDB Setup ───────────────────────────
44
+ const IDB_NAME = 'falak_offline_db'
45
+ const IDB_VERSION = 1
46
+
47
+ async function getIDB() {
48
+ return openDB(IDB_NAME, IDB_VERSION, {
49
+ upgrade(db) {
50
+ if (!db.objectStoreNames.contains('cache')) {
51
+ const store = db.createObjectStore('cache', { keyPath: 'path' })
52
+ store.createIndex('updatedAt', 'updatedAt')
53
+ }
54
+ if (!db.objectStoreNames.contains('pending_writes')) {
55
+ db.createObjectStore('pending_writes', { keyPath: 'id', autoIncrement: true })
56
+ }
57
+ }
58
+ })
59
+ }
60
+
61
+ // ── Online Status ─────────────────────────────
62
+ const isOnline = ref(navigator.onLine)
63
+ window.addEventListener('online', () => { isOnline.value = true })
64
+ window.addEventListener('offline', () => { isOnline.value = false })
65
+
66
+ // ── Active listeners registry ─────────────────
67
+ const listeners = new Map()
68
+
69
+ export function useDatabase(options = {}) {
70
+ const {
71
+ encryptedPaths = [], // ['users/*/email', 'payments/*']
72
+ encryptFields: encryptFieldsList = [] // field names to encrypt
73
+ } = options
74
+
75
+ const { encrypt, decrypt, encryptFields, decryptFields } = useCrypto()
76
+ const unsubscribers = []
77
+
78
+ // ── Helpers ────────────────────────────────
79
+ function shouldEncrypt(path) {
80
+ return encryptedPaths.some((pattern) => {
81
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '[^/]+') + '$')
82
+ return regex.test(path)
83
+ })
84
+ }
85
+
86
+ function prepareForWrite(path, data) {
87
+ if (!data || typeof data !== 'object') return data
88
+ return shouldEncrypt(path) || encryptFieldsList.length
89
+ ? encryptFields(data, encryptFieldsList)
90
+ : data
91
+ }
92
+
93
+ function prepareForRead(path, data) {
94
+ if (!data || typeof data !== 'object') return data
95
+ return shouldEncrypt(path) || encryptFieldsList.length
96
+ ? decryptFields(data, encryptFieldsList)
97
+ : data
98
+ }
99
+
100
+ // ── IndexedDB Helpers ─────────────────────
101
+ async function idbGet(path) {
102
+ const db = await getIDB()
103
+ const item = await db.get('cache', path)
104
+ return item ? prepareForRead(path, item.data) : null
105
+ }
106
+
107
+ async function idbSet(path, data) {
108
+ const db = await getIDB()
109
+ await db.put('cache', { path, data, updatedAt: Date.now() })
110
+ }
111
+
112
+ async function idbDelete(path) {
113
+ const db = await getIDB()
114
+ await db.delete('cache', path)
115
+ }
116
+
117
+ async function idbGetAll(prefix) {
118
+ const db = await getIDB()
119
+ const all = await db.getAll('cache')
120
+ return all
121
+ .filter(item => item.path.startsWith(prefix))
122
+ .reduce((acc, item) => {
123
+ const key = item.path.replace(prefix + '/', '')
124
+ acc[key] = prepareForRead(item.path, item.data)
125
+ return acc
126
+ }, {})
127
+ }
128
+
129
+ // ── READ ───────────────────────────────────
130
+ /**
131
+ * One-time read. Returns offline data if offline.
132
+ */
133
+ async function get$(path) {
134
+ if (!isOnline.value) {
135
+ return idbGet(path)
136
+ }
137
+ const snap = await get(dbRef(rtdb, path))
138
+ const data = snap.exists() ? snap.val() : null
139
+ if (data) await idbSet(path, data)
140
+ return prepareForRead(path, data)
141
+ }
142
+
143
+ // ── WRITE ──────────────────────────────────
144
+ /**
145
+ * Set data at path. Blocked offline.
146
+ */
147
+ async function set$(path, data) {
148
+ if (!isOnline.value) {
149
+ return { success: false, error: 'You are offline. Data is read-only.' }
150
+ }
151
+ try {
152
+ const prepared = prepareForWrite(path, data)
153
+ await set(dbRef(rtdb, path), {
154
+ ...prepared,
155
+ updatedAt: serverTimestamp()
156
+ })
157
+ await idbSet(path, data)
158
+ return { success: true }
159
+ } catch (err) {
160
+ return { success: false, error: err.message }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Update (merge) data at path. Blocked offline.
166
+ */
167
+ async function update$(path, data) {
168
+ if (!isOnline.value) {
169
+ return { success: false, error: 'You are offline. Data is read-only.' }
170
+ }
171
+ try {
172
+ const prepared = prepareForWrite(path, data)
173
+ await update(dbRef(rtdb, path), {
174
+ ...prepared,
175
+ updatedAt: serverTimestamp()
176
+ })
177
+ // Merge into IDB cache
178
+ const existing = await idbGet(path) || {}
179
+ await idbSet(path, { ...existing, ...data })
180
+ return { success: true }
181
+ } catch (err) {
182
+ return { success: false, error: err.message }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Push new child to a list. Blocked offline.
188
+ */
189
+ async function push$(path, data) {
190
+ if (!isOnline.value) {
191
+ return { success: false, error: 'You are offline. Data is read-only.' }
192
+ }
193
+ try {
194
+ const prepared = prepareForWrite(path, data)
195
+ const newRef = await push(dbRef(rtdb, path), {
196
+ ...prepared,
197
+ createdAt: serverTimestamp()
198
+ })
199
+ await idbSet(`${path}/${newRef.key}`, data)
200
+ return { success: true, key: newRef.key }
201
+ } catch (err) {
202
+ return { success: false, error: err.message }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Remove data at path. Blocked offline.
208
+ */
209
+ async function remove$(path) {
210
+ if (!isOnline.value) {
211
+ return { success: false, error: 'You are offline. Data is read-only.' }
212
+ }
213
+ try {
214
+ await remove(dbRef(rtdb, path))
215
+ await idbDelete(path)
216
+ return { success: true }
217
+ } catch (err) {
218
+ return { success: false, error: err.message }
219
+ }
220
+ }
221
+
222
+ // ── REAL-TIME LISTENER ─────────────────────
223
+ /**
224
+ * Subscribe to a path. Returns reactive ref updated live.
225
+ * Also syncs to IndexedDB on every remote change.
226
+ *
227
+ * @param {string} path - RTDB path
228
+ * @param {object} queryOptions - { orderBy, equalTo, limit, limitMode }
229
+ * @returns {{ data: Ref, unsubscribe: Function }}
230
+ */
231
+ function listen(path, queryOptions = {}) {
232
+ const data = ref(null)
233
+
234
+ // Preload from IDB
235
+ idbGet(path).then((cached) => {
236
+ if (cached && data.value === null) data.value = cached
237
+ })
238
+
239
+ let q = dbRef(rtdb, path)
240
+ if (queryOptions.orderBy) {
241
+ q = query(q, orderByChild(queryOptions.orderBy))
242
+ }
243
+ if (queryOptions.equalTo !== undefined) {
244
+ q = query(q, equalTo(queryOptions.equalTo))
245
+ }
246
+ if (queryOptions.limit) {
247
+ q = queryOptions.limitMode === 'first'
248
+ ? query(q, limitToFirst(queryOptions.limit))
249
+ : query(q, limitToLast(queryOptions.limit))
250
+ }
251
+
252
+ const unsub = onValue(q, async (snap) => {
253
+ const val = snap.exists() ? snap.val() : null
254
+ if (val) await idbSet(path, val)
255
+ data.value = prepareForRead(path, val)
256
+ })
257
+
258
+ unsubscribers.push(unsub)
259
+
260
+ return {
261
+ data: readonly(data),
262
+ unsubscribe: unsub
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Listen to child-level events (added/changed/removed) on a list.
268
+ * More efficient than listening to the whole list.
269
+ *
270
+ * @returns {{ items: Ref<Object>, unsubscribe: Function }}
271
+ */
272
+ function listenList(path) {
273
+ const items = ref({})
274
+
275
+ // Preload from IDB
276
+ idbGetAll(path).then((cached) => {
277
+ if (Object.keys(cached).length && Object.keys(items.value).length === 0) {
278
+ items.value = cached
279
+ }
280
+ })
281
+
282
+ const baseRef = dbRef(rtdb, path)
283
+
284
+ const u1 = onChildAdded(baseRef, async (snap) => {
285
+ const key = snap.key
286
+ const val = prepareForRead(`${path}/${key}`, snap.val())
287
+ items.value = { ...items.value, [key]: val }
288
+ await idbSet(`${path}/${key}`, snap.val())
289
+ })
290
+
291
+ const u2 = onChildChanged(baseRef, async (snap) => {
292
+ const key = snap.key
293
+ const val = prepareForRead(`${path}/${key}`, snap.val())
294
+ items.value = { ...items.value, [key]: val }
295
+ await idbSet(`${path}/${key}`, snap.val())
296
+ })
297
+
298
+ const u3 = onChildRemoved(baseRef, async (snap) => {
299
+ const key = snap.key
300
+ const copy = { ...items.value }
301
+ delete copy[key]
302
+ items.value = copy
303
+ await idbDelete(`${path}/${key}`)
304
+ })
305
+
306
+ const unsubscribe = () => { u1(); u2(); u3() }
307
+ unsubscribers.push(unsubscribe)
308
+
309
+ return { items, unsubscribe }
310
+ }
311
+
312
+ // ── Offline IDB-only read ──────────────────
313
+ async function getOffline(path) {
314
+ return idbGet(path)
315
+ }
316
+
317
+ async function getAllOffline(prefix) {
318
+ return idbGetAll(prefix)
319
+ }
320
+
321
+ // ── Cleanup on component unmount ───────────
322
+ onUnmounted(() => {
323
+ unsubscribers.forEach((u) => u())
324
+ })
325
+
326
+ return {
327
+ // State
328
+ isOnline: readonly(isOnline),
329
+
330
+ // Methods
331
+ get: get$,
332
+ set: set$,
333
+ update: update$,
334
+ push: push$,
335
+ remove: remove$,
336
+ listen,
337
+ listenList,
338
+ getOffline,
339
+ getAllOffline
340
+ }
341
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * useGroq — Groq AI Composable
3
+ * ──────────────────────────────
4
+ * Wraps groq-sdk to provide chat completions, streaming, and
5
+ * a simple conversation history manager.
6
+ *
7
+ * Usage:
8
+ * const groq = useGroq()
9
+ * const reply = await groq.chat('What is Vue.js?')
10
+ *
11
+ * // Streaming
12
+ * await groq.streamChat('Tell me a story', (chunk) => console.log(chunk))
13
+ *
14
+ * // Multi-turn conversation
15
+ * groq.addMessage('user', 'Hello')
16
+ * const reply = await groq.sendConversation()
17
+ */
18
+
19
+ import Groq from 'groq-sdk'
20
+ import { ref, readonly } from 'vue'
21
+
22
+ const DEFAULT_MODEL = import.meta.env.VITE_GROQ_MODEL || 'llama-3.3-70b-versatile'
23
+ const GROQ_API_KEY = import.meta.env.VITE_GROQ_API_KEY || ''
24
+
25
+ // Singleton client
26
+ let _client = null
27
+ function getClient() {
28
+ if (!_client) {
29
+ if (!GROQ_API_KEY) {
30
+ console.warn('[useGroq] VITE_GROQ_API_KEY is not set')
31
+ }
32
+ _client = new Groq({
33
+ apiKey: GROQ_API_KEY,
34
+ dangerouslyAllowBrowser: true // required for browser usage
35
+ })
36
+ }
37
+ return _client
38
+ }
39
+
40
+ export function useGroq(defaultSystemPrompt = null) {
41
+ const isLoading = ref(false)
42
+ const lastError = ref(null)
43
+ const messages = ref([]) // conversation history
44
+
45
+ if (defaultSystemPrompt) {
46
+ messages.value = [{ role: 'system', content: defaultSystemPrompt }]
47
+ }
48
+
49
+ // ── Single completion ──────────────────────
50
+ /**
51
+ * Send a single prompt and get a response.
52
+ * Does NOT track conversation history.
53
+ */
54
+ async function chat(prompt, options = {}) {
55
+ isLoading.value = true
56
+ lastError.value = null
57
+ try {
58
+ const completion = await getClient().chat.completions.create({
59
+ model: options.model || DEFAULT_MODEL,
60
+ max_tokens: options.maxTokens || 1024,
61
+ temperature: options.temperature ?? 0.7,
62
+ messages: [
63
+ ...(options.systemPrompt
64
+ ? [{ role: 'system', content: options.systemPrompt }]
65
+ : []),
66
+ { role: 'user', content: prompt }
67
+ ]
68
+ })
69
+ return completion.choices[0]?.message?.content || ''
70
+ } catch (err) {
71
+ lastError.value = err.message
72
+ throw err
73
+ } finally {
74
+ isLoading.value = false
75
+ }
76
+ }
77
+
78
+ // ── Streaming completion ───────────────────
79
+ /**
80
+ * Stream a response, calling onChunk for each token.
81
+ * @param {string} prompt
82
+ * @param {Function} onChunk - called with each text chunk
83
+ * @param {object} options
84
+ * @returns {Promise<string>} full response
85
+ */
86
+ async function streamChat(prompt, onChunk, options = {}) {
87
+ isLoading.value = true
88
+ lastError.value = null
89
+ let fullText = ''
90
+ try {
91
+ const stream = await getClient().chat.completions.create({
92
+ model: options.model || DEFAULT_MODEL,
93
+ max_tokens: options.maxTokens || 2048,
94
+ temperature: options.temperature ?? 0.7,
95
+ stream: true,
96
+ messages: [
97
+ ...(options.systemPrompt
98
+ ? [{ role: 'system', content: options.systemPrompt }]
99
+ : []),
100
+ { role: 'user', content: prompt }
101
+ ]
102
+ })
103
+
104
+ for await (const chunk of stream) {
105
+ const text = chunk.choices[0]?.delta?.content || ''
106
+ if (text) {
107
+ fullText += text
108
+ onChunk(text, fullText)
109
+ }
110
+ }
111
+ return fullText
112
+ } catch (err) {
113
+ lastError.value = err.message
114
+ throw err
115
+ } finally {
116
+ isLoading.value = false
117
+ }
118
+ }
119
+
120
+ // ── Multi-turn conversation ────────────────
121
+ function addMessage(role, content) {
122
+ messages.value.push({ role, content })
123
+ }
124
+
125
+ function setSystemPrompt(prompt) {
126
+ const idx = messages.value.findIndex(m => m.role === 'system')
127
+ if (idx >= 0) {
128
+ messages.value[idx].content = prompt
129
+ } else {
130
+ messages.value.unshift({ role: 'system', content: prompt })
131
+ }
132
+ }
133
+
134
+ function clearConversation(keepSystem = true) {
135
+ if (keepSystem) {
136
+ messages.value = messages.value.filter(m => m.role === 'system')
137
+ } else {
138
+ messages.value = []
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Send current conversation history and get a reply.
144
+ * Automatically appends assistant reply to history.
145
+ */
146
+ async function sendConversation(userMessage = null, options = {}) {
147
+ if (userMessage) addMessage('user', userMessage)
148
+ isLoading.value = true
149
+ lastError.value = null
150
+ try {
151
+ const completion = await getClient().chat.completions.create({
152
+ model: options.model || DEFAULT_MODEL,
153
+ max_tokens: options.maxTokens || 1024,
154
+ temperature: options.temperature ?? 0.7,
155
+ messages: messages.value
156
+ })
157
+ const reply = completion.choices[0]?.message?.content || ''
158
+ addMessage('assistant', reply)
159
+ return reply
160
+ } catch (err) {
161
+ lastError.value = err.message
162
+ throw err
163
+ } finally {
164
+ isLoading.value = false
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Stream multi-turn conversation.
170
+ */
171
+ async function streamConversation(userMessage, onChunk, options = {}) {
172
+ addMessage('user', userMessage)
173
+ isLoading.value = true
174
+ lastError.value = null
175
+ let fullText = ''
176
+ try {
177
+ const stream = await getClient().chat.completions.create({
178
+ model: options.model || DEFAULT_MODEL,
179
+ max_tokens: options.maxTokens || 2048,
180
+ temperature: options.temperature ?? 0.7,
181
+ stream: true,
182
+ messages: messages.value
183
+ })
184
+ for await (const chunk of stream) {
185
+ const text = chunk.choices[0]?.delta?.content || ''
186
+ if (text) {
187
+ fullText += text
188
+ onChunk(text, fullText)
189
+ }
190
+ }
191
+ addMessage('assistant', fullText)
192
+ return fullText
193
+ } catch (err) {
194
+ lastError.value = err.message
195
+ throw err
196
+ } finally {
197
+ isLoading.value = false
198
+ }
199
+ }
200
+
201
+ // ── Audio transcription ────────────────────
202
+ /**
203
+ * Transcribe an audio file (Blob or File) using Whisper.
204
+ */
205
+ async function transcribe(audioFile, language = 'en') {
206
+ isLoading.value = true
207
+ try {
208
+ const transcription = await getClient().audio.transcriptions.create({
209
+ file: audioFile,
210
+ model: 'whisper-large-v3',
211
+ language,
212
+ response_format: 'json'
213
+ })
214
+ return transcription.text
215
+ } catch (err) {
216
+ lastError.value = err.message
217
+ throw err
218
+ } finally {
219
+ isLoading.value = false
220
+ }
221
+ }
222
+
223
+ return {
224
+ isLoading: readonly(isLoading),
225
+ lastError: readonly(lastError),
226
+ messages: readonly(messages),
227
+
228
+ chat,
229
+ streamChat,
230
+ sendConversation,
231
+ streamConversation,
232
+ addMessage,
233
+ setSystemPrompt,
234
+ clearConversation,
235
+ transcribe
236
+ }
237
+ }