cyber-elx 1.0.7 → 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.
@@ -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
+ ```