Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 326b835b50 | |||
| 374c83f746 | |||
| 47d6a2f0c8 | |||
| 58458c8755 | |||
| 0339958262 | |||
| cf5dc65779 | |||
| 16126669ff | |||
| 288f893f7e | |||
| 9689db4c5a | |||
| 6d1ca0da63 | |||
| 45008afe91 | |||
| 7db74c435e | |||
| 41c6ffc743 | |||
| d1c44eb82f | |||
| 032756a658 | |||
| 0f2da28b12 | |||
| c6d7598772 | |||
| 4451a7a590 | |||
| de2c96a39c | |||
| a370c9d74e | |||
| 3251939454 | |||
| 4e6c529df2 | |||
| a97556b9ae | |||
| feed0a38ab | |||
| bf9e7c9538 | |||
| da89952aaf | |||
| 92f35bf73d | |||
| f48a4b2800 | |||
| 0099a63fad | |||
| f4fb8277d2 | |||
| fb56271b8e | |||
| 0b26340b76 | |||
| 442fbb648e | |||
| 7d8173f437 | |||
| f4f3b2f814 | |||
| 89fd27e9fd | |||
| 6bfde7df59 | |||
| 89203083ac | |||
| e5ba9a7ee1 | |||
| 4563006b82 | |||
| 7a5731c0e2 | |||
| cd476aea24 | |||
| 4116b874fe | |||
| 1b7de87479 | |||
| cab7547f9a | |||
| 20bc88087c | |||
| ac0b65d0ea | |||
| 347746188a | |||
| 3334f5d507 | |||
| 632c57e453 | |||
| d135b8ef79 | |||
| 1d8a498f15 | |||
| c2e0d6d23c | |||
| 6a8d321a3f | |||
| 78db26e5c4 | |||
| a10db14b54 | |||
| e867d4e568 | |||
| 6335ee7702 | |||
| 973f2c2c26 | |||
| 81ce5f8263 | |||
| df7678b129 |
@@ -0,0 +1,42 @@
|
||||
name: Build Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Retrieve Server Install Action
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: Nodarx/action-server-install
|
||||
ref: main
|
||||
path: action-server-install
|
||||
github-server-url: https://git.ktrix.dev
|
||||
|
||||
- name: Install Server
|
||||
uses: ./action-server-install
|
||||
with:
|
||||
install-php: 'false'
|
||||
install-node: 'true'
|
||||
php-version: '8.5'
|
||||
node-version: '24'
|
||||
server-path: './server'
|
||||
|
||||
- name: Checkout Pull Request
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: server/modules/user_profile
|
||||
github-server-url: https://git.ktrix.dev
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: server/modules/user_profile
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
working-directory: server/modules/user_profile
|
||||
@@ -0,0 +1,42 @@
|
||||
name: JS Unit Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Retrieve Server Install Action
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: Nodarx/action-server-install
|
||||
ref: main
|
||||
path: action-server-install
|
||||
github-server-url: https://git.ktrix.dev
|
||||
|
||||
- name: Install Server
|
||||
uses: ./action-server-install
|
||||
with:
|
||||
install-php: 'false'
|
||||
install-node: 'true'
|
||||
php-version: '8.5'
|
||||
node-version: '24'
|
||||
server-path: './server'
|
||||
|
||||
- name: Checkout Pull Request
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: server/modules/user_profile
|
||||
github-server-url: https://git.ktrix.dev
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: server/modules/user_profile
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:unit
|
||||
working-directory: server/modules/user_profile
|
||||
@@ -25,11 +25,17 @@ jobs:
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Renovate
|
||||
run: npm install -g renovate
|
||||
run: |
|
||||
npm install --global --no-audit --fund=false \
|
||||
--prefix "${{ runner.temp }}/renovate-npm" \
|
||||
--cache "${{ runner.temp }}/renovate-npm-cache" \
|
||||
renovate
|
||||
"${{ runner.temp }}/renovate-npm/bin/renovate" --version
|
||||
|
||||
- name: Run Renovate
|
||||
env:
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
||||
run: renovate ${{ gitea.repository }}
|
||||
run: |
|
||||
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
|
||||
+2
-4
@@ -12,12 +12,10 @@ node_modules/
|
||||
/static/
|
||||
|
||||
# Backend development
|
||||
/vendor/
|
||||
/lib/vendor/
|
||||
coverage/
|
||||
phpunit.xml.cache
|
||||
.phpunit.result.cache
|
||||
.php-cs-fixer.cache
|
||||
.phpstan.cache
|
||||
*.cache
|
||||
.phpactor/
|
||||
|
||||
# Editors
|
||||
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "71f6c55e6bc13ae680f9f4bb06eae7ce",
|
||||
"packages": [],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"account": {
|
||||
"address": "Adresse",
|
||||
"addressPlaceholder": "Musterstraße 123",
|
||||
"city": "Stadt",
|
||||
"cityPlaceholder": "Berlin",
|
||||
"countries": {
|
||||
"australia": "Australien",
|
||||
"brazil": "Brasilien",
|
||||
"canada": "Kanada",
|
||||
"china": "China",
|
||||
"france": "Frankreich",
|
||||
"germany": "Deutschland",
|
||||
"india": "Indien",
|
||||
"japan": "Japan",
|
||||
"mexico": "Mexiko",
|
||||
"southAfrica": "Südafrika",
|
||||
"unitedKingdom": "Vereinigtes Königreich",
|
||||
"unitedStates": "Vereinigte Staaten"
|
||||
},
|
||||
"country": "Land",
|
||||
"email": "E-Mail",
|
||||
"firstName": "Vorname",
|
||||
"firstNamePlaceholder": "Max",
|
||||
"lastName": "Nachname",
|
||||
"lastNamePlaceholder": "Mustermann",
|
||||
"managedFieldHint": "Wird vom Authentifizierungsanbieter verwaltet",
|
||||
"phoneNumber": "Telefonnummer",
|
||||
"photoRequirements": "Zulässig sind JPG, GIF oder PNG. Maximale Größe: 800 KB",
|
||||
"selectCountry": "Land auswählen",
|
||||
"stateOrProvince": "Bundesland/Provinz",
|
||||
"statePlaceholder": "Bundesland",
|
||||
"title": "Kontodetails",
|
||||
"uploadPhoto": "Neues Foto hochladen",
|
||||
"zipCode": "Postleitzahl"
|
||||
},
|
||||
"actions": {
|
||||
"reset": "Zurücksetzen",
|
||||
"saveChanges": "Änderungen speichern"
|
||||
},
|
||||
"nav": {
|
||||
"preferences": "Einstellungen",
|
||||
"profile": "Profil",
|
||||
"profileSettings": "Profil & Einstellungen",
|
||||
"security": "Sicherheit"
|
||||
},
|
||||
"notifications": {
|
||||
"columns": {
|
||||
"app": "App",
|
||||
"browser": "BROWSER",
|
||||
"email": "E-MAIL",
|
||||
"type": "Typ"
|
||||
},
|
||||
"permissionDescription": "Wir benötigen die Berechtigung Ihres Browsers, um Benachrichtigungen anzuzeigen.",
|
||||
"requestPermission": "Berechtigung anfordern",
|
||||
"timing": {
|
||||
"anytime": "Jederzeit",
|
||||
"online": "Nur wenn ich online bin"
|
||||
},
|
||||
"timingQuestion": "Wann sollen wir Ihnen Benachrichtigungen senden?",
|
||||
"title": "Benachrichtigungen",
|
||||
"types": {
|
||||
"accountActivity": "Kontoaktivität",
|
||||
"newBrowserSignIn": "Anmeldung über einen neuen Browser",
|
||||
"newDeviceLinked": "Ein neues Gerät wurde verknüpft",
|
||||
"newForYou": "Neu für Sie"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"languageHint": "Wird sofort angewendet und in Ihrem Konto gespeichert",
|
||||
"languageLabel": "Anzeigesprache",
|
||||
"languageTitle": "Sprache"
|
||||
},
|
||||
"security": {
|
||||
"emptyDescription": "Derzeit sind keine Sicherheitsmodule aktiviert oder installiert.",
|
||||
"emptyTitle": "Keine Sicherheitseinstellungen verfügbar"
|
||||
},
|
||||
"tabs": {
|
||||
"account": "Konto",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"preferences": "Einstellungen",
|
||||
"security": "Sicherheit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"account": {
|
||||
"address": "Address",
|
||||
"addressPlaceholder": "123 Main St",
|
||||
"city": "City",
|
||||
"cityPlaceholder": "New York",
|
||||
"countries": {
|
||||
"australia": "Australia",
|
||||
"brazil": "Brazil",
|
||||
"canada": "Canada",
|
||||
"china": "China",
|
||||
"france": "France",
|
||||
"germany": "Germany",
|
||||
"india": "India",
|
||||
"japan": "Japan",
|
||||
"mexico": "Mexico",
|
||||
"southAfrica": "South Africa",
|
||||
"unitedKingdom": "United Kingdom",
|
||||
"unitedStates": "United States"
|
||||
},
|
||||
"country": "Country",
|
||||
"email": "E-mail",
|
||||
"firstName": "First Name",
|
||||
"firstNamePlaceholder": "John",
|
||||
"lastName": "Last Name",
|
||||
"lastNamePlaceholder": "Doe",
|
||||
"managedFieldHint": "Managed by authentication provider",
|
||||
"phoneNumber": "Phone Number",
|
||||
"photoRequirements": "Allowed JPG, GIF or PNG. Max size of 800K",
|
||||
"selectCountry": "Select Country",
|
||||
"stateOrProvince": "State/Province",
|
||||
"statePlaceholder": "State",
|
||||
"title": "Account Details",
|
||||
"uploadPhoto": "Upload new photo",
|
||||
"zipCode": "Zip Code"
|
||||
},
|
||||
"actions": {
|
||||
"reset": "Reset",
|
||||
"saveChanges": "Save changes"
|
||||
},
|
||||
"nav": {
|
||||
"preferences": "Preferences",
|
||||
"profile": "Profile",
|
||||
"profileSettings": "Profile & Settings",
|
||||
"security": "Security"
|
||||
},
|
||||
"notifications": {
|
||||
"columns": {
|
||||
"app": "App",
|
||||
"browser": "BROWSER",
|
||||
"email": "EMAIL",
|
||||
"type": "Type"
|
||||
},
|
||||
"permissionDescription": "We need permission from your browser to show notifications.",
|
||||
"requestPermission": "Request Permission",
|
||||
"timing": {
|
||||
"anytime": "Anytime",
|
||||
"online": "Only when I'm online"
|
||||
},
|
||||
"timingQuestion": "When should we send you notifications?",
|
||||
"title": "Notifications",
|
||||
"types": {
|
||||
"accountActivity": "Account activity",
|
||||
"newBrowserSignIn": "A new browser used to sign in",
|
||||
"newDeviceLinked": "A new device is linked",
|
||||
"newForYou": "New for you"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"languageHint": "Applies immediately and is saved to your account",
|
||||
"languageLabel": "Display language",
|
||||
"languageTitle": "Language"
|
||||
},
|
||||
"security": {
|
||||
"emptyDescription": "No security modules are currently enabled or installed.",
|
||||
"emptyTitle": "No Security Settings Available"
|
||||
},
|
||||
"tabs": {
|
||||
"account": "Account",
|
||||
"notifications": "Notifications",
|
||||
"preferences": "Preferences",
|
||||
"security": "Security"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"nav": {
|
||||
"preferences": "Preferences",
|
||||
"profile": "Profile",
|
||||
"profileSettings": "Profile & Settings",
|
||||
"security": "Security"
|
||||
}
|
||||
}
|
||||
Generated
+2784
-864
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -11,20 +11,28 @@
|
||||
"dev": "vite build --mode development --config vite.config.ts",
|
||||
"watch": "vite build --mode development --watch --config vite.config.ts",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"test": "vitest run --config tests/js/vitest.config.ts",
|
||||
"test:unit": "vitest run --config tests/js/vitest.config.ts",
|
||||
"test:watch": "vitest watch --config tests/js/vitest.config.ts",
|
||||
"test:coverage": "vitest run --coverage --config tests/js/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
"pinia": "^3.0.0",
|
||||
"pinia": "^4.0.0",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-router": "^5.0.0",
|
||||
"vuetify": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^7.1.2",
|
||||
"vue-tsc": "^3.0.5"
|
||||
"vite": "^8.0.0",
|
||||
"vue-tsc": "^3.0.5",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"@vue/test-utils": "^2.4.10",
|
||||
"jsdom": "^30.0.0",
|
||||
"vitest": "^4.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@KTXC/stores/userStore'
|
||||
import { userService } from '@KTXC/services/user/userService'
|
||||
import { useL10n } from '@KTXC'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { t } = useL10n('user_profile')
|
||||
|
||||
const refInputEl = ref<HTMLElement>()
|
||||
const isSaving = ref(false)
|
||||
@@ -134,27 +136,27 @@ const resetAvatar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const countries = [
|
||||
'United States',
|
||||
'United Kingdom',
|
||||
'Canada',
|
||||
'Australia',
|
||||
'Germany',
|
||||
'France',
|
||||
'India',
|
||||
'China',
|
||||
'Japan',
|
||||
'Brazil',
|
||||
'Mexico',
|
||||
'South Africa',
|
||||
]
|
||||
const countries = computed(() => [
|
||||
{ value: 'United States', title: t('account.countries.unitedStates', 'United States') },
|
||||
{ value: 'United Kingdom', title: t('account.countries.unitedKingdom', 'United Kingdom') },
|
||||
{ value: 'Canada', title: t('account.countries.canada', 'Canada') },
|
||||
{ value: 'Australia', title: t('account.countries.australia', 'Australia') },
|
||||
{ value: 'Germany', title: t('account.countries.germany', 'Germany') },
|
||||
{ value: 'France', title: t('account.countries.france', 'France') },
|
||||
{ value: 'India', title: t('account.countries.india', 'India') },
|
||||
{ value: 'China', title: t('account.countries.china', 'China') },
|
||||
{ value: 'Japan', title: t('account.countries.japan', 'Japan') },
|
||||
{ value: 'Brazil', title: t('account.countries.brazil', 'Brazil') },
|
||||
{ value: 'Mexico', title: t('account.countries.mexico', 'Mexico') },
|
||||
{ value: 'South Africa', title: t('account.countries.southAfrica', 'South Africa') },
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="Account Details">
|
||||
<VCard :title="t('account.title', 'Account Details')">
|
||||
<VCardText class="d-flex">
|
||||
<!-- 👉 Avatar -->
|
||||
<VAvatar
|
||||
@@ -172,7 +174,7 @@ const countries = [
|
||||
@click="refInputEl?.click()"
|
||||
>
|
||||
<VIcon icon="mdi-cloud-upload" class="d-sm-none" />
|
||||
<span class="d-none d-sm-block">Upload new photo</span>
|
||||
<span class="d-none d-sm-block">{{ t('account.uploadPhoto', 'Upload new photo') }}</span>
|
||||
</VBtn>
|
||||
|
||||
<input
|
||||
@@ -190,13 +192,13 @@ const countries = [
|
||||
variant="outlined"
|
||||
@click="resetAvatar"
|
||||
>
|
||||
<span class="d-none d-sm-block">Reset</span>
|
||||
<span class="d-none d-sm-block">{{ t('actions.reset', 'Reset') }}</span>
|
||||
<VIcon icon="mdi-refresh" class="d-sm-none" />
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<p class="text-body-1 mb-0">
|
||||
Allowed JPG, GIF or PNG. Max size of 800K
|
||||
{{ t('account.photoRequirements', 'Allowed JPG, GIF or PNG. Max size of 800K') }}
|
||||
</p>
|
||||
</form>
|
||||
</VCardText>
|
||||
@@ -214,10 +216,10 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.name_given"
|
||||
placeholder="John"
|
||||
label="First Name"
|
||||
:placeholder="t('account.firstNamePlaceholder', 'John')"
|
||||
:label="t('account.firstName', 'First Name')"
|
||||
:readonly="!isFieldEditable('name_given')"
|
||||
:hint="!isFieldEditable('name_given') ? 'Managed by authentication provider' : ''"
|
||||
:hint="!isFieldEditable('name_given') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
@@ -229,10 +231,10 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.name_family"
|
||||
placeholder="Doe"
|
||||
label="Last Name"
|
||||
:placeholder="t('account.lastNamePlaceholder', 'Doe')"
|
||||
:label="t('account.lastName', 'Last Name')"
|
||||
:readonly="!isFieldEditable('name_family')"
|
||||
:hint="!isFieldEditable('name_family') ? 'Managed by authentication provider' : ''"
|
||||
:hint="!isFieldEditable('name_family') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
@@ -244,11 +246,11 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.email"
|
||||
label="E-mail"
|
||||
:label="t('account.email', 'E-mail')"
|
||||
placeholder="john.doe@example.com"
|
||||
type="email"
|
||||
:readonly="!isFieldEditable('email')"
|
||||
:hint="!isFieldEditable('email') ? 'Managed by authentication provider' : ''"
|
||||
:hint="!isFieldEditable('email') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
@@ -260,10 +262,10 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.phone"
|
||||
label="Phone Number"
|
||||
:label="t('account.phoneNumber', 'Phone Number')"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
:readonly="!isFieldEditable('phone')"
|
||||
:hint="!isFieldEditable('phone') ? 'Managed by authentication provider' : ''"
|
||||
:hint="!isFieldEditable('phone') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
@@ -275,8 +277,8 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.address"
|
||||
label="Address"
|
||||
placeholder="123 Main St"
|
||||
:label="t('account.address', 'Address')"
|
||||
:placeholder="t('account.addressPlaceholder', '123 Main St')"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -287,8 +289,8 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.city"
|
||||
label="City"
|
||||
placeholder="New York"
|
||||
:label="t('account.city', 'City')"
|
||||
:placeholder="t('account.cityPlaceholder', 'New York')"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -299,8 +301,8 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.state"
|
||||
label="State/Province"
|
||||
placeholder="State"
|
||||
:label="t('account.stateOrProvince', 'State/Province')"
|
||||
:placeholder="t('account.statePlaceholder', 'State')"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -311,7 +313,7 @@ const countries = [
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountDataLocal.zip"
|
||||
label="Zip Code"
|
||||
:label="t('account.zipCode', 'Zip Code')"
|
||||
placeholder="10001"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -323,9 +325,9 @@ const countries = [
|
||||
>
|
||||
<VSelect
|
||||
v-model="accountDataLocal.country"
|
||||
label="Country"
|
||||
:label="t('account.country', 'Country')"
|
||||
:items="countries"
|
||||
placeholder="Select Country"
|
||||
:placeholder="t('account.selectCountry', 'Select Country')"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -339,7 +341,7 @@ const countries = [
|
||||
:loading="isSaving"
|
||||
color="primary"
|
||||
>
|
||||
Save changes
|
||||
{{ t('actions.saveChanges', 'Save changes') }}
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
@@ -348,7 +350,7 @@ const countries = [
|
||||
type="reset"
|
||||
@click.prevent="resetForm"
|
||||
>
|
||||
Reset
|
||||
{{ t('actions.reset', 'Reset') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -1,69 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useL10n } from '@KTXC'
|
||||
|
||||
const recentDevices = ref(
|
||||
[
|
||||
{
|
||||
type: 'New for you',
|
||||
email: true,
|
||||
browser: true,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'Account activity',
|
||||
email: true,
|
||||
browser: true,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'A new browser used to sign in',
|
||||
email: true,
|
||||
browser: true,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'A new device is linked',
|
||||
email: true,
|
||||
browser: false,
|
||||
app: false,
|
||||
},
|
||||
],
|
||||
)
|
||||
const { t } = useL10n('user_profile')
|
||||
|
||||
const selectedNotification = ref('Only when I\'m online')
|
||||
const recentDevices = ref([
|
||||
{ id: 'newForYou', email: true, browser: true, app: true },
|
||||
{ id: 'accountActivity', email: true, browser: true, app: true },
|
||||
{ id: 'newBrowserSignIn', email: true, browser: true, app: false },
|
||||
{ id: 'newDeviceLinked', email: true, browser: false, app: false },
|
||||
])
|
||||
|
||||
const notificationTypeLabels = computed<Record<string, string>>(() => ({
|
||||
newForYou: t('notifications.types.newForYou', 'New for you'),
|
||||
accountActivity: t('notifications.types.accountActivity', 'Account activity'),
|
||||
newBrowserSignIn: t('notifications.types.newBrowserSignIn', 'A new browser used to sign in'),
|
||||
newDeviceLinked: t('notifications.types.newDeviceLinked', 'A new device is linked'),
|
||||
}))
|
||||
|
||||
const notificationTimingOptions = computed(() => [
|
||||
{ value: 'online', title: t('notifications.timing.online', 'Only when I\'m online') },
|
||||
{ value: 'anytime', title: t('notifications.timing.anytime', 'Anytime') },
|
||||
])
|
||||
|
||||
const selectedNotification = ref('online')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="Notifications">
|
||||
<VCard :title="t('notifications.title', 'Notifications')">
|
||||
<VCardText>
|
||||
We need permission from your browser to show notifications.
|
||||
<a href="javascript:void(0)">Request Permission</a>
|
||||
{{ t('notifications.permissionDescription', 'We need permission from your browser to show notifications.') }}
|
||||
<a href="javascript:void(0)">{{ t('notifications.requestPermission', 'Request Permission') }}</a>
|
||||
</VCardText>
|
||||
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
Type
|
||||
{{ t('notifications.columns.type', 'Type') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
EMAIL
|
||||
{{ t('notifications.columns.email', 'EMAIL') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
BROWSER
|
||||
{{ t('notifications.columns.browser', 'BROWSER') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
App
|
||||
{{ t('notifications.columns.app', 'App') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="device in recentDevices"
|
||||
:key="device.type"
|
||||
:key="device.id"
|
||||
>
|
||||
<td>
|
||||
{{ device.type }}
|
||||
{{ notificationTypeLabels[device.id] }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="device.email" />
|
||||
@@ -82,7 +75,7 @@ const selectedNotification = ref('Only when I\'m online')
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<p class="text-base font-weight-medium">
|
||||
When should we send you notifications?
|
||||
{{ t('notifications.timingQuestion', 'When should we send you notifications?') }}
|
||||
</p>
|
||||
|
||||
<VRow>
|
||||
@@ -93,21 +86,21 @@ const selectedNotification = ref('Only when I\'m online')
|
||||
<VSelect
|
||||
v-model="selectedNotification"
|
||||
mandatory
|
||||
:items="['Only when I\'m online', 'Anytime']"
|
||||
:items="notificationTimingOptions"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit">
|
||||
Save Changes
|
||||
{{ t('actions.saveChanges', 'Save changes') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
type="reset"
|
||||
>
|
||||
Reset
|
||||
{{ t('actions.reset', 'Reset') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useL10nStore, useL10n } from '@KTXC'
|
||||
|
||||
const l10nStore = useL10nStore()
|
||||
const { t } = useL10n('user_profile')
|
||||
|
||||
// Offer each available locale under its own native name
|
||||
const languageOptions = computed(() =>
|
||||
l10nStore.available.map((locale: string) => {
|
||||
let title = locale
|
||||
try {
|
||||
title = new Intl.DisplayNames([locale], { type: 'language' }).of(locale) ?? locale
|
||||
} catch {
|
||||
// unknown tag: fall back to the raw locale code
|
||||
}
|
||||
return { value: locale, title }
|
||||
})
|
||||
)
|
||||
|
||||
const selectLanguage = (locale: string | null) => {
|
||||
if (locale && locale !== l10nStore.locale) {
|
||||
l10nStore.setLocale(locale)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCard :title="t('preferences.languageTitle', 'Language')">
|
||||
<VCardText>
|
||||
<VSelect
|
||||
:model-value="l10nStore.locale"
|
||||
:items="languageOptions"
|
||||
:label="t('preferences.languageLabel', 'Display language')"
|
||||
:hint="t('preferences.languageHint', 'Applies immediately and is saved to your account')"
|
||||
persistent-hint
|
||||
@update:model-value="selectLanguage"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||
import { useL10n } from '@KTXC'
|
||||
|
||||
const integrationStore = useIntegrationStore()
|
||||
const { t } = useL10n('user_profile')
|
||||
|
||||
// Get all registered security panel components from modules
|
||||
const securityPanels = computed(() => {
|
||||
@@ -59,9 +61,11 @@ onMounted(async () => {
|
||||
class="mb-4"
|
||||
color="grey"
|
||||
/>
|
||||
<p class="text-h6 mb-2">No Security Settings Available</p>
|
||||
<p class="text-h6 mb-2">
|
||||
{{ t('security.emptyTitle', 'No Security Settings Available') }}
|
||||
</p>
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
No security modules are currently enabled or installed.
|
||||
{{ t('security.emptyDescription', 'No security modules are currently enabled or installed.') }}
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
+5
-1
@@ -5,17 +5,19 @@ const integrations: ModuleIntegrations = {
|
||||
{
|
||||
id: 'user_profile',
|
||||
label: 'Profile & Settings',
|
||||
l10n: 'nav.profileSettings',
|
||||
path: '/me',
|
||||
icon: 'mdi-account-outline',
|
||||
priority: 90,
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
// User personal settings
|
||||
user_settings_menu: [
|
||||
{
|
||||
id: 'profile',
|
||||
label: 'Profile',
|
||||
l10n: 'nav.profile',
|
||||
path: '/me/profile',
|
||||
icon: 'mdi-account-circle',
|
||||
priority: 10,
|
||||
@@ -23,6 +25,7 @@ const integrations: ModuleIntegrations = {
|
||||
{
|
||||
id: 'preferences',
|
||||
label: 'Preferences',
|
||||
l10n: 'nav.preferences',
|
||||
path: '/me/preferences',
|
||||
icon: 'mdi-tune',
|
||||
priority: 20,
|
||||
@@ -30,6 +33,7 @@ const integrations: ModuleIntegrations = {
|
||||
{
|
||||
id: 'security',
|
||||
label: 'Security',
|
||||
l10n: 'nav.security',
|
||||
path: '/me/security',
|
||||
icon: 'mdi-shield-lock',
|
||||
priority: 30,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import routes from '@/routes'
|
||||
import integrations from '@/integrations'
|
||||
|
||||
// Module ships translation catalogs at static/l10n/{locale}.json
|
||||
export const l10n = true
|
||||
|
||||
export { routes, integrations }
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ProfileAccount from '@/components/ProfileAccount.vue'
|
||||
import ProfileNotifications from '@/components/ProfileNotifications.vue'
|
||||
import ProfilePreferences from '@/components/ProfilePreferences.vue'
|
||||
import ProfileSecurity from '@/components/ProfileSecurity.vue'
|
||||
import { useL10n } from '@KTXC'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useL10n('user_profile')
|
||||
|
||||
const activeTab = ref(route.params.tab || 'account')
|
||||
|
||||
// tabs
|
||||
const tabs = [
|
||||
{ title: 'Account', icon: 'mdi-account-outline', tab: 'account' },
|
||||
{ title: 'Security', icon: 'mdi-lock-outline', tab: 'security' },
|
||||
{ title: 'Notifications', icon: 'mdi-bell-outline', tab: 'notifications' },
|
||||
]
|
||||
const tabs = computed(() => [
|
||||
{ title: t('tabs.account', 'Account'), icon: 'mdi-account-outline', tab: 'account' },
|
||||
{ title: t('tabs.security', 'Security'), icon: 'mdi-lock-outline', tab: 'security' },
|
||||
{ title: t('tabs.preferences', 'Preferences'), icon: 'mdi-tune', tab: 'preferences' },
|
||||
{ title: t('tabs.notifications', 'Notifications'), icon: 'mdi-bell-outline', tab: 'notifications' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PerfectScrollbar
|
||||
<div
|
||||
class="pa-4"
|
||||
style="height: calc(100vh - 64px);"
|
||||
:options="{ wheelPropagation: false }"
|
||||
>
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
@@ -61,6 +63,11 @@ const tabs = [
|
||||
<VWindowItem value="notifications">
|
||||
<ProfileNotifications />
|
||||
</VWindowItem>
|
||||
|
||||
<!-- Preferences -->
|
||||
<VWindowItem value="preferences">
|
||||
<ProfilePreferences />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
+2
-2
@@ -2,7 +2,7 @@ const routes = [
|
||||
{
|
||||
name: 'user_profile',
|
||||
path: '/me',
|
||||
component: () => import('@/views/ProfilePage.vue'),
|
||||
component: () => import('@/pages/Main.vue'),
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const routes = [
|
||||
{
|
||||
name: 'user_profile-tab',
|
||||
path: '/me/:tab',
|
||||
component: () => import('@/views/ProfilePage.vue'),
|
||||
component: () => import('@/pages/Main.vue'),
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('Basic Tests', () => {
|
||||
it('should perform basic assertion', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
it('should test array operations', () => {
|
||||
const array = ['foo', 'bar', 'baz']
|
||||
|
||||
expect(array).toHaveLength(3)
|
||||
expect(array).toContain('bar')
|
||||
expect(array[0]).toBe('foo')
|
||||
})
|
||||
|
||||
it('should test string operations', () => {
|
||||
const string = 'Hello, World!'
|
||||
|
||||
expect(string).toContain('World')
|
||||
expect(string.length).toBe(13)
|
||||
})
|
||||
|
||||
it('should test object operations', () => {
|
||||
const obj = { foo: 'bar', count: 42 }
|
||||
|
||||
expect(obj).toHaveProperty('foo')
|
||||
expect(obj.foo).toBe('bar')
|
||||
expect(obj.count).toBeGreaterThan(40)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig, configDefaults } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '../../src'),
|
||||
'@KTXC': path.resolve(__dirname, '../../../../core/src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('../../', import.meta.url)),
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'tests/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/dist/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -5,8 +5,6 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@KTXC/*": ["../../core/src/*"]
|
||||
|
||||
+26
-3
@@ -1,10 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, type PluginOption } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
// Copy translation catalogs (l10n/*.json, except the extractor-only
|
||||
// en.manual.json) into static/l10n/ so the core shell can fetch them at
|
||||
// /modules/{handle}/static/l10n/{locale}.json
|
||||
const copyL10nCatalogs = (): PluginOption => ({
|
||||
name: 'copy-l10n-catalogs',
|
||||
apply: 'build',
|
||||
closeBundle() {
|
||||
const sourceDir = path.resolve(__dirname, 'l10n')
|
||||
const outDir = path.resolve(__dirname, 'static/l10n')
|
||||
if (!fs.existsSync(sourceDir)) return
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
for (const file of fs.readdirSync(sourceDir)) {
|
||||
if (file.endsWith('.json') && !file.endsWith('.manual.json')) {
|
||||
fs.copyFileSync(path.join(sourceDir, file), path.join(outDir, file))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [vue(), copyL10nCatalogs()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
@@ -20,7 +40,10 @@ export default defineConfig({
|
||||
fileName: () => 'module.mjs',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'vue-router', 'pinia'],
|
||||
external: ['vue', 'vue-router', 'pinia', '@KTXC'],
|
||||
output: {
|
||||
paths: (id) => (id === '@KTXC' ? '/js/ktxc.mjs' : id),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user