@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="ltr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="theme-color" content="#2563eb" />
|
|
8
|
+
<title>__APP_NAME__</title>
|
|
9
|
+
<!-- Google Fonts: Inter + Cairo (Arabic) -->
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
|
+
<link
|
|
13
|
+
href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap"
|
|
14
|
+
rel="stylesheet"
|
|
15
|
+
/>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div id="app"></div>
|
|
19
|
+
<script type="module" src="/src/main.js"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "falak-project",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "eslint . --fix"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@primevue/themes": "^4.2.0",
|
|
14
|
+
"axios": "^1.7.4",
|
|
15
|
+
"crypto-js": "^4.2.0",
|
|
16
|
+
"firebase": "^11.0.0",
|
|
17
|
+
"groq-sdk": "^0.7.0",
|
|
18
|
+
"idb": "^8.0.0",
|
|
19
|
+
"pinia": "^2.2.0",
|
|
20
|
+
"primevue": "^4.2.0",
|
|
21
|
+
"vue": "^3.5.0",
|
|
22
|
+
"vue-i18n": "^10.0.0",
|
|
23
|
+
"vue-router": "^4.4.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@tailwindcss/forms": "^0.5.9",
|
|
27
|
+
"@tailwindcss/typography": "^0.5.15",
|
|
28
|
+
"@vitejs/plugin-vue": "^5.1.0",
|
|
29
|
+
"autoprefixer": "^10.4.20",
|
|
30
|
+
"eslint": "^9.0.0",
|
|
31
|
+
"eslint-plugin-vue": "^9.28.0",
|
|
32
|
+
"postcss": "^8.4.47",
|
|
33
|
+
"tailwindcss": "^3.4.14",
|
|
34
|
+
"vite": "^5.4.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="{ dark: isDark }" :dir="locale === 'ar' ? 'rtl' : 'ltr'">
|
|
3
|
+
<!-- Offline Banner -->
|
|
4
|
+
<Transition name="slide-down">
|
|
5
|
+
<div
|
|
6
|
+
v-if="!isOnline"
|
|
7
|
+
class="fixed top-0 inset-x-0 z-50 bg-amber-500 text-white text-sm font-medium
|
|
8
|
+
text-center py-2 px-4 flex items-center justify-center gap-2"
|
|
9
|
+
>
|
|
10
|
+
<i class="pi pi-wifi text-base" />
|
|
11
|
+
{{ $t('offline.message') }}
|
|
12
|
+
</div>
|
|
13
|
+
</Transition>
|
|
14
|
+
|
|
15
|
+
<!-- Router View -->
|
|
16
|
+
<RouterView v-slot="{ Component, route }">
|
|
17
|
+
<Transition :name="route.meta.transition || 'fade'" mode="out-in">
|
|
18
|
+
<component :is="Component" :key="route.path" />
|
|
19
|
+
</Transition>
|
|
20
|
+
</RouterView>
|
|
21
|
+
|
|
22
|
+
<!-- Global Toast -->
|
|
23
|
+
<Toast position="top-right" />
|
|
24
|
+
|
|
25
|
+
<!-- Global Confirm Dialog -->
|
|
26
|
+
<ConfirmDialog />
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import { ref, watch, onMounted } from 'vue'
|
|
32
|
+
import { useI18n } from 'vue-i18n'
|
|
33
|
+
import { RouterView } from 'vue-router'
|
|
34
|
+
import Toast from 'primevue/toast'
|
|
35
|
+
import ConfirmDialog from 'primevue/confirmdialog'
|
|
36
|
+
import { useDatabase } from '@/composables/useDatabase.js'
|
|
37
|
+
|
|
38
|
+
const { locale } = useI18n()
|
|
39
|
+
const db = useDatabase()
|
|
40
|
+
const { isOnline } = db
|
|
41
|
+
|
|
42
|
+
// ── Dark mode ──────────────────────────────────
|
|
43
|
+
const isDark = ref(
|
|
44
|
+
localStorage.getItem('falak_theme') === 'dark' ||
|
|
45
|
+
(!localStorage.getItem('falak_theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Expose toggle for child components via provide
|
|
49
|
+
import { provide } from 'vue'
|
|
50
|
+
provide('isDark', isDark)
|
|
51
|
+
provide('toggleDark', () => {
|
|
52
|
+
isDark.value = !isDark.value
|
|
53
|
+
localStorage.setItem('falak_theme', isDark.value ? 'dark' : 'light')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ── RTL sync ───────────────────────────────────
|
|
57
|
+
watch(locale, (val) => {
|
|
58
|
+
document.documentElement.lang = val
|
|
59
|
+
document.documentElement.dir = val === 'ar' ? 'rtl' : 'ltr'
|
|
60
|
+
}, { immediate: true })
|
|
61
|
+
|
|
62
|
+
onMounted(() => {
|
|
63
|
+
document.documentElement.lang = locale.value
|
|
64
|
+
document.documentElement.dir = locale.value === 'ar' ? 'rtl' : 'ltr'
|
|
65
|
+
})
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
/* ── Page Transitions ─────────────────────────── */
|
|
70
|
+
.fade-enter-active,
|
|
71
|
+
.fade-leave-active {
|
|
72
|
+
transition: opacity 0.2s ease;
|
|
73
|
+
}
|
|
74
|
+
.fade-enter-from,
|
|
75
|
+
.fade-leave-to {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.slide-down-enter-active,
|
|
80
|
+
.slide-down-leave-active {
|
|
81
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
82
|
+
}
|
|
83
|
+
.slide-down-enter-from,
|
|
84
|
+
.slide-down-leave-to {
|
|
85
|
+
transform: translateY(-100%);
|
|
86
|
+
opacity: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.slide-enter-active,
|
|
90
|
+
.slide-leave-active {
|
|
91
|
+
transition: transform 0.25s ease;
|
|
92
|
+
}
|
|
93
|
+
.slide-enter-from { transform: translateX(20px); opacity: 0; }
|
|
94
|
+
.slide-leave-to { transform: translateX(-20px); opacity: 0; }
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* ── Tailwind Base Layers ─────────────────────── */
|
|
2
|
+
@tailwind base;
|
|
3
|
+
@tailwind components;
|
|
4
|
+
@tailwind utilities;
|
|
5
|
+
|
|
6
|
+
/* ── PrimeVue Icons ───────────────────────────── */
|
|
7
|
+
@import 'primeicons/primeicons.css';
|
|
8
|
+
|
|
9
|
+
/* ── Base Styles ──────────────────────────────── */
|
|
10
|
+
@layer base {
|
|
11
|
+
:root {
|
|
12
|
+
--font-sans: 'Inter', ui-sans-serif, system-ui;
|
|
13
|
+
--font-arabic: 'Cairo', ui-sans-serif, system-ui;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html {
|
|
17
|
+
font-family: var(--font-sans);
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
-moz-osx-font-smoothing: grayscale;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html[dir='rtl'],
|
|
23
|
+
html[lang='ar'] {
|
|
24
|
+
font-family: var(--font-arabic);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
@apply bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
* {
|
|
32
|
+
@apply border-gray-200 dark:border-gray-800;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Smooth scrolling */
|
|
36
|
+
html {
|
|
37
|
+
scroll-behavior: smooth;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── Utility Classes ──────────────────────────── */
|
|
42
|
+
@layer components {
|
|
43
|
+
.btn {
|
|
44
|
+
@apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg
|
|
45
|
+
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
|
|
46
|
+
disabled:opacity-50 disabled:cursor-not-allowed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.btn-primary {
|
|
50
|
+
@apply btn bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.btn-secondary {
|
|
54
|
+
@apply btn bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400
|
|
55
|
+
dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.btn-danger {
|
|
59
|
+
@apply btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.card {
|
|
63
|
+
@apply bg-white dark:bg-gray-900 rounded-xl border shadow-sm p-6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.input {
|
|
67
|
+
@apply w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900
|
|
68
|
+
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent
|
|
69
|
+
placeholder:text-gray-400 dark:placeholder:text-gray-600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.label {
|
|
73
|
+
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.badge {
|
|
77
|
+
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.badge-success {
|
|
81
|
+
@apply badge bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.badge-warning {
|
|
85
|
+
@apply badge bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.badge-danger {
|
|
89
|
+
@apply badge bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.badge-info {
|
|
93
|
+
@apply badge bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* RTL-aware flex direction */
|
|
97
|
+
[dir='rtl'] .rtl-reverse {
|
|
98
|
+
@apply flex-row-reverse;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen bg-gray-50 dark:bg-gray-950 flex">
|
|
3
|
+
<!-- Sidebar -->
|
|
4
|
+
<aside
|
|
5
|
+
:class="[
|
|
6
|
+
'fixed inset-y-0 start-0 z-40 flex flex-col bg-white dark:bg-gray-900 border-e shadow-sm',
|
|
7
|
+
'transition-all duration-300',
|
|
8
|
+
sidebarOpen ? 'w-64' : 'w-16'
|
|
9
|
+
]"
|
|
10
|
+
>
|
|
11
|
+
<!-- Logo -->
|
|
12
|
+
<div class="flex items-center h-16 px-4 border-b gap-3">
|
|
13
|
+
<div class="w-8 h-8 rounded-lg bg-primary-600 flex items-center justify-center shrink-0">
|
|
14
|
+
<span class="text-white font-bold text-sm">F</span>
|
|
15
|
+
</div>
|
|
16
|
+
<Transition name="fade">
|
|
17
|
+
<span v-if="sidebarOpen" class="font-bold text-gray-900 dark:text-white truncate">
|
|
18
|
+
{{ $t('app.name') }}
|
|
19
|
+
</span>
|
|
20
|
+
</Transition>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Nav -->
|
|
24
|
+
<nav class="flex-1 py-4 overflow-y-auto">
|
|
25
|
+
<ul class="space-y-1 px-2">
|
|
26
|
+
<li v-for="item in navItems" :key="item.name">
|
|
27
|
+
<RouterLink
|
|
28
|
+
:to="item.to"
|
|
29
|
+
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium
|
|
30
|
+
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800
|
|
31
|
+
hover:text-gray-900 dark:hover:text-white transition-colors group"
|
|
32
|
+
active-class="!bg-primary-50 !text-primary-700 dark:!bg-primary-900/20 dark:!text-primary-400"
|
|
33
|
+
v-tooltip.right="!sidebarOpen ? $t(item.label) : undefined"
|
|
34
|
+
>
|
|
35
|
+
<i :class="`pi ${item.icon} text-base`" />
|
|
36
|
+
<Transition name="fade">
|
|
37
|
+
<span v-if="sidebarOpen">{{ $t(item.label) }}</span>
|
|
38
|
+
</Transition>
|
|
39
|
+
</RouterLink>
|
|
40
|
+
</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</nav>
|
|
43
|
+
|
|
44
|
+
<!-- User / Logout -->
|
|
45
|
+
<div class="border-t p-3">
|
|
46
|
+
<button
|
|
47
|
+
@click="handleLogout"
|
|
48
|
+
class="flex items-center gap-3 w-full px-3 py-2 rounded-lg text-sm font-medium
|
|
49
|
+
text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
50
|
+
v-tooltip.right="!sidebarOpen ? $t('nav.logout') : undefined"
|
|
51
|
+
>
|
|
52
|
+
<i class="pi pi-sign-out text-base" />
|
|
53
|
+
<Transition name="fade">
|
|
54
|
+
<span v-if="sidebarOpen">{{ $t('nav.logout') }}</span>
|
|
55
|
+
</Transition>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</aside>
|
|
59
|
+
|
|
60
|
+
<!-- Main content -->
|
|
61
|
+
<div :class="['flex-1 flex flex-col transition-all duration-300', sidebarOpen ? 'ms-64' : 'ms-16']">
|
|
62
|
+
<!-- Topbar -->
|
|
63
|
+
<header class="sticky top-0 z-30 h-16 bg-white dark:bg-gray-900 border-b flex items-center px-4 gap-4">
|
|
64
|
+
<!-- Toggle sidebar -->
|
|
65
|
+
<button
|
|
66
|
+
@click="sidebarOpen = !sidebarOpen"
|
|
67
|
+
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
|
|
68
|
+
>
|
|
69
|
+
<i :class="`pi ${sidebarOpen ? 'pi-align-left' : 'pi-bars'}`" />
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
<!-- Page title -->
|
|
73
|
+
<h1 class="text-base font-semibold text-gray-900 dark:text-white flex-1">
|
|
74
|
+
{{ pageTitle }}
|
|
75
|
+
</h1>
|
|
76
|
+
|
|
77
|
+
<!-- Actions -->
|
|
78
|
+
<div class="flex items-center gap-2">
|
|
79
|
+
<!-- Language toggle -->
|
|
80
|
+
<button
|
|
81
|
+
@click="toggleLocale"
|
|
82
|
+
class="px-3 py-1.5 text-xs font-semibold rounded-lg border hover:bg-gray-50 dark:hover:bg-gray-800
|
|
83
|
+
text-gray-600 dark:text-gray-400 transition-colors"
|
|
84
|
+
>
|
|
85
|
+
{{ locale === 'ar' ? 'EN' : 'عربي' }}
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
<!-- Dark mode -->
|
|
89
|
+
<button
|
|
90
|
+
@click="toggleDark()"
|
|
91
|
+
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
|
|
92
|
+
>
|
|
93
|
+
<i :class="`pi ${isDark ? 'pi-sun' : 'pi-moon'}`" />
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
<!-- User avatar -->
|
|
97
|
+
<div v-if="currentUser" class="flex items-center gap-2">
|
|
98
|
+
<Avatar
|
|
99
|
+
:image="currentUser.photoURL || undefined"
|
|
100
|
+
:label="!currentUser.photoURL ? userInitials : undefined"
|
|
101
|
+
shape="circle"
|
|
102
|
+
class="cursor-pointer"
|
|
103
|
+
size="normal"
|
|
104
|
+
@click="$router.push('/profile')"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</header>
|
|
109
|
+
|
|
110
|
+
<!-- Page -->
|
|
111
|
+
<main class="flex-1 p-6">
|
|
112
|
+
<slot />
|
|
113
|
+
</main>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script setup>
|
|
119
|
+
import { ref, computed, inject } from 'vue'
|
|
120
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
121
|
+
import { useI18n } from 'vue-i18n'
|
|
122
|
+
import { useToast } from 'primevue/usetoast'
|
|
123
|
+
import Avatar from 'primevue/avatar'
|
|
124
|
+
import { useAuth } from '@/composables/useAuth.js'
|
|
125
|
+
import { setLocale } from '@/i18n/index.js'
|
|
126
|
+
|
|
127
|
+
const { locale, t } = useI18n()
|
|
128
|
+
const router = useRouter()
|
|
129
|
+
const route = useRoute()
|
|
130
|
+
const toast = useToast()
|
|
131
|
+
const { currentUser, logout } = useAuth()
|
|
132
|
+
const isDark = inject('isDark')
|
|
133
|
+
const toggleDark = inject('toggleDark')
|
|
134
|
+
|
|
135
|
+
const sidebarOpen = ref(true)
|
|
136
|
+
|
|
137
|
+
const navItems = [
|
|
138
|
+
{ name: 'dashboard', to: '/dashboard', icon: 'pi-home', label: 'nav.dashboard' },
|
|
139
|
+
{ name: 'profile', to: '/profile', icon: 'pi-user', label: 'nav.profile' },
|
|
140
|
+
{ name: 'admin', to: '/admin', icon: 'pi-cog', label: 'nav.admin' }
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
const pageTitle = computed(() => route.meta.title ? t(`nav.${route.name}`, route.meta.title) : t('app.name'))
|
|
144
|
+
const userInitials = computed(() => {
|
|
145
|
+
const name = currentUser.value?.displayName || currentUser.value?.email || '?'
|
|
146
|
+
return name.slice(0, 2).toUpperCase()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
function toggleLocale() {
|
|
150
|
+
setLocale(locale.value === 'ar' ? 'en' : 'ar')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleLogout() {
|
|
154
|
+
await logout()
|
|
155
|
+
toast.add({ severity: 'success', summary: t('auth.logoutSuccess'), life: 2000 })
|
|
156
|
+
router.push('/login')
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<style scoped>
|
|
161
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
|
|
162
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
163
|
+
</style>
|