@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,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppLayout>
|
|
3
|
+
<div class="max-w-2xl mx-auto space-y-6">
|
|
4
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white">{{ $t('user.profile') }}</h2>
|
|
5
|
+
|
|
6
|
+
<!-- Profile card -->
|
|
7
|
+
<div class="card space-y-6">
|
|
8
|
+
<!-- Avatar -->
|
|
9
|
+
<div class="flex items-center gap-4">
|
|
10
|
+
<Avatar
|
|
11
|
+
:image="currentUser?.photoURL || undefined"
|
|
12
|
+
:label="!currentUser?.photoURL ? initials : undefined"
|
|
13
|
+
shape="circle"
|
|
14
|
+
size="xlarge"
|
|
15
|
+
class="shrink-0"
|
|
16
|
+
/>
|
|
17
|
+
<div>
|
|
18
|
+
<p class="font-semibold text-gray-900 dark:text-white">{{ currentUser?.displayName || '—' }}</p>
|
|
19
|
+
<p class="text-sm text-gray-500">{{ currentUser?.email }}</p>
|
|
20
|
+
<span class="badge-info badge mt-1">{{ $t(`user.roles.${userRole}`) }}</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- Edit form -->
|
|
25
|
+
<form @submit.prevent="saveProfile" class="space-y-4">
|
|
26
|
+
<div>
|
|
27
|
+
<label class="label">{{ $t('auth.displayName') }}</label>
|
|
28
|
+
<InputText v-model="form.displayName" class="w-full" />
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<label class="label">{{ $t('auth.email') }}</label>
|
|
32
|
+
<InputText :value="currentUser?.email" disabled class="w-full opacity-60" />
|
|
33
|
+
</div>
|
|
34
|
+
<div class="flex justify-end">
|
|
35
|
+
<Button type="submit" :label="$t('common.save')" :loading="isLoading" />
|
|
36
|
+
</div>
|
|
37
|
+
</form>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Change password card -->
|
|
41
|
+
<div class="card space-y-4">
|
|
42
|
+
<h3 class="font-semibold text-gray-900 dark:text-white">{{ $t('user.changePassword') }}</h3>
|
|
43
|
+
|
|
44
|
+
<Message v-if="pwError" severity="error" :closable="false">{{ pwError }}</Message>
|
|
45
|
+
<Message v-if="pwSuccess" severity="success" :closable="false">Password updated!</Message>
|
|
46
|
+
|
|
47
|
+
<form @submit.prevent="savePassword" class="space-y-4">
|
|
48
|
+
<div>
|
|
49
|
+
<label class="label">Current Password</label>
|
|
50
|
+
<Password v-model="pw.current" :feedback="false" toggleMask class="w-full" inputClass="w-full" />
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<label class="label">New Password</label>
|
|
54
|
+
<Password v-model="pw.next" toggleMask class="w-full" inputClass="w-full" />
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex justify-end">
|
|
57
|
+
<Button type="submit" :label="$t('common.save')" :loading="isLoading" severity="secondary" />
|
|
58
|
+
</div>
|
|
59
|
+
</form>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Danger zone -->
|
|
63
|
+
<div class="card border-red-200 dark:border-red-900 space-y-3">
|
|
64
|
+
<h3 class="font-semibold text-red-600">Danger Zone</h3>
|
|
65
|
+
<p class="text-sm text-gray-500">{{ $t('user.deleteAccountConfirm') }}</p>
|
|
66
|
+
<Button
|
|
67
|
+
:label="$t('user.deleteAccount')"
|
|
68
|
+
severity="danger"
|
|
69
|
+
outlined
|
|
70
|
+
@click="confirmDelete"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</AppLayout>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script setup>
|
|
78
|
+
import { reactive, ref, computed } from 'vue'
|
|
79
|
+
import { useRouter } from 'vue-router'
|
|
80
|
+
import { useI18n } from 'vue-i18n'
|
|
81
|
+
import { useToast } from 'primevue/usetoast'
|
|
82
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
83
|
+
import InputText from 'primevue/inputtext'
|
|
84
|
+
import Password from 'primevue/password'
|
|
85
|
+
import Button from 'primevue/button'
|
|
86
|
+
import Avatar from 'primevue/avatar'
|
|
87
|
+
import Message from 'primevue/message'
|
|
88
|
+
import AppLayout from '@/components/layout/AppLayout.vue'
|
|
89
|
+
import { useAuth } from '@/composables/useAuth.js'
|
|
90
|
+
|
|
91
|
+
const { t } = useI18n()
|
|
92
|
+
const router = useRouter()
|
|
93
|
+
const toast = useToast()
|
|
94
|
+
const confirm = useConfirm()
|
|
95
|
+
const { currentUser, userRole, isLoading, updateUserProfile, changePassword, deleteAccount } = useAuth()
|
|
96
|
+
|
|
97
|
+
const form = reactive({ displayName: currentUser.value?.displayName || '' })
|
|
98
|
+
const pw = reactive({ current: '', next: '' })
|
|
99
|
+
const pwError = ref('')
|
|
100
|
+
const pwSuccess = ref(false)
|
|
101
|
+
|
|
102
|
+
const initials = computed(() => {
|
|
103
|
+
const n = currentUser.value?.displayName || currentUser.value?.email || '?'
|
|
104
|
+
return n.slice(0, 2).toUpperCase()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
async function saveProfile() {
|
|
108
|
+
const result = await updateUserProfile({ displayName: form.displayName })
|
|
109
|
+
if (result.success) toast.add({ severity: 'success', summary: t('common.success'), life: 2000 })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function savePassword() {
|
|
113
|
+
pwError.value = ''
|
|
114
|
+
pwSuccess.value = false
|
|
115
|
+
const result = await changePassword({ currentPassword: pw.current, newPassword: pw.next })
|
|
116
|
+
if (result.success) { pwSuccess.value = true; pw.current = ''; pw.next = '' }
|
|
117
|
+
else pwError.value = result.error
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function confirmDelete() {
|
|
121
|
+
confirm.require({
|
|
122
|
+
message: t('user.deleteAccountConfirm'),
|
|
123
|
+
header: t('user.deleteAccount'),
|
|
124
|
+
icon: 'pi pi-exclamation-triangle',
|
|
125
|
+
rejectLabel: t('common.cancel'),
|
|
126
|
+
acceptLabel: t('common.delete'),
|
|
127
|
+
acceptClass: 'p-button-danger',
|
|
128
|
+
accept: async () => {
|
|
129
|
+
await deleteAccount()
|
|
130
|
+
router.push('/')
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen bg-gradient-to-br from-primary-50 to-blue-100 dark:from-gray-950 dark:to-gray-900
|
|
3
|
+
flex items-center justify-center p-4">
|
|
4
|
+
<div class="w-full max-w-md">
|
|
5
|
+
<div class="card">
|
|
6
|
+
<div class="text-center mb-8">
|
|
7
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-primary-600 mb-4">
|
|
8
|
+
<i class="pi pi-lock-open text-white text-2xl" />
|
|
9
|
+
</div>
|
|
10
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ $t('auth.resetPassword') }}</h1>
|
|
11
|
+
<p class="text-sm text-gray-500 mt-1">{{ $t('auth.resetPasswordDesc') }}</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<Message v-if="sent" severity="success" :closable="false" class="mb-4">
|
|
15
|
+
✅ Reset link sent! Check your inbox.
|
|
16
|
+
</Message>
|
|
17
|
+
<Message v-if="authError" severity="error" :closable="false" class="mb-4">
|
|
18
|
+
{{ authError }}
|
|
19
|
+
</Message>
|
|
20
|
+
|
|
21
|
+
<form v-if="!sent" @submit.prevent="handleReset" class="space-y-4">
|
|
22
|
+
<div>
|
|
23
|
+
<label class="label">{{ $t('auth.email') }}</label>
|
|
24
|
+
<InputText v-model="email" type="email" class="w-full" :placeholder="$t('auth.email')" />
|
|
25
|
+
</div>
|
|
26
|
+
<Button type="submit" :label="$t('auth.resetPassword')" :loading="isLoading" class="w-full" />
|
|
27
|
+
</form>
|
|
28
|
+
|
|
29
|
+
<RouterLink to="/login" class="flex items-center justify-center gap-2 text-sm text-primary-600
|
|
30
|
+
hover:underline mt-6">
|
|
31
|
+
<i class="pi pi-arrow-left text-xs" />
|
|
32
|
+
{{ $t('auth.backToLogin') }}
|
|
33
|
+
</RouterLink>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
import { ref } from 'vue'
|
|
41
|
+
import { useI18n } from 'vue-i18n'
|
|
42
|
+
import InputText from 'primevue/inputtext'
|
|
43
|
+
import Button from 'primevue/button'
|
|
44
|
+
import Message from 'primevue/message'
|
|
45
|
+
import { useAuth } from '@/composables/useAuth.js'
|
|
46
|
+
|
|
47
|
+
const { t } = useI18n()
|
|
48
|
+
const { resetPassword, authError, isLoading } = useAuth()
|
|
49
|
+
const email = ref('')
|
|
50
|
+
const sent = ref(false)
|
|
51
|
+
|
|
52
|
+
async function handleReset() {
|
|
53
|
+
if (!email.value) return
|
|
54
|
+
const result = await resetPassword(email.value)
|
|
55
|
+
if (result.success) sent.value = true
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen bg-gradient-to-br from-primary-50 to-blue-100 dark:from-gray-950 dark:to-gray-900
|
|
3
|
+
flex items-center justify-center p-4">
|
|
4
|
+
<div class="w-full max-w-md">
|
|
5
|
+
<!-- Card -->
|
|
6
|
+
<div class="card">
|
|
7
|
+
<!-- Header -->
|
|
8
|
+
<div class="text-center mb-8">
|
|
9
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-primary-600 mb-4">
|
|
10
|
+
<span class="text-white font-bold text-2xl">F</span>
|
|
11
|
+
</div>
|
|
12
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ $t('nav.login') }}</h1>
|
|
13
|
+
<p class="text-sm text-gray-500 mt-1">{{ $t('app.tagline') }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Error alert -->
|
|
17
|
+
<Message v-if="authError" severity="error" :closable="false" class="mb-4">
|
|
18
|
+
{{ authError }}
|
|
19
|
+
</Message>
|
|
20
|
+
|
|
21
|
+
<!-- Email/Password Form -->
|
|
22
|
+
<form @submit.prevent="handleLogin" class="space-y-4">
|
|
23
|
+
<div>
|
|
24
|
+
<label class="label">{{ $t('auth.email') }}</label>
|
|
25
|
+
<InputText
|
|
26
|
+
v-model="form.email"
|
|
27
|
+
type="email"
|
|
28
|
+
:placeholder="$t('auth.email')"
|
|
29
|
+
class="w-full"
|
|
30
|
+
:invalid="!!errors.email"
|
|
31
|
+
autocomplete="email"
|
|
32
|
+
/>
|
|
33
|
+
<small v-if="errors.email" class="text-red-500 text-xs mt-1">{{ errors.email }}</small>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div>
|
|
37
|
+
<div class="flex items-center justify-between mb-1">
|
|
38
|
+
<label class="label !mb-0">{{ $t('auth.password') }}</label>
|
|
39
|
+
<RouterLink to="/forgot-password" class="text-xs text-primary-600 hover:underline">
|
|
40
|
+
{{ $t('auth.forgotPassword') }}
|
|
41
|
+
</RouterLink>
|
|
42
|
+
</div>
|
|
43
|
+
<Password
|
|
44
|
+
v-model="form.password"
|
|
45
|
+
:placeholder="$t('auth.password')"
|
|
46
|
+
:feedback="false"
|
|
47
|
+
toggleMask
|
|
48
|
+
class="w-full"
|
|
49
|
+
inputClass="w-full"
|
|
50
|
+
:invalid="!!errors.password"
|
|
51
|
+
autocomplete="current-password"
|
|
52
|
+
/>
|
|
53
|
+
<small v-if="errors.password" class="text-red-500 text-xs mt-1">{{ errors.password }}</small>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<Checkbox v-model="form.remember" inputId="remember" binary />
|
|
58
|
+
<label for="remember" class="text-sm text-gray-600 dark:text-gray-400 cursor-pointer">
|
|
59
|
+
{{ $t('auth.rememberMe') }}
|
|
60
|
+
</label>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<Button
|
|
64
|
+
type="submit"
|
|
65
|
+
:label="$t('nav.login')"
|
|
66
|
+
:loading="isLoading"
|
|
67
|
+
class="w-full"
|
|
68
|
+
/>
|
|
69
|
+
</form>
|
|
70
|
+
|
|
71
|
+
<!-- Divider -->
|
|
72
|
+
<div class="flex items-center gap-3 my-6">
|
|
73
|
+
<div class="flex-1 border-t" />
|
|
74
|
+
<span class="text-xs text-gray-400">{{ $t('auth.orContinueWith') }}</span>
|
|
75
|
+
<div class="flex-1 border-t" />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- OAuth Buttons -->
|
|
79
|
+
<div class="grid grid-cols-2 gap-3">
|
|
80
|
+
<Button
|
|
81
|
+
@click="handleGoogle"
|
|
82
|
+
:loading="isLoading"
|
|
83
|
+
outlined
|
|
84
|
+
class="w-full"
|
|
85
|
+
>
|
|
86
|
+
<template #icon>
|
|
87
|
+
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
|
88
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
89
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
90
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
91
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
92
|
+
</svg>
|
|
93
|
+
</template>
|
|
94
|
+
Google
|
|
95
|
+
</Button>
|
|
96
|
+
|
|
97
|
+
<Button
|
|
98
|
+
@click="handleFacebook"
|
|
99
|
+
:loading="isLoading"
|
|
100
|
+
outlined
|
|
101
|
+
class="w-full"
|
|
102
|
+
>
|
|
103
|
+
<template #icon>
|
|
104
|
+
<svg class="w-4 h-4" fill="#1877F2" viewBox="0 0 24 24">
|
|
105
|
+
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
|
106
|
+
</svg>
|
|
107
|
+
</template>
|
|
108
|
+
Facebook
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Register link -->
|
|
113
|
+
<p class="text-center text-sm text-gray-600 dark:text-gray-400 mt-6">
|
|
114
|
+
{{ $t('auth.noAccount') }}
|
|
115
|
+
<RouterLink to="/register" class="text-primary-600 font-medium hover:underline ms-1">
|
|
116
|
+
{{ $t('nav.register') }}
|
|
117
|
+
</RouterLink>
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<script setup>
|
|
125
|
+
import { reactive, ref } from 'vue'
|
|
126
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
127
|
+
import { useI18n } from 'vue-i18n'
|
|
128
|
+
import { useToast } from 'primevue/usetoast'
|
|
129
|
+
import InputText from 'primevue/inputtext'
|
|
130
|
+
import Password from 'primevue/password'
|
|
131
|
+
import Button from 'primevue/button'
|
|
132
|
+
import Checkbox from 'primevue/checkbox'
|
|
133
|
+
import Message from 'primevue/message'
|
|
134
|
+
import { useAuth } from '@/composables/useAuth.js'
|
|
135
|
+
|
|
136
|
+
const { t } = useI18n()
|
|
137
|
+
const router = useRouter()
|
|
138
|
+
const route = useRoute()
|
|
139
|
+
const toast = useToast()
|
|
140
|
+
const { login, loginWithGoogle, loginWithFacebook, authError, isLoading } = useAuth()
|
|
141
|
+
|
|
142
|
+
const form = reactive({ email: '', password: '', remember: false })
|
|
143
|
+
const errors = reactive({ email: '', password: '' })
|
|
144
|
+
|
|
145
|
+
function validate() {
|
|
146
|
+
errors.email = !form.email ? t('common.required') : ''
|
|
147
|
+
errors.password = !form.password ? t('common.required') : ''
|
|
148
|
+
return !errors.email && !errors.password
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function handleLogin() {
|
|
152
|
+
if (!validate()) return
|
|
153
|
+
const result = await login({ email: form.email, password: form.password })
|
|
154
|
+
if (result.success) {
|
|
155
|
+
toast.add({ severity: 'success', summary: t('auth.loginSuccess'), life: 2000 })
|
|
156
|
+
router.push(route.query.redirect || '/dashboard')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handleGoogle() {
|
|
161
|
+
const result = await loginWithGoogle()
|
|
162
|
+
if (result.success) router.push(route.query.redirect || '/dashboard')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function handleFacebook() {
|
|
166
|
+
const result = await loginWithFacebook()
|
|
167
|
+
if (result.success) router.push(route.query.redirect || '/dashboard')
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen bg-gradient-to-br from-primary-50 to-blue-100 dark:from-gray-950 dark:to-gray-900
|
|
3
|
+
flex items-center justify-center p-4">
|
|
4
|
+
<div class="w-full max-w-md">
|
|
5
|
+
<div class="card">
|
|
6
|
+
<!-- Header -->
|
|
7
|
+
<div class="text-center mb-8">
|
|
8
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-primary-600 mb-4">
|
|
9
|
+
<span class="text-white font-bold text-2xl">F</span>
|
|
10
|
+
</div>
|
|
11
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ $t('nav.register') }}</h1>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<Message v-if="authError" severity="error" :closable="false" class="mb-4">
|
|
15
|
+
{{ authError }}
|
|
16
|
+
</Message>
|
|
17
|
+
|
|
18
|
+
<form @submit.prevent="handleRegister" class="space-y-4">
|
|
19
|
+
<div>
|
|
20
|
+
<label class="label">{{ $t('auth.displayName') }}</label>
|
|
21
|
+
<InputText v-model="form.displayName" type="text" class="w-full"
|
|
22
|
+
:placeholder="$t('auth.displayName')" :invalid="!!errors.displayName" />
|
|
23
|
+
<small v-if="errors.displayName" class="text-red-500 text-xs">{{ errors.displayName }}</small>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<label class="label">{{ $t('auth.email') }}</label>
|
|
28
|
+
<InputText v-model="form.email" type="email" class="w-full"
|
|
29
|
+
:placeholder="$t('auth.email')" :invalid="!!errors.email" />
|
|
30
|
+
<small v-if="errors.email" class="text-red-500 text-xs">{{ errors.email }}</small>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div>
|
|
34
|
+
<label class="label">{{ $t('auth.password') }}</label>
|
|
35
|
+
<Password v-model="form.password" toggleMask class="w-full" inputClass="w-full"
|
|
36
|
+
:placeholder="$t('auth.password')" :invalid="!!errors.password" />
|
|
37
|
+
<small v-if="errors.password" class="text-red-500 text-xs">{{ errors.password }}</small>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div>
|
|
41
|
+
<label class="label">{{ $t('auth.confirmPassword') }}</label>
|
|
42
|
+
<Password v-model="form.confirm" :feedback="false" toggleMask class="w-full" inputClass="w-full"
|
|
43
|
+
:placeholder="$t('auth.confirmPassword')" :invalid="!!errors.confirm" />
|
|
44
|
+
<small v-if="errors.confirm" class="text-red-500 text-xs">{{ errors.confirm }}</small>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<Button type="submit" :label="$t('nav.register')" :loading="isLoading" class="w-full" />
|
|
48
|
+
</form>
|
|
49
|
+
|
|
50
|
+
<p class="text-center text-sm text-gray-600 dark:text-gray-400 mt-6">
|
|
51
|
+
{{ $t('auth.hasAccount') }}
|
|
52
|
+
<RouterLink to="/login" class="text-primary-600 font-medium hover:underline ms-1">
|
|
53
|
+
{{ $t('nav.login') }}
|
|
54
|
+
</RouterLink>
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup>
|
|
62
|
+
import { reactive } from 'vue'
|
|
63
|
+
import { useRouter } from 'vue-router'
|
|
64
|
+
import { useI18n } from 'vue-i18n'
|
|
65
|
+
import { useToast } from 'primevue/usetoast'
|
|
66
|
+
import InputText from 'primevue/inputtext'
|
|
67
|
+
import Password from 'primevue/password'
|
|
68
|
+
import Button from 'primevue/button'
|
|
69
|
+
import Message from 'primevue/message'
|
|
70
|
+
import { useAuth } from '@/composables/useAuth.js'
|
|
71
|
+
import { useSubscription } from '@/composables/usePaymob.js'
|
|
72
|
+
|
|
73
|
+
const { t } = useI18n()
|
|
74
|
+
const router = useRouter()
|
|
75
|
+
const toast = useToast()
|
|
76
|
+
const { register, authError, isLoading } = useAuth()
|
|
77
|
+
const { initFreeplan } = useSubscription()
|
|
78
|
+
|
|
79
|
+
const form = reactive({ displayName: '', email: '', password: '', confirm: '' })
|
|
80
|
+
const errors = reactive({ displayName: '', email: '', password: '', confirm: '' })
|
|
81
|
+
|
|
82
|
+
function validate() {
|
|
83
|
+
errors.displayName = !form.displayName ? t('common.required') : ''
|
|
84
|
+
errors.email = !form.email ? t('common.required') : ''
|
|
85
|
+
errors.password = form.password.length < 8 ? t('auth.weakPassword') : ''
|
|
86
|
+
errors.confirm = form.password !== form.confirm ? t('auth.passwordMismatch') : ''
|
|
87
|
+
return !Object.values(errors).some(Boolean)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleRegister() {
|
|
91
|
+
if (!validate()) return
|
|
92
|
+
const result = await register({
|
|
93
|
+
email: form.email,
|
|
94
|
+
password: form.password,
|
|
95
|
+
displayName: form.displayName
|
|
96
|
+
})
|
|
97
|
+
if (result.success) {
|
|
98
|
+
await initFreeplan(result.user.uid)
|
|
99
|
+
toast.add({ severity: 'success', summary: t('auth.registerSuccess'), life: 4000 })
|
|
100
|
+
router.push('/dashboard')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
'./index.html',
|
|
5
|
+
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
|
6
|
+
'./node_modules/primevue/**/*.{vue,js,ts}'
|
|
7
|
+
],
|
|
8
|
+
darkMode: 'class',
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {
|
|
11
|
+
colors: {
|
|
12
|
+
primary: {
|
|
13
|
+
50: '#eff6ff',
|
|
14
|
+
100: '#dbeafe',
|
|
15
|
+
200: '#bfdbfe',
|
|
16
|
+
300: '#93c5fd',
|
|
17
|
+
400: '#60a5fa',
|
|
18
|
+
500: '#3b82f6',
|
|
19
|
+
600: '#2563eb',
|
|
20
|
+
700: '#1d4ed8',
|
|
21
|
+
800: '#1e40af',
|
|
22
|
+
900: '#1e3a8a',
|
|
23
|
+
950: '#172554'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
fontFamily: {
|
|
27
|
+
sans: ['Inter', 'ui-sans-serif', 'system-ui'],
|
|
28
|
+
arabic: ['Cairo', 'ui-sans-serif', 'system-ui']
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
plugins: [
|
|
33
|
+
// Uncomment if needed:
|
|
34
|
+
// require('@tailwindcss/forms'),
|
|
35
|
+
// require('@tailwindcss/typography'),
|
|
36
|
+
],
|
|
37
|
+
// Prevent Tailwind from conflicting with PrimeVue
|
|
38
|
+
corePlugins: {
|
|
39
|
+
preflight: true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'@': path.resolve(__dirname, './src')
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
server: {
|
|
13
|
+
port: 5173,
|
|
14
|
+
host: true
|
|
15
|
+
},
|
|
16
|
+
build: {
|
|
17
|
+
outDir: 'dist',
|
|
18
|
+
sourcemap: false,
|
|
19
|
+
rollupOptions: {
|
|
20
|
+
output: {
|
|
21
|
+
manualChunks: {
|
|
22
|
+
'firebase': ['firebase/app', 'firebase/auth', 'firebase/database'],
|
|
23
|
+
'primevue': ['primevue'],
|
|
24
|
+
'vendor': ['vue', 'vue-router', 'pinia', 'vue-i18n']
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|