@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,121 @@
1
+ {
2
+ "app": {
3
+ "name": "Falak App",
4
+ "tagline": "Built with Vue 3 & Firebase"
5
+ },
6
+ "nav": {
7
+ "home": "Home",
8
+ "dashboard": "Dashboard",
9
+ "profile": "Profile",
10
+ "admin": "Admin",
11
+ "login": "Sign In",
12
+ "register": "Create Account",
13
+ "logout": "Sign Out",
14
+ "settings": "Settings"
15
+ },
16
+ "auth": {
17
+ "email": "Email Address",
18
+ "password": "Password",
19
+ "confirmPassword": "Confirm Password",
20
+ "displayName": "Full Name",
21
+ "forgotPassword": "Forgot password?",
22
+ "rememberMe": "Remember me",
23
+ "noAccount": "Don't have an account?",
24
+ "hasAccount": "Already have an account?",
25
+ "orContinueWith": "Or continue with",
26
+ "signInWithGoogle": "Sign in with Google",
27
+ "signInWithFacebook": "Sign in with Facebook",
28
+ "resetPassword": "Reset Password",
29
+ "resetPasswordDesc": "We'll send you a link to reset your password.",
30
+ "backToLogin": "Back to sign in",
31
+ "emailVerification": "Please verify your email address. Check your inbox.",
32
+ "passwordMismatch": "Passwords do not match.",
33
+ "weakPassword": "Password must be at least 8 characters.",
34
+ "loginSuccess": "Welcome back!",
35
+ "logoutSuccess": "You have been signed out.",
36
+ "registerSuccess": "Account created! Please verify your email."
37
+ },
38
+ "user": {
39
+ "role": "Role",
40
+ "roles": {
41
+ "super_admin": "Super Admin",
42
+ "admin": "Admin",
43
+ "user": "User",
44
+ "guest": "Guest"
45
+ },
46
+ "profile": "Profile",
47
+ "editProfile": "Edit Profile",
48
+ "changePassword": "Change Password",
49
+ "deleteAccount": "Delete Account",
50
+ "deleteAccountConfirm": "Are you sure you want to delete your account? This action is irreversible.",
51
+ "avatar": "Profile Picture",
52
+ "memberSince": "Member since"
53
+ },
54
+ "subscription": {
55
+ "title": "Subscription",
56
+ "currentPlan": "Current Plan",
57
+ "upgradePlan": "Upgrade Plan",
58
+ "cancelPlan": "Cancel Subscription",
59
+ "renewPlan": "Renew",
60
+ "expiresOn": "Expires on",
61
+ "status": {
62
+ "active": "Active",
63
+ "expired": "Expired",
64
+ "cancelled": "Cancelled",
65
+ "pending": "Pending"
66
+ },
67
+ "plans": {
68
+ "free": "Free",
69
+ "basic": "Basic",
70
+ "pro": "Pro",
71
+ "enterprise": "Enterprise"
72
+ },
73
+ "payNow": "Pay Now",
74
+ "paymentSuccess": "Payment successful! Your plan has been upgraded.",
75
+ "paymentFailed": "Payment failed. Please try again."
76
+ },
77
+ "common": {
78
+ "save": "Save",
79
+ "cancel": "Cancel",
80
+ "delete": "Delete",
81
+ "edit": "Edit",
82
+ "confirm": "Confirm",
83
+ "close": "Close",
84
+ "back": "Back",
85
+ "next": "Next",
86
+ "loading": "Loading...",
87
+ "search": "Search",
88
+ "filter": "Filter",
89
+ "reset": "Reset",
90
+ "submit": "Submit",
91
+ "yes": "Yes",
92
+ "no": "No",
93
+ "or": "or",
94
+ "and": "and",
95
+ "error": "An error occurred",
96
+ "success": "Operation successful",
97
+ "noData": "No data available",
98
+ "required": "This field is required",
99
+ "darkMode": "Dark Mode",
100
+ "lightMode": "Light Mode"
101
+ },
102
+ "offline": {
103
+ "title": "You're Offline",
104
+ "message": "Data is read-only while you're offline. Changes will resume when you reconnect.",
105
+ "reconnected": "Back online! You can make changes again."
106
+ },
107
+ "errors": {
108
+ "404": "Page not found",
109
+ "403": "You don't have permission to view this page.",
110
+ "500": "Something went wrong on our end.",
111
+ "network": "Network error. Please check your connection.",
112
+ "unknown": "An unexpected error occurred."
113
+ },
114
+ "ai": {
115
+ "title": "AI Assistant",
116
+ "placeholder": "Ask me anything...",
117
+ "thinking": "Thinking...",
118
+ "clearChat": "Clear Chat",
119
+ "send": "Send"
120
+ }
121
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Falak App — main.js
3
+ * ─────────────────────
4
+ * Bootstraps Vue, PrimeVue, Pinia, Vue Router, and Vue I18n.
5
+ */
6
+
7
+ import { createApp } from 'vue'
8
+ import { createPinia } from 'pinia'
9
+
10
+ // PrimeVue
11
+ import PrimeVue from 'primevue/config'
12
+ import Aura from '@primevue/themes/aura'
13
+ import ToastService from 'primevue/toastservice'
14
+ import ConfirmationService from 'primevue/confirmationservice'
15
+ import DialogService from 'primevue/dialogservice'
16
+ import Tooltip from 'primevue/tooltip'
17
+ import 'primeicons/primeicons.css'
18
+
19
+ // Router & i18n
20
+ import router from '@/router/index.js'
21
+ import i18n from '@/i18n/index.js'
22
+
23
+ // Styles
24
+ import '@/assets/main.css'
25
+
26
+ // Root Component
27
+ import App from './App.vue'
28
+
29
+ const app = createApp(App)
30
+
31
+ // ── Pinia ──────────────────────────────────────
32
+ app.use(createPinia())
33
+
34
+ // ── PrimeVue ───────────────────────────────────
35
+ app.use(PrimeVue, {
36
+ theme: {
37
+ preset: Aura,
38
+ options: {
39
+ prefix: 'p',
40
+ darkModeSelector: '.dark',
41
+ cssLayer: false
42
+ }
43
+ },
44
+ ripple: true,
45
+ inputVariant: 'filled'
46
+ })
47
+ app.use(ToastService)
48
+ app.use(ConfirmationService)
49
+ app.use(DialogService)
50
+ app.directive('tooltip', Tooltip)
51
+
52
+ // ── Router ─────────────────────────────────────
53
+ app.use(router)
54
+
55
+ // ── i18n ───────────────────────────────────────
56
+ app.use(i18n)
57
+
58
+ // ── Mount ──────────────────────────────────────
59
+ app.mount('#app')
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Vue Router Configuration
3
+ * ─────────────────────────
4
+ * Routes are organized by feature.
5
+ * Auth guard is applied via meta.requiresAuth and meta.requiresRole.
6
+ */
7
+
8
+ import { createRouter, createWebHistory } from 'vue-router'
9
+ import { useAuth } from '@/composables/useAuth.js'
10
+
11
+ // ── Lazy-loaded route components ──────────────
12
+ const HomeView = () => import('@/views/HomeView.vue')
13
+ const LoginView = () => import('@/views/auth/LoginView.vue')
14
+ const RegisterView = () => import('@/views/auth/RegisterView.vue')
15
+ const ForgotView = () => import('@/views/auth/ForgotView.vue')
16
+ const DashboardView = () => import('@/views/DashboardView.vue')
17
+ const ProfileView = () => import('@/views/ProfileView.vue')
18
+ const AdminView = () => import('@/views/AdminView.vue')
19
+ const NotFoundView = () => import('@/views/NotFoundView.vue')
20
+
21
+ const routes = [
22
+ // ── Public ──────────────────────────────────
23
+ {
24
+ path: '/',
25
+ name: 'home',
26
+ component: HomeView,
27
+ meta: { title: 'Home' }
28
+ },
29
+ {
30
+ path: '/login',
31
+ name: 'login',
32
+ component: LoginView,
33
+ meta: { title: 'Sign In', guestOnly: true }
34
+ },
35
+ {
36
+ path: '/register',
37
+ name: 'register',
38
+ component: RegisterView,
39
+ meta: { title: 'Create Account', guestOnly: true }
40
+ },
41
+ {
42
+ path: '/forgot-password',
43
+ name: 'forgot-password',
44
+ component: ForgotView,
45
+ meta: { title: 'Reset Password', guestOnly: true }
46
+ },
47
+
48
+ // ── Authenticated ────────────────────────────
49
+ {
50
+ path: '/dashboard',
51
+ name: 'dashboard',
52
+ component: DashboardView,
53
+ meta: { title: 'Dashboard', requiresAuth: true }
54
+ },
55
+ {
56
+ path: '/profile',
57
+ name: 'profile',
58
+ component: ProfileView,
59
+ meta: { title: 'Profile', requiresAuth: true }
60
+ },
61
+
62
+ // ── Admin ────────────────────────────────────
63
+ {
64
+ path: '/admin',
65
+ name: 'admin',
66
+ component: AdminView,
67
+ meta: { title: 'Admin', requiresAuth: true, requiresRole: ['admin', 'super_admin'] }
68
+ },
69
+
70
+ // ── 404 ──────────────────────────────────────
71
+ {
72
+ path: '/:pathMatch(.*)*',
73
+ name: 'not-found',
74
+ component: NotFoundView,
75
+ meta: { title: '404 Not Found' }
76
+ }
77
+ ]
78
+
79
+ const router = createRouter({
80
+ history: createWebHistory(import.meta.env.BASE_URL),
81
+ routes,
82
+ scrollBehavior(to, from, savedPosition) {
83
+ if (savedPosition) return savedPosition
84
+ return { top: 0, behavior: 'smooth' }
85
+ }
86
+ })
87
+
88
+ // ── Navigation Guard ──────────────────────────
89
+ router.beforeEach(async (to, from, next) => {
90
+ const { isAuthenticated, isAuthReady, userRole } = useAuth()
91
+
92
+ // Wait for Firebase auth to initialize
93
+ if (!isAuthReady.value) {
94
+ await new Promise((resolve) => {
95
+ const { getAuth } = require('firebase/auth') // eslint-disable-line
96
+ // Actually handled via the composable's onAuthStateChanged
97
+ const stop = setInterval(() => {
98
+ if (isAuthReady.value) { clearInterval(stop); resolve() }
99
+ }, 50)
100
+ setTimeout(() => { clearInterval(stop); resolve() }, 3000)
101
+ })
102
+ }
103
+
104
+ // Set document title
105
+ document.title = to.meta.title
106
+ ? `${to.meta.title} — ${import.meta.env.VITE_APP_NAME || 'Falak App'}`
107
+ : import.meta.env.VITE_APP_NAME || 'Falak App'
108
+
109
+ // Guest-only routes (redirect logged-in users)
110
+ if (to.meta.guestOnly && isAuthenticated.value) {
111
+ return next({ name: 'dashboard' })
112
+ }
113
+
114
+ // Auth required
115
+ if (to.meta.requiresAuth && !isAuthenticated.value) {
116
+ return next({ name: 'login', query: { redirect: to.fullPath } })
117
+ }
118
+
119
+ // Role required
120
+ if (to.meta.requiresRole && !to.meta.requiresRole.includes(userRole.value)) {
121
+ return next({ name: 'dashboard' })
122
+ }
123
+
124
+ next()
125
+ })
126
+
127
+ export default router
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Auth Store (Pinia)
3
+ * ───────────────────
4
+ * Wraps the useAuth composable for global state access.
5
+ * Useful when you need auth state outside of components (e.g., in other stores).
6
+ */
7
+
8
+ import { defineStore } from 'pinia'
9
+ import { useAuth } from '@/composables/useAuth.js'
10
+
11
+ export const useAuthStore = defineStore('auth', () => {
12
+ const auth = useAuth()
13
+ return { ...auth }
14
+ })
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <AppLayout>
3
+ <div class="space-y-6">
4
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white">{{ $t('nav.admin') }}</h2>
5
+
6
+ <div class="card">
7
+ <h3 class="font-semibold mb-4 text-gray-900 dark:text-white">Users</h3>
8
+ <DataTable :value="userList" :loading="loading" stripedRows size="small" class="w-full">
9
+ <Column field="email" header="Email" />
10
+ <Column field="displayName" header="Name" />
11
+ <Column field="role" header="Role">
12
+ <template #body="{ data }">
13
+ <Select
14
+ :model-value="data.role"
15
+ :options="roleOptions"
16
+ optionLabel="label"
17
+ optionValue="value"
18
+ size="small"
19
+ @change="(e) => updateRole(data.uid, e.value)"
20
+ />
21
+ </template>
22
+ </Column>
23
+ <Column field="createdAt" header="Joined">
24
+ <template #body="{ data }">
25
+ {{ data.createdAt ? new Date(data.createdAt).toLocaleDateString() : '—' }}
26
+ </template>
27
+ </Column>
28
+ </DataTable>
29
+ </div>
30
+ </div>
31
+ </AppLayout>
32
+ </template>
33
+
34
+ <script setup>
35
+ import { ref, onUnmounted } from 'vue'
36
+ import { useToast } from 'primevue/usetoast'
37
+ import DataTable from 'primevue/datatable'
38
+ import Column from 'primevue/column'
39
+ import Select from 'primevue/select'
40
+ import AppLayout from '@/components/layout/AppLayout.vue'
41
+ import { useAuth, ROLES } from '@/composables/useAuth.js'
42
+ import { useDatabase } from '@/composables/useDatabase.js'
43
+
44
+ const toast = useToast()
45
+ const { setUserRole } = useAuth()
46
+ const db = useDatabase()
47
+
48
+ const loading = ref(true)
49
+ const userList = ref([])
50
+
51
+ const roleOptions = Object.values(ROLES).map(r => ({ label: r.replace('_', ' '), value: r }))
52
+
53
+ const { items, unsubscribe } = db.listenList('users')
54
+ onUnmounted(unsubscribe)
55
+
56
+ import { watch } from 'vue'
57
+ watch(items, (val) => {
58
+ userList.value = Object.values(val || {})
59
+ loading.value = false
60
+ }, { immediate: true })
61
+
62
+ async function updateRole(uid, role) {
63
+ const result = await setUserRole(uid, role)
64
+ if (result.success) toast.add({ severity: 'success', summary: 'Role updated', life: 2000 })
65
+ else toast.add({ severity: 'error', summary: result.error, life: 3000 })
66
+ }
67
+ </script>
@@ -0,0 +1,253 @@
1
+ <template>
2
+ <AppLayout>
3
+ <div class="space-y-6">
4
+ <!-- Welcome -->
5
+ <div class="flex items-center justify-between">
6
+ <div>
7
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white">
8
+ {{ $t('nav.dashboard') }} 👋
9
+ </h2>
10
+ <p class="text-sm text-gray-500">{{ currentUser?.displayName || currentUser?.email }}</p>
11
+ </div>
12
+ <span :class="subStatus === 'active' ? 'badge-success' : 'badge-warning'" class="badge">
13
+ {{ $t(`subscription.plans.${subPlan || 'free'}`) }}
14
+ </span>
15
+ </div>
16
+
17
+ <!-- Stats grid -->
18
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
19
+ <div v-for="stat in stats" :key="stat.label" class="card flex items-center gap-4">
20
+ <div :class="`w-12 h-12 rounded-xl ${stat.bg} flex items-center justify-center shrink-0`">
21
+ <i :class="`pi ${stat.icon} ${stat.color} text-xl`" />
22
+ </div>
23
+ <div>
24
+ <p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stat.value }}</p>
25
+ <p class="text-xs text-gray-500">{{ stat.label }}</p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Live data + AI assistant -->
31
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
32
+ <!-- Live feed from RTDB -->
33
+ <div class="card">
34
+ <div class="flex items-center justify-between mb-4">
35
+ <h3 class="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
36
+ <span class="inline-block w-2 h-2 rounded-full bg-green-500 animate-pulse" />
37
+ Live Feed
38
+ </h3>
39
+ <Button size="small" outlined @click="addTestEntry" :disabled="!isOnline">
40
+ <i class="pi pi-plus me-1" /> Add Entry
41
+ </Button>
42
+ </div>
43
+
44
+ <div v-if="!isOnline" class="badge-warning badge mb-3">
45
+ <i class="pi pi-wifi-off me-1 text-xs" /> Offline — read only
46
+ </div>
47
+
48
+ <div class="space-y-2 max-h-64 overflow-y-auto">
49
+ <div
50
+ v-for="(item, key) in feedItems"
51
+ :key="key"
52
+ class="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800 text-sm"
53
+ >
54
+ <i class="pi pi-circle-fill text-primary-500 text-xs mt-1" />
55
+ <div class="flex-1 min-w-0">
56
+ <p class="font-medium text-gray-800 dark:text-gray-200 truncate">{{ item.text }}</p>
57
+ <p class="text-xs text-gray-400">{{ formatTime(item.createdAt) }}</p>
58
+ </div>
59
+ </div>
60
+ <p v-if="!Object.keys(feedItems).length" class="text-sm text-gray-400 text-center py-4">
61
+ {{ $t('common.noData') }}
62
+ </p>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Groq AI chat -->
67
+ <div class="card flex flex-col h-80">
68
+ <h3 class="font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
69
+ <i class="pi pi-sparkles text-primary-500" />
70
+ {{ $t('ai.title') }}
71
+ </h3>
72
+
73
+ <div ref="chatEl" class="flex-1 overflow-y-auto space-y-2 mb-3 text-sm">
74
+ <div v-for="(msg, i) in chatHistory" :key="i"
75
+ :class="['rounded-lg px-3 py-2 max-w-[85%]',
76
+ msg.role === 'user'
77
+ ? 'ms-auto bg-primary-600 text-white'
78
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200']">
79
+ <span v-if="msg.role === 'assistant' && groq.isLoading.value && i === chatHistory.length - 1">
80
+ <i class="pi pi-spin pi-spinner me-1" />{{ $t('ai.thinking') }}
81
+ </span>
82
+ <span v-else>{{ msg.content }}</span>
83
+ </div>
84
+ <p v-if="!chatHistory.length" class="text-gray-400 text-xs text-center mt-4">
85
+ {{ $t('ai.placeholder') }}
86
+ </p>
87
+ </div>
88
+
89
+ <div class="flex gap-2">
90
+ <InputText
91
+ v-model="aiInput"
92
+ :placeholder="$t('ai.placeholder')"
93
+ class="flex-1 text-sm"
94
+ @keyup.enter="sendAI"
95
+ />
96
+ <Button size="small" @click="sendAI" :loading="groq.isLoading.value" icon="pi pi-send" />
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Subscription card -->
102
+ <div class="card border-primary-200 dark:border-primary-800">
103
+ <div class="flex items-center justify-between flex-wrap gap-4">
104
+ <div>
105
+ <h3 class="font-semibold text-gray-900 dark:text-white">{{ $t('subscription.title') }}</h3>
106
+ <p class="text-sm text-gray-500 mt-1">
107
+ {{ $t('subscription.currentPlan') }}:
108
+ <strong>{{ $t(`subscription.plans.${subPlan || 'free'}`) }}</strong>
109
+ <span v-if="subExpiry" class="ms-2">
110
+ · {{ $t('subscription.expiresOn') }} {{ new Date(subExpiry).toLocaleDateString() }}
111
+ </span>
112
+ </p>
113
+ </div>
114
+ <Button
115
+ v-if="subPlan !== 'enterprise'"
116
+ :label="$t('subscription.upgradePlan')"
117
+ icon="pi pi-arrow-up"
118
+ @click="upgradeVisible = true"
119
+ />
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Upgrade Dialog -->
125
+ <Dialog v-model:visible="upgradeVisible" :header="$t('subscription.upgradePlan')" modal class="w-full max-w-lg">
126
+ <div class="grid grid-cols-2 gap-4 py-4">
127
+ <div
128
+ v-for="(plan, key) in PLANS"
129
+ :key="key"
130
+ class="border rounded-xl p-4 cursor-pointer transition-all hover:border-primary-500"
131
+ :class="{ 'border-primary-500 bg-primary-50 dark:bg-primary-900/20': selectedPlan === key }"
132
+ @click="selectedPlan = key"
133
+ >
134
+ <p class="font-bold text-gray-900 dark:text-white">{{ plan.name }}</p>
135
+ <p class="text-lg font-bold text-primary-600 mt-1">
136
+ {{ plan.amountCents === 0 ? 'Free' : (plan.amountCents / 100).toFixed(2) + ' EGP' }}
137
+ </p>
138
+ <p class="text-xs text-gray-500 mt-2">{{ plan.features.join(', ') }}</p>
139
+ </div>
140
+ </div>
141
+ <template #footer>
142
+ <Button :label="$t('common.cancel')" text @click="upgradeVisible = false" />
143
+ <Button :label="$t('subscription.payNow')" :loading="payLoading" @click="handlePayment" />
144
+ </template>
145
+ </Dialog>
146
+ </AppLayout>
147
+ </template>
148
+
149
+ <script setup>
150
+ import { ref, computed, nextTick, onUnmounted } from 'vue'
151
+ import { useI18n } from 'vue-i18n'
152
+ import { useToast } from 'primevue/usetoast'
153
+ import InputText from 'primevue/inputtext'
154
+ import Button from 'primevue/button'
155
+ import Dialog from 'primevue/dialog'
156
+ import AppLayout from '@/components/layout/AppLayout.vue'
157
+ import { useAuth } from '@/composables/useAuth.js'
158
+ import { useDatabase } from '@/composables/useDatabase.js'
159
+ import { useGroq } from '@/composables/useGroq.js'
160
+ import { usePaymob, useSubscription, PLANS } from '@/composables/usePaymob.js'
161
+
162
+ const { t } = useI18n()
163
+ const toast = useToast()
164
+ const { currentUser } = useAuth()
165
+ const db = useDatabase()
166
+ const groq = useGroq('You are a helpful assistant embedded in Falak App. Keep answers concise.')
167
+ const paymob = usePaymob()
168
+ const sub = useSubscription()
169
+
170
+ const { isOnline } = db
171
+
172
+ // ── Live feed ──────────────────────────────────
173
+ const { items: feedItems, unsubscribe } = db.listenList('demo_feed')
174
+ onUnmounted(unsubscribe)
175
+
176
+ async function addTestEntry() {
177
+ await db.push('demo_feed', { text: `Entry at ${new Date().toLocaleTimeString()}` })
178
+ }
179
+
180
+ function formatTime(ts) {
181
+ if (!ts) return ''
182
+ return new Date(ts).toLocaleTimeString()
183
+ }
184
+
185
+ // ── Subscription ───────────────────────────────
186
+ const subData = sub.listenSubscription(currentUser.value?.uid || 'none')
187
+ const subPlan = computed(() => subData.data.value?.planId)
188
+ const subStatus = computed(() => subData.data.value?.status)
189
+ const subExpiry = computed(() => subData.data.value?.expiresAt)
190
+
191
+ // ── Stats ──────────────────────────────────────
192
+ const stats = [
193
+ { label: 'Plan', value: computed(() => subPlan.value || 'Free'), icon: 'pi-star', bg: 'bg-yellow-100 dark:bg-yellow-900/30', color: 'text-yellow-600' },
194
+ { label: 'Status', value: computed(() => subStatus.value || '—'), icon: 'pi-check-circle', bg: 'bg-green-100 dark:bg-green-900/30', color: 'text-green-600' },
195
+ { label: 'Feed Items', value: computed(() => Object.keys(feedItems.value).length), icon: 'pi-database', bg: 'bg-blue-100 dark:bg-blue-900/30', color: 'text-blue-600' },
196
+ { label: 'Network', value: computed(() => isOnline.value ? 'Online' : 'Offline'), icon: 'pi-wifi', bg: 'bg-purple-100 dark:bg-purple-900/30', color: 'text-purple-600' }
197
+ ]
198
+
199
+ // ── AI Chat ────────────────────────────────────
200
+ const aiInput = ref('')
201
+ const chatHistory = ref([])
202
+ const chatEl = ref(null)
203
+
204
+ async function sendAI() {
205
+ const msg = aiInput.value.trim()
206
+ if (!msg || groq.isLoading.value) return
207
+ aiInput.value = ''
208
+ chatHistory.value.push({ role: 'user', content: msg })
209
+ chatHistory.value.push({ role: 'assistant', content: '' })
210
+ await nextTick(() => chatEl.value?.scrollTo({ top: 99999, behavior: 'smooth' }))
211
+
212
+ let full = ''
213
+ await groq.streamChat(msg, (chunk) => {
214
+ full += chunk
215
+ chatHistory.value[chatHistory.value.length - 1].content = full
216
+ })
217
+ }
218
+
219
+ // ── Payment / Upgrade ──────────────────────────
220
+ const upgradeVisible = ref(false)
221
+ const selectedPlan = ref('PRO')
222
+ const payLoading = ref(false)
223
+
224
+ async function handlePayment() {
225
+ const plan = PLANS[selectedPlan.value]
226
+ if (!plan || plan.amountCents === 0) {
227
+ toast.add({ severity: 'info', summary: 'Free plan activated', life: 2000 })
228
+ await sub.activateSubscription(currentUser.value.uid, selectedPlan.value)
229
+ upgradeVisible.value = false
230
+ return
231
+ }
232
+ payLoading.value = true
233
+ const result = await paymob.openPaymentTab({
234
+ amountCents: plan.amountCents,
235
+ billingData: {
236
+ first_name: currentUser.value?.displayName?.split(' ')[0] || 'Customer',
237
+ last_name: currentUser.value?.displayName?.split(' ')[1] || 'User',
238
+ email: currentUser.value?.email || 'user@example.com',
239
+ phone_number: '+20xxxxxxxxxx',
240
+ apartment: 'NA', floor: 'NA', street: 'NA',
241
+ building: 'NA', city: 'Cairo', country: 'EG',
242
+ state: 'NA', postal_code: 'NA', shipping_method: 'NA'
243
+ }
244
+ })
245
+ payLoading.value = false
246
+ if (!result.success) {
247
+ toast.add({ severity: 'error', summary: t('subscription.paymentFailed'), life: 3000 })
248
+ } else {
249
+ toast.add({ severity: 'info', summary: 'Complete payment in the new tab', life: 4000 })
250
+ upgradeVisible.value = false
251
+ }
252
+ }
253
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div class="min-h-screen bg-white dark:bg-gray-950 flex flex-col items-center justify-center p-8 text-center">
3
+ <div class="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-primary-600 mb-6 shadow-lg">
4
+ <span class="text-white font-bold text-4xl">F</span>
5
+ </div>
6
+ <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-3">{{ $t('app.name') }}</h1>
7
+ <p class="text-gray-500 mb-8 max-w-md">{{ $t('app.tagline') }}</p>
8
+ <div class="flex gap-3">
9
+ <RouterLink to="/login" class="btn-secondary">{{ $t('nav.login') }}</RouterLink>
10
+ <RouterLink to="/register" class="btn-primary">{{ $t('nav.register') }}</RouterLink>
11
+ </div>
12
+ </div>
13
+ </template>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <div class="min-h-screen flex flex-col items-center justify-center p-8 text-center bg-white dark:bg-gray-950">
3
+ <p class="text-8xl font-black text-primary-100 dark:text-primary-900 mb-4">404</p>
4
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">{{ $t('errors.404') }}</h1>
5
+ <p class="text-gray-500 mb-8">The page you're looking for doesn't exist.</p>
6
+ <RouterLink to="/" class="btn-primary">← Back to Home</RouterLink>
7
+ </div>
8
+ </template>