cyber-elx 1.0.6 → 1.1.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/DEV_DOC/LoginRegisterPagesDev.md +658 -0
- package/DEV_DOC/PaymentPageDev.md +576 -0
- package/DEV_DOC/ThemeDev.md +3 -2
- package/README.md +9 -4
- package/package.json +3 -2
- package/src/api.js +52 -0
- package/src/cache.js +20 -4
- package/src/files.js +155 -2
- package/src/index.js +174 -25
- package/src/vue-utils.js +149 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# CyberOcean Custom Login & Register Pages
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
- [CyberOcean Custom Login \& Register Pages](#cyberocean-custom-login--register-pages)
|
|
6
|
+
- [Summary](#summary)
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Login Page](#login-page)
|
|
9
|
+
- [Component Structure](#component-structure)
|
|
10
|
+
- [Available Props](#available-props)
|
|
11
|
+
- [Login Flow Steps](#login-flow-steps)
|
|
12
|
+
- [Events to Emit](#events-to-emit)
|
|
13
|
+
- [Example Login Page:](#example-login-page)
|
|
14
|
+
- [Register Page](#register-page)
|
|
15
|
+
- [Component Structure](#component-structure-1)
|
|
16
|
+
- [Available Props](#available-props-1)
|
|
17
|
+
- [inputsData Object](#inputsdata-object)
|
|
18
|
+
- [Register Flow States](#register-flow-states)
|
|
19
|
+
- [Events to Emit](#events-to-emit-1)
|
|
20
|
+
- [Example Register Page:](#example-register-page)
|
|
21
|
+
- [Vue Component Format](#vue-component-format)
|
|
22
|
+
- [Basic Structure](#basic-structure)
|
|
23
|
+
- [Available Fields](#available-fields)
|
|
24
|
+
- [Key Rules](#key-rules)
|
|
25
|
+
- [Minimal Example](#minimal-example)
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
The Login and Register pages are Vue.js components that handle user authentication. These pages are written as Vue.js component modules with a specific structure.
|
|
30
|
+
IMPORTANT: You will only handle UI and styles, all logic and functionality is handled by the parent component.
|
|
31
|
+
|
|
32
|
+
**Key Notes for Development:**
|
|
33
|
+
- Component-based architecture with props and events
|
|
34
|
+
- Multi-step form flows managed via the `step` prop
|
|
35
|
+
- Built-in translation support via `$t()` function
|
|
36
|
+
- Events are emitted to parent for state management
|
|
37
|
+
|
|
38
|
+
## Login Page
|
|
39
|
+
|
|
40
|
+
The login page handles multiple authentication flows:
|
|
41
|
+
1. **Standard Login** - Email → Password → Login
|
|
42
|
+
2. **Password Recovery** - Email → Send Code → Verify Code → New Password
|
|
43
|
+
|
|
44
|
+
### Component Structure
|
|
45
|
+
|
|
46
|
+
The login component must export a module with:
|
|
47
|
+
- `name` - Component name (should be `"LoginPage"`)
|
|
48
|
+
- `props` - Required props object
|
|
49
|
+
- `template` - HTML template string
|
|
50
|
+
- `style` - CSS styles string (optional but recommended)
|
|
51
|
+
|
|
52
|
+
### Available Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Description |
|
|
55
|
+
|------|------|-------------|
|
|
56
|
+
| `step` | String | Current step in the login flow (`loading`, `email`, `password`, `sendCode`, `verifyCode`, `newPassword`) |
|
|
57
|
+
| `email` | String | User's email address |
|
|
58
|
+
| `password` | String | User's password |
|
|
59
|
+
| `verificationCode` | String | Code sent to user's email for password recovery |
|
|
60
|
+
| `checkbox_remember_me` | Boolean | Remember me checkbox state |
|
|
61
|
+
| `snackbar` | Boolean | Whether to show error message |
|
|
62
|
+
| `errorMessages` | String | Error message to display |
|
|
63
|
+
| `color` | String | Theme color |
|
|
64
|
+
| `showPassword` | Boolean | Toggle password visibility |
|
|
65
|
+
| `processing` | Boolean | Whether a request is in progress |
|
|
66
|
+
| `hidePassword` | Boolean | Toggle password visibility (inverse) |
|
|
67
|
+
| `newPassword` | String | New password for recovery flow |
|
|
68
|
+
| `repeatNewPassword` | String | Confirm new password |
|
|
69
|
+
| `showNewPassword` | Boolean | Toggle new password visibility |
|
|
70
|
+
| `showRepeatNewPassword` | Boolean | Toggle confirm password visibility |
|
|
71
|
+
| `logo` | String | Path to the logo image |
|
|
72
|
+
| `store` | Object | Vuex store instance |
|
|
73
|
+
| `getters` | Object | Vuex getters |
|
|
74
|
+
|
|
75
|
+
### Login Flow Steps
|
|
76
|
+
|
|
77
|
+
1. **`loading`** - Initial loading state, show a spinner
|
|
78
|
+
2. **`email`** - User enters their email address
|
|
79
|
+
3. **`password`** - User enters their password (email field disabled)
|
|
80
|
+
4. **`sendCode`** - Password recovery: inform user a code will be sent
|
|
81
|
+
5. **`verifyCode`** - User enters the verification code
|
|
82
|
+
6. **`newPassword`** - User creates a new password
|
|
83
|
+
|
|
84
|
+
### Events to Emit
|
|
85
|
+
|
|
86
|
+
| Event | When to Emit | Description |
|
|
87
|
+
|-------|--------------|-------------|
|
|
88
|
+
| `update` | On input change | Update a prop value: `$emit('update', { key: 'propName', value: newValue })` |
|
|
89
|
+
| `checkForStepTwo` | Email step submit | Validate email and proceed to password step |
|
|
90
|
+
| `login` | Password step submit | Attempt login with credentials |
|
|
91
|
+
| `goToSendCodeStep` | Forgot password click | Switch to password recovery flow |
|
|
92
|
+
| `sendVerificationCode` | Send code step submit | Send verification code to email |
|
|
93
|
+
| `verifyCode` | Verify code step submit | Verify the entered code |
|
|
94
|
+
| `createNewPassword` | New password step submit | Set the new password |
|
|
95
|
+
|
|
96
|
+
### Example Login Page:
|
|
97
|
+
```js
|
|
98
|
+
module.exports = {
|
|
99
|
+
name: "LoginPage",
|
|
100
|
+
props: [
|
|
101
|
+
'step',
|
|
102
|
+
'email',
|
|
103
|
+
'password',
|
|
104
|
+
'verificationCode',
|
|
105
|
+
'checkbox_remember_me',
|
|
106
|
+
'snackbar',
|
|
107
|
+
'errorMessages',
|
|
108
|
+
'color',
|
|
109
|
+
'showPassword',
|
|
110
|
+
'processing',
|
|
111
|
+
'hidePassword',
|
|
112
|
+
'newPassword',
|
|
113
|
+
'repeatNewPassword',
|
|
114
|
+
'showNewPassword',
|
|
115
|
+
'showRepeatNewPassword',
|
|
116
|
+
'logo',
|
|
117
|
+
'store',
|
|
118
|
+
'getters',
|
|
119
|
+
],
|
|
120
|
+
template: /* html */`
|
|
121
|
+
<div class="lp">
|
|
122
|
+
<img v-if="logo" class="lp__logo" :src="logo" alt="Logo" />
|
|
123
|
+
|
|
124
|
+
<div class="lp__card">
|
|
125
|
+
<div class="lp__top">
|
|
126
|
+
<div class="lp__title">{{ $t('public-pages.login.title') }}</div>
|
|
127
|
+
<router-link class="lp__link" to="@PVP/register">{{ $t('public-pages.login.register-link') }}</router-link>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div v-if="step === 'loading'" class="lp__center" aria-live="polite">
|
|
131
|
+
<div class="spinner-border text-primary" role="status">
|
|
132
|
+
<span class="visually-hidden">{{ $t('public-pages.login.loading') }}</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div v-else>
|
|
137
|
+
<div class="lp__subtitle">{{ $t('public-pages.login.welcome') }}</div>
|
|
138
|
+
|
|
139
|
+
<div v-if="snackbar" class="lp__alert" role="alert">{{ errorMessages }}</div>
|
|
140
|
+
|
|
141
|
+
<form class="lp__form" @submit.prevent>
|
|
142
|
+
<div v-if="step === 'email' || step === 'password'" class="lp__field">
|
|
143
|
+
<label class="lp__label">{{ $t('public-pages.login.your-email') }}</label>
|
|
144
|
+
<input
|
|
145
|
+
class="lp__input"
|
|
146
|
+
type="email"
|
|
147
|
+
autocomplete="email"
|
|
148
|
+
inputmode="email"
|
|
149
|
+
:value="email"
|
|
150
|
+
:disabled="step === 'password'"
|
|
151
|
+
:placeholder="$t('public-pages.login.enter-email')"
|
|
152
|
+
@input="$emit('update', { key: 'email', value: $event.target.value })"
|
|
153
|
+
@keydown.enter="$emit('checkForStepTwo')"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div v-if="step === 'password'" class="lp__field">
|
|
158
|
+
<div class="lp__row">
|
|
159
|
+
<label class="lp__label">{{ $t('public-pages.login.your-password') }}</label>
|
|
160
|
+
<a class="lp__link" href="#" @click.prevent="$emit('goToSendCodeStep')">{{ $t('public-pages.login.forgot-password') }}</a>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="lp__pass">
|
|
163
|
+
<input
|
|
164
|
+
class="lp__input"
|
|
165
|
+
:type="hidePassword ? 'password' : 'text'"
|
|
166
|
+
autocomplete="current-password"
|
|
167
|
+
:value="password"
|
|
168
|
+
:placeholder="$t('public-pages.login.enter-password')"
|
|
169
|
+
@input="$emit('update', { key: 'password', value: $event.target.value })"
|
|
170
|
+
@keydown.enter="$emit('login')"
|
|
171
|
+
/>
|
|
172
|
+
<button type="button" class="lp__icon" @click="$emit('update', { key: 'hidePassword', value: !hidePassword })">
|
|
173
|
+
<v-icon>{{ hidePassword ? 'mdi-eye' : 'mdi-eye-off' }}</v-icon>
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div v-if="step === 'sendCode'" class="lp__hint">
|
|
179
|
+
{{ $t('public-pages.login.verification-message') }} "<b>{{ email }}</b>".
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div v-if="step === 'verifyCode'" class="lp__field">
|
|
183
|
+
<label class="lp__label">{{ $t('public-pages.login.verification-code') }}</label>
|
|
184
|
+
<input
|
|
185
|
+
class="lp__input"
|
|
186
|
+
type="text"
|
|
187
|
+
inputmode="numeric"
|
|
188
|
+
autocomplete="one-time-code"
|
|
189
|
+
:value="verificationCode"
|
|
190
|
+
:placeholder="$t('public-pages.login.enter-code')"
|
|
191
|
+
@input="$emit('update', { key: 'verificationCode', value: $event.target.value })"
|
|
192
|
+
@keydown.enter="$emit('verifyCode')"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<template v-if="step === 'newPassword'">
|
|
197
|
+
<div class="lp__field">
|
|
198
|
+
<label class="lp__label">{{ $t('public-pages.login.new-password') }}</label>
|
|
199
|
+
<div class="lp__pass">
|
|
200
|
+
<input
|
|
201
|
+
class="lp__input"
|
|
202
|
+
:type="showNewPassword ? 'text' : 'password'"
|
|
203
|
+
autocomplete="new-password"
|
|
204
|
+
:value="newPassword"
|
|
205
|
+
:placeholder="$t('public-pages.login.new-password')"
|
|
206
|
+
@input="$emit('update', { key: 'newPassword', value: $event.target.value })"
|
|
207
|
+
/>
|
|
208
|
+
<button type="button" class="lp__icon" @click="$emit('update', { key: 'showNewPassword', value: !showNewPassword })">
|
|
209
|
+
<v-icon>{{ showNewPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div class="lp__field">
|
|
215
|
+
<label class="lp__label">{{ $t('public-pages.login.confirm-password') }}</label>
|
|
216
|
+
<div class="lp__pass">
|
|
217
|
+
<input
|
|
218
|
+
class="lp__input"
|
|
219
|
+
:type="showRepeatNewPassword ? 'text' : 'password'"
|
|
220
|
+
autocomplete="new-password"
|
|
221
|
+
:value="repeatNewPassword"
|
|
222
|
+
:placeholder="$t('public-pages.login.confirm-password')"
|
|
223
|
+
@input="$emit('update', { key: 'repeatNewPassword', value: $event.target.value })"
|
|
224
|
+
/>
|
|
225
|
+
<button type="button" class="lp__icon" @click="$emit('update', { key: 'showRepeatNewPassword', value: !showRepeatNewPassword })">
|
|
226
|
+
<v-icon>{{ showRepeatNewPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</template>
|
|
231
|
+
|
|
232
|
+
<button v-if="step === 'email'" type="button" class="lp__btn" @click="$emit('checkForStepTwo')">{{ $t('public-pages.login.next') }}</button>
|
|
233
|
+
<button v-if="step === 'password'" type="button" class="lp__btn" @click="$emit('login')">{{ $t('public-pages.login.login-button') }}</button>
|
|
234
|
+
<button v-if="step === 'sendCode'" type="button" class="lp__btn" @click="$emit('sendVerificationCode')">{{ $t('public-pages.login.send-code') }}</button>
|
|
235
|
+
<button v-if="step === 'verifyCode'" type="button" class="lp__btn" @click="$emit('verifyCode')">{{ $t('public-pages.login.verify-code') }}</button>
|
|
236
|
+
<button v-if="step === 'newPassword'" type="button" class="lp__btn" @click="$emit('createNewPassword')">{{ $t('public-pages.login.create-new-password') }}</button>
|
|
237
|
+
</form>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
`,
|
|
242
|
+
style: /* css */`
|
|
243
|
+
:root {
|
|
244
|
+
--clr-theme-primary: #1268eb;
|
|
245
|
+
--clr-body-heading: #1e1e1e;
|
|
246
|
+
--lp-border: rgba(0, 0, 0, 0.15);
|
|
247
|
+
}
|
|
248
|
+
.lp { max-width: 520px; margin: 40px auto; padding: 0 16px; }
|
|
249
|
+
.lp__logo { display: block; height: 52px; margin: 0 auto 16px; }
|
|
250
|
+
.lp__card { border: 1px solid var(--lp-border); border-radius: 12px; padding: 16px; background: #fff; }
|
|
251
|
+
.lp__top { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
|
252
|
+
.lp__title { font-weight: 700; color: var(--clr-body-heading); }
|
|
253
|
+
.lp__subtitle { margin: 10px 0 0; color: rgba(0,0,0,.65); }
|
|
254
|
+
.lp__link { color: var(--clr-theme-primary); text-decoration: none; font-weight: 600; font-size: 14px; }
|
|
255
|
+
.lp__link:hover { text-decoration: underline; }
|
|
256
|
+
.lp__alert { margin-top: 12px; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(220,53,69,.25); background: #fef2f2; color: #b91c1c; }
|
|
257
|
+
.lp__form { margin-top: 12px; display: grid; gap: 12px; }
|
|
258
|
+
.lp__field { display: grid; gap: 6px; }
|
|
259
|
+
.lp__row { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
|
|
260
|
+
.lp__label { font-size: 13px; font-weight: 600; color: rgba(0,0,0,.8); }
|
|
261
|
+
.lp__input { width: 100%; height: 40px; padding: 0 10px; border: 1px solid var(--lp-border); border-radius: 8px; box-sizing: border-box; }
|
|
262
|
+
.lp__input:focus { outline: none; border-color: var(--clr-theme-primary); box-shadow: 0 0 0 3px rgba(18,104,235,.15); }
|
|
263
|
+
.lp__pass { position: relative; }
|
|
264
|
+
.lp__pass .lp__input { padding-right: 44px; }
|
|
265
|
+
.lp__icon { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); width: 34px; height: 34px; border: 1px solid var(--lp-border); background: #fff; border-radius: 8px; cursor: pointer; }
|
|
266
|
+
.lp__hint { padding: 10px 12px; border: 1px solid rgba(0,0,0,.08); border-radius: 8px; background: #f8fafc; color: rgba(0,0,0,.75); }
|
|
267
|
+
.lp__btn { height: 40px; border: none; border-radius: 8px; background: var(--clr-theme-primary); color: #fff; font-weight: 700; cursor: pointer; }
|
|
268
|
+
.lp__btn:hover { background: #0d5bcd; }
|
|
269
|
+
.spinner-border { width: 2.25rem; height: 2.25rem; }
|
|
270
|
+
.text-primary { color: var(--clr-theme-primary) !important; }
|
|
271
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
272
|
+
`
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Register Page
|
|
277
|
+
|
|
278
|
+
The register page handles user registration with optional email verification. It supports:
|
|
279
|
+
1. **Standard Registration** - User fills form → Submit → Done
|
|
280
|
+
2. **Registration with Email Verification** - User fills form → Verify Email → Done
|
|
281
|
+
|
|
282
|
+
### Component Structure
|
|
283
|
+
|
|
284
|
+
The register component must export a module with:
|
|
285
|
+
- `name` - Component name (should be `"RegisterPage"`)
|
|
286
|
+
- `props` - Required props object
|
|
287
|
+
- `template` - HTML template string
|
|
288
|
+
- `style` - CSS styles string (optional but recommended)
|
|
289
|
+
|
|
290
|
+
### Available Props
|
|
291
|
+
|
|
292
|
+
| Prop | Type | Description |
|
|
293
|
+
|------|------|-------------|
|
|
294
|
+
| `USE_VERIFICATION` | Boolean | Whether email verification is enabled |
|
|
295
|
+
| `loading` | Boolean | Whether a request is in progress |
|
|
296
|
+
| `finished` | Boolean | Whether registration completed successfully |
|
|
297
|
+
| `activationCodeError` | Boolean | Whether the activation code is invalid |
|
|
298
|
+
| `willCallYourdialog` | Boolean | Dialog state flag |
|
|
299
|
+
| `pageState` | String | Current page state (`register`, `verification`) |
|
|
300
|
+
| `inputsData` | Object | Form data object (see below) |
|
|
301
|
+
| `itemsTunisiaGrades` | Array | List of grade options `[{ value, text }]` |
|
|
302
|
+
| `phoneNumberRequire` | Boolean | Whether phone number is required |
|
|
303
|
+
| `showPassword` | Boolean | Toggle password visibility |
|
|
304
|
+
| `showRepeatPassword` | Boolean | Toggle repeat password visibility |
|
|
305
|
+
| `gradesListSettings` | Object | Grade list configuration |
|
|
306
|
+
| `logo` | String | Path to the logo image |
|
|
307
|
+
| `gradeOption` | Boolean | Whether to show grade selection |
|
|
308
|
+
| `isThirdOrFourthGrade` | Boolean | Special grade flag |
|
|
309
|
+
|
|
310
|
+
### inputsData Object
|
|
311
|
+
|
|
312
|
+
The `inputsData` prop contains all form field values:
|
|
313
|
+
|
|
314
|
+
| Field | Type | Description |
|
|
315
|
+
|-------|------|-------------|
|
|
316
|
+
| `firstName` | String | User's first name |
|
|
317
|
+
| `lastName` | String | User's last name |
|
|
318
|
+
| `phoneNumber` | String | User's phone number |
|
|
319
|
+
| `grade` | String | Selected grade value |
|
|
320
|
+
| `email` | String | User's email address |
|
|
321
|
+
| `password` | String | User's password |
|
|
322
|
+
| `repeatPassword` | String | Password confirmation |
|
|
323
|
+
| `agree` | Boolean | Terms agreement checkbox |
|
|
324
|
+
| `activationCode` | String | Email verification code |
|
|
325
|
+
|
|
326
|
+
### Register Flow States
|
|
327
|
+
|
|
328
|
+
1. **`register`** - Main registration form with all fields
|
|
329
|
+
2. **`verification`** - Email verification step (if `USE_VERIFICATION` is true)
|
|
330
|
+
|
|
331
|
+
### Events to Emit
|
|
332
|
+
|
|
333
|
+
| Event | When to Emit | Description |
|
|
334
|
+
|-------|--------------|-------------|
|
|
335
|
+
| `update` | On state change | Update a prop value: `$emit('update', { key: 'propName', value: newValue })` |
|
|
336
|
+
| `update-inputs-data` | On form input | Update form field: `$emit('update-inputs-data', { key: 'fieldName', value: newValue })` |
|
|
337
|
+
| `fixEmail` | On email input | Sanitize email input |
|
|
338
|
+
| `goToEmailActivation` | Form submit (with verification) | Proceed to email verification step |
|
|
339
|
+
| `submitForm` | Form submit (no verification) | Submit registration directly |
|
|
340
|
+
| `verifyActivationCode` | Verification submit | Verify the activation code and complete registration |
|
|
341
|
+
|
|
342
|
+
### Example Register Page:
|
|
343
|
+
```js
|
|
344
|
+
module.exports = {
|
|
345
|
+
name: "RegisterPage",
|
|
346
|
+
props: [
|
|
347
|
+
'USE_VERIFICATION',
|
|
348
|
+
'loading',
|
|
349
|
+
'finished',
|
|
350
|
+
'activationCodeError',
|
|
351
|
+
'willCallYourdialog',
|
|
352
|
+
'pageState',
|
|
353
|
+
'inputsData',
|
|
354
|
+
'itemsTunisiaGrades',
|
|
355
|
+
'phoneNumberRequire',
|
|
356
|
+
'showPassword',
|
|
357
|
+
'showRepeatPassword',
|
|
358
|
+
'gradesListSettings',
|
|
359
|
+
'logo',
|
|
360
|
+
'gradeOption',
|
|
361
|
+
'isThirdOrFourthGrade',
|
|
362
|
+
],
|
|
363
|
+
template: /* html */`
|
|
364
|
+
<div class="rp">
|
|
365
|
+
<img v-if="logo" class="rp__logo" :src="logo" alt="Logo" />
|
|
366
|
+
|
|
367
|
+
<div class="rp__card">
|
|
368
|
+
<div class="rp__top">
|
|
369
|
+
<div class="rp__title">{{ $t('public-pages.register.title') }}</div>
|
|
370
|
+
<router-link class="rp__link" to="@PVP/login">{{ $t('public-pages.register.login-link') }}</router-link>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div v-if="loading" class="rp__center" aria-live="polite">
|
|
374
|
+
<div class="spinner-border text-primary" role="status">
|
|
375
|
+
<span class="visually-hidden">Loading</span>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div v-else-if="finished" class="rp__center">
|
|
380
|
+
<v-icon color="success" size="72">mdi-check-circle</v-icon>
|
|
381
|
+
<div class="rp__success">{{ $t('public-pages.register.success') }}</div>
|
|
382
|
+
<div class="rp__muted">{{ $t('public-pages.register.success-message') }}</div>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<div v-else>
|
|
386
|
+
<form
|
|
387
|
+
v-if="pageState === 'register'"
|
|
388
|
+
class="rp__form"
|
|
389
|
+
@submit.prevent="USE_VERIFICATION ? $emit('goToEmailActivation') : $emit('submitForm')"
|
|
390
|
+
>
|
|
391
|
+
<div class="rp__field">
|
|
392
|
+
<label class="rp__label">{{ $t('public-pages.register.first-name') }}</label>
|
|
393
|
+
<input
|
|
394
|
+
class="rp__input"
|
|
395
|
+
type="text"
|
|
396
|
+
required
|
|
397
|
+
:placeholder="$t('public-pages.register.first-name')"
|
|
398
|
+
:value="inputsData.firstName"
|
|
399
|
+
@input="$emit('update-inputs-data', { key: 'firstName', value: $event.target.value })"
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<div class="rp__field">
|
|
404
|
+
<label class="rp__label">{{ $t('public-pages.register.last-name') }}</label>
|
|
405
|
+
<input
|
|
406
|
+
class="rp__input"
|
|
407
|
+
type="text"
|
|
408
|
+
required
|
|
409
|
+
:placeholder="$t('public-pages.register.last-name')"
|
|
410
|
+
:value="inputsData.lastName"
|
|
411
|
+
@input="$emit('update-inputs-data', { key: 'lastName', value: $event.target.value })"
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div class="rp__field">
|
|
416
|
+
<label class="rp__label">
|
|
417
|
+
{{ phoneNumberRequire ? $t('public-pages.register.phone-number') : $t('public-pages.register.phone-number-optional') }}
|
|
418
|
+
</label>
|
|
419
|
+
<input
|
|
420
|
+
class="rp__input"
|
|
421
|
+
type="number"
|
|
422
|
+
:required="phoneNumberRequire"
|
|
423
|
+
:placeholder="$t('public-pages.register.phone-placeholder')"
|
|
424
|
+
:value="inputsData.phoneNumber"
|
|
425
|
+
@input="$emit('update-inputs-data', { key: 'phoneNumber', value: $event.target.value })"
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div v-if="gradeOption" class="rp__field">
|
|
430
|
+
<label class="rp__label">{{ $t('public-pages.register.grade') }}</label>
|
|
431
|
+
<select
|
|
432
|
+
class="rp__select"
|
|
433
|
+
required
|
|
434
|
+
:value="inputsData.grade"
|
|
435
|
+
@input="$emit('update-inputs-data', { key: 'grade', value: $event.target.value })"
|
|
436
|
+
>
|
|
437
|
+
<option value="" disabled>{{ $t('public-pages.register.choose-grade') }}</option>
|
|
438
|
+
<option v-for="g in itemsTunisiaGrades" :key="g.value" :value="g.value">{{ g.text }}</option>
|
|
439
|
+
</select>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div class="rp__field">
|
|
443
|
+
<label class="rp__label">{{ $t('public-pages.register.email') }}</label>
|
|
444
|
+
<input
|
|
445
|
+
class="rp__input"
|
|
446
|
+
type="email"
|
|
447
|
+
required
|
|
448
|
+
:placeholder="$t('public-pages.register.email')"
|
|
449
|
+
:value="inputsData.email"
|
|
450
|
+
@input="$emit('update-inputs-data', { key: 'email', value: $event.target.value });$emit('fixEmail', $event);"
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<div class="rp__field">
|
|
455
|
+
<label class="rp__label">{{ $t('public-pages.register.password') }}</label>
|
|
456
|
+
<div class="rp__pass">
|
|
457
|
+
<input
|
|
458
|
+
class="rp__input"
|
|
459
|
+
:type="showPassword ? 'text' : 'password'"
|
|
460
|
+
required
|
|
461
|
+
:placeholder="$t('public-pages.register.password')"
|
|
462
|
+
:value="inputsData.password"
|
|
463
|
+
@input="$emit('update-inputs-data', { key: 'password', value: $event.target.value })"
|
|
464
|
+
/>
|
|
465
|
+
<button type="button" class="rp__icon" @click="$emit('update', { key: 'showPassword', value: !showPassword })">
|
|
466
|
+
<v-icon>{{ showPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
|
|
467
|
+
</button>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<div class="rp__field">
|
|
472
|
+
<label class="rp__label">{{ $t('public-pages.register.repeat-password') }}</label>
|
|
473
|
+
<div class="rp__pass">
|
|
474
|
+
<input
|
|
475
|
+
class="rp__input"
|
|
476
|
+
:type="showRepeatPassword ? 'text' : 'password'"
|
|
477
|
+
required
|
|
478
|
+
:placeholder="$t('public-pages.register.repeat-password')"
|
|
479
|
+
:value="inputsData.repeatPassword"
|
|
480
|
+
@input="$emit('update-inputs-data', { key: 'repeatPassword', value: $event.target.value })"
|
|
481
|
+
/>
|
|
482
|
+
<button type="button" class="rp__icon" @click="$emit('update', { key: 'showRepeatPassword', value: !showRepeatPassword })">
|
|
483
|
+
<v-icon>{{ showRepeatPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<label class="rp__check">
|
|
489
|
+
<input
|
|
490
|
+
type="checkbox"
|
|
491
|
+
:checked="!!inputsData.agree"
|
|
492
|
+
@input="$emit('update-inputs-data', { key: 'agree', value: $event.target.checked })"
|
|
493
|
+
/>
|
|
494
|
+
<span>{{ $t('public-pages.register.terms-agreement') }}</span>
|
|
495
|
+
</label>
|
|
496
|
+
|
|
497
|
+
<button type="submit" class="rp__btn">{{ $t('public-pages.register.register-button') }}</button>
|
|
498
|
+
</form>
|
|
499
|
+
|
|
500
|
+
<div v-else-if="pageState === 'verification'" class="rp__form">
|
|
501
|
+
<div class="rp__subtitle">{{ $t('public-pages.register.email-verification') }}</div>
|
|
502
|
+
|
|
503
|
+
<div class="rp__field">
|
|
504
|
+
<label class="rp__label">{{ $t('public-pages.register.activation-code') }}</label>
|
|
505
|
+
<input
|
|
506
|
+
class="rp__input"
|
|
507
|
+
type="text"
|
|
508
|
+
:placeholder="$t('public-pages.register.activation-code')"
|
|
509
|
+
:value="inputsData.activationCode"
|
|
510
|
+
@input="$emit('update-inputs-data', { key: 'activationCode', value: $event.target.value })"
|
|
511
|
+
/>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div v-if="activationCodeError" class="rp__error">{{ $t('public-pages.register.invalid-activation-code') }}</div>
|
|
515
|
+
|
|
516
|
+
<div class="rp__actions">
|
|
517
|
+
<button class="rp__btn rp__btn--secondary" @click="$emit('update', { key: 'pageState', value: 'register' })">
|
|
518
|
+
{{ $t('public-pages.register.previous') }}
|
|
519
|
+
</button>
|
|
520
|
+
<button class="rp__btn" @click="$emit('verifyActivationCode')">
|
|
521
|
+
{{ $t('public-pages.register.verify-and-register') }}
|
|
522
|
+
</button>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
`,
|
|
529
|
+
style: /* css */`
|
|
530
|
+
:root {
|
|
531
|
+
--clr-theme-primary: #1268eb;
|
|
532
|
+
--clr-body-heading: #1e1e1e;
|
|
533
|
+
--rp-border: rgba(0, 0, 0, 0.15);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.rp { max-width: 640px; margin: 40px auto; padding: 0 16px; }
|
|
537
|
+
.rp__logo { display: block; height: 52px; margin: 0 auto 16px; }
|
|
538
|
+
.rp__card { border: 1px solid var(--rp-border); border-radius: 12px; padding: 16px; background: #fff; }
|
|
539
|
+
.rp__top { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
|
540
|
+
.rp__title { font-weight: 700; color: var(--clr-body-heading); }
|
|
541
|
+
.rp__subtitle { margin-top: 10px; font-weight: 700; color: var(--clr-body-heading); }
|
|
542
|
+
.rp__link { color: var(--clr-theme-primary); text-decoration: none; font-weight: 600; font-size: 14px; }
|
|
543
|
+
.rp__link:hover { text-decoration: underline; }
|
|
544
|
+
.rp__muted { color: rgba(0,0,0,.65); margin-top: 6px; }
|
|
545
|
+
.rp__center { padding: 18px 0; display: grid; gap: 10px; justify-items: center; }
|
|
546
|
+
.rp__success { font-weight: 800; color: #2e7d32; }
|
|
547
|
+
|
|
548
|
+
.rp__form { margin-top: 12px; display: grid; gap: 12px; }
|
|
549
|
+
.rp__field { display: grid; gap: 6px; }
|
|
550
|
+
.rp__label { font-size: 13px; font-weight: 600; color: rgba(0,0,0,.8); }
|
|
551
|
+
.rp__input, .rp__select { width: 100%; height: 40px; padding: 0 10px; border: 1px solid var(--rp-border); border-radius: 8px; box-sizing: border-box; background: #fff; }
|
|
552
|
+
.rp__input:focus, .rp__select:focus { outline: none; border-color: var(--clr-theme-primary); box-shadow: 0 0 0 3px rgba(18,104,235,.15); }
|
|
553
|
+
.rp__pass { position: relative; }
|
|
554
|
+
.rp__pass .rp__input { padding-right: 44px; }
|
|
555
|
+
.rp__icon { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); width: 34px; height: 34px; border: 1px solid var(--rp-border); background: #fff; border-radius: 8px; cursor: pointer; }
|
|
556
|
+
.rp__check { display: flex; align-items: center; gap: 10px; font-size: 14px; color: rgba(0,0,0,.8); }
|
|
557
|
+
.rp__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
558
|
+
.rp__error { color: #b91c1c; font-weight: 600; }
|
|
559
|
+
|
|
560
|
+
.rp__btn { height: 40px; border: none; border-radius: 8px; background: var(--clr-theme-primary); color: #fff; font-weight: 700; cursor: pointer; }
|
|
561
|
+
.rp__btn:hover { background: #0d5bcd; }
|
|
562
|
+
.rp__btn--secondary { background: rgba(0,0,0,.10); color: rgba(0,0,0,.85); }
|
|
563
|
+
.rp__btn--secondary:hover { background: rgba(0,0,0,.14); }
|
|
564
|
+
|
|
565
|
+
.spinner-border { width: 2.25rem; height: 2.25rem; }
|
|
566
|
+
.text-primary { color: var(--clr-theme-primary) !important; }
|
|
567
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
568
|
+
`
|
|
569
|
+
};
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## Vue Component Format
|
|
573
|
+
|
|
574
|
+
### Basic Structure
|
|
575
|
+
|
|
576
|
+
```js
|
|
577
|
+
module.exports = {
|
|
578
|
+
name: "MyComponent",
|
|
579
|
+
|
|
580
|
+
props: {
|
|
581
|
+
title: { required: true },
|
|
582
|
+
count: { default: 0 }
|
|
583
|
+
},
|
|
584
|
+
// Or in array format:
|
|
585
|
+
// props: [
|
|
586
|
+
// 'title',
|
|
587
|
+
// 'count'
|
|
588
|
+
// ],
|
|
589
|
+
|
|
590
|
+
template: /* html */`
|
|
591
|
+
<div class="my-component">
|
|
592
|
+
<h1>{{ title }}</h1>
|
|
593
|
+
<button @click="increment">Count: {{ counter }}</button>
|
|
594
|
+
</div>
|
|
595
|
+
`,
|
|
596
|
+
|
|
597
|
+
style: /* css */`
|
|
598
|
+
.my-component { padding: 20px; }
|
|
599
|
+
.my-component h1 { color: blue; }
|
|
600
|
+
`,
|
|
601
|
+
|
|
602
|
+
data: /* js */`
|
|
603
|
+
function() {
|
|
604
|
+
return {
|
|
605
|
+
counter: 0
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
`,
|
|
609
|
+
|
|
610
|
+
methods: /* js */`
|
|
611
|
+
{
|
|
612
|
+
increment() {
|
|
613
|
+
this.counter++;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
`,
|
|
617
|
+
|
|
618
|
+
mounted: /* js */`
|
|
619
|
+
function() {
|
|
620
|
+
console.log('Component mounted!');
|
|
621
|
+
}
|
|
622
|
+
`
|
|
623
|
+
};
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Available Fields
|
|
627
|
+
|
|
628
|
+
| Field | Format | Description |
|
|
629
|
+
|-------|--------|-------------|
|
|
630
|
+
| `name` | String | Component name (required) |
|
|
631
|
+
| `props` | Object | Props definition (not a string) |
|
|
632
|
+
| `template` | Template literal | HTML template with Vue syntax |
|
|
633
|
+
| `style` | Template literal | CSS styles for the component |
|
|
634
|
+
| `data` | Template literal | Function returning initial state |
|
|
635
|
+
| `computed` | Template literal | Object with computed properties |
|
|
636
|
+
| `watch` | Template literal | Object with watchers |
|
|
637
|
+
| `methods` | Template literal | Object with methods |
|
|
638
|
+
| `mounted` | Template literal | Lifecycle hook function |
|
|
639
|
+
| `created`, `beforeMount`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed` | Template literal | Other lifecycle hooks |
|
|
640
|
+
|
|
641
|
+
### Key Rules
|
|
642
|
+
|
|
643
|
+
1. **Use template literals** (backticks) for `template`, `style`, `data`, `methods`, etc.
|
|
644
|
+
2. **Props is an object**, not a template literal
|
|
645
|
+
3. **Comments are optional** but recommended: `/* html */`, `/* css */`, `/* js */`
|
|
646
|
+
4. **Unused fields** can be omitted or set to `null`
|
|
647
|
+
5. **Empty file marker**: `/* EMPTY FILE */` for placeholder files
|
|
648
|
+
|
|
649
|
+
### Minimal Example
|
|
650
|
+
|
|
651
|
+
```js
|
|
652
|
+
module.exports = {
|
|
653
|
+
name: "HelloWorld",
|
|
654
|
+
template: /* html */`
|
|
655
|
+
<div>Hello, World!</div>
|
|
656
|
+
`
|
|
657
|
+
};
|
|
658
|
+
```
|