51 Commits

Author SHA1 Message Date
Sebastian 492d053ffa Update dependency vue to v3.5.41
JS Unit Tests / test (pull_request) Successful in 1m5s
Build Test / test (pull_request) Successful in 1m8s
2026-08-06 03:04:22 +00:00
Sebastian 374c83f746 Merge pull request 'Update dependency pinia to v4' (#40) from renovate/pinia-4.x into main
Reviewed-on: #40
2026-07-25 23:53:54 +00:00
Sebastian 47d6a2f0c8 Update dependency pinia to v4
Build Test / test (pull_request) Successful in 55s
JS Unit Tests / test (pull_request) Successful in 1m0s
2026-07-25 03:02:11 +00:00
Sebastian 58458c8755 Merge pull request 'fix: regenerate package-lock.json to fix corrupted vue dependency tree' (#39) from fix/lockfile-vue-tree into main
Reviewed-on: #39
2026-07-24 05:06:43 +00:00
Sebastian 0339958262 fix: regenerate package-lock.json to fix corrupted vue dependency tree
JS Unit Tests / test (pull_request) Successful in 37s
Build Test / test (pull_request) Successful in 42s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-24 01:04:21 -04:00
Sebastian cf5dc65779 Merge pull request 'chore: implement js test suites' (#38) from chore/js-test-suites into main
Reviewed-on: #38
2026-07-24 04:20:24 +00:00
Sebastian 16126669ff chore: implement js test suites
Build Test / test (pull_request) Successful in 34s
JS Unit Tests / test (pull_request) Successful in 34s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-24 00:13:52 -04:00
Sebastian 288f893f7e Merge pull request 'Update dependency vue to v3.5.40' (#36) from renovate/vue-monorepo into main
Reviewed-on: #36
2026-07-24 03:15:34 +00:00
Sebastian 9689db4c5a Merge pull request 'Update dependency vuetify to v4.1.6' (#37) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #37
2026-07-24 03:15:28 +00:00
Sebastian 6d1ca0da63 Update dependency vuetify to v4.1.6 2026-07-24 03:12:43 +00:00
Sebastian 45008afe91 Update dependency vue to v3.5.40 2026-07-24 03:12:37 +00:00
Sebastian 7db74c435e Merge pull request 'Update dependency vuetify to v4.1.5' (#32) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #32
2026-07-24 02:40:22 +00:00
Sebastian 41c6ffc743 Merge pull request 'Update dependency vue-tsc to v3.3.8' (#31) from renovate/vue-language-tools-monorepo into main
Reviewed-on: #31
2026-07-24 02:40:16 +00:00
Sebastian d1c44eb82f Merge pull request 'Update dependency vite to v8.1.5' (#30) from renovate/vite-8.x-lockfile into main
Reviewed-on: #30
2026-07-24 02:40:10 +00:00
Sebastian 032756a658 Merge pull request 'Update dependency typescript to v7' (#33) from renovate/typescript-7.x into main
Reviewed-on: #33
2026-07-24 02:39:54 +00:00
Sebastian 0f2da28b12 Merge pull request 'Update dependency @vitejs/plugin-vue to v6.0.8' (#34) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #34
2026-07-24 02:39:47 +00:00
Sebastian c6d7598772 Merge pull request 'chore: implement php test suites' (#35) from chore/php83-test-suites into main
Reviewed-on: #35
2026-07-24 02:39:40 +00:00
Sebastian 4451a7a590 chore: implement php test suites
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-23 22:20:36 -04:00
Sebastian de2c96a39c Update dependency vue-tsc to v3.3.8 2026-07-23 03:05:31 +00:00
Sebastian a370c9d74e Update dependency vite to v8.1.5 2026-07-23 03:05:29 +00:00
Sebastian 3251939454 Update dependency vuetify to v4.1.5 2026-07-18 03:02:36 +00:00
Sebastian 4e6c529df2 Update dependency @vitejs/plugin-vue to v6.0.8 2026-07-18 03:02:27 +00:00
Sebastian a97556b9ae refactor: rename main entry point
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-14 17:40:11 -04:00
Sebastian feed0a38ab Update dependency typescript to v7 2026-07-11 03:03:06 +00:00
Sebastian bf9e7c9538 Track Composer dependencies
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-09 22:56:21 -04:00
Sebastian da89952aaf fix(user-profile): use shell page scrolling
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-09 21:20:30 -04:00
Sebastian 92f35bf73d fix(renovate): install renovate in runner temp
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-09 20:12:08 -04:00
Sebastian f48a4b2800 fix: tsconfig
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-07 18:20:36 -04:00
Sebastian 0099a63fad feat: localizations
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-07 18:16:54 -04:00
Sebastian f4fb8277d2 Merge pull request 'Update dependency vue to v3.5.39' (#27) from renovate/vue-monorepo into main
Reviewed-on: #27
2026-06-28 14:39:47 +00:00
Sebastian fb56271b8e Merge pull request 'Update dependency vue-tsc to v3.3.5' (#28) from renovate/vue-language-tools-monorepo into main
Reviewed-on: #28
2026-06-28 14:39:40 +00:00
Sebastian 0b26340b76 Merge pull request 'Update dependency vite to v8.1.0' (#29) from renovate/vite-8.x-lockfile into main
Reviewed-on: #29
2026-06-28 14:39:32 +00:00
Sebastian 442fbb648e Update dependency vite to v8.1.0 2026-06-28 03:05:17 +00:00
Sebastian 7d8173f437 Update dependency vue-tsc to v3.3.5 2026-06-27 03:05:31 +00:00
Sebastian f4f3b2f814 Update dependency vue to v3.5.39 2026-06-27 03:05:29 +00:00
Sebastian 89fd27e9fd Merge pull request 'Update dependency vuetify to v4.1.2' (#25) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #25
2026-06-20 22:59:50 +00:00
Sebastian 6bfde7df59 Merge pull request 'Update vue monorepo' (#26) from renovate/vue-monorepo into main
Reviewed-on: #26
2026-06-20 22:59:42 +00:00
Sebastian 89203083ac Update vue monorepo 2026-06-20 03:05:41 +00:00
Sebastian e5ba9a7ee1 Update dependency vuetify to v4.1.2 2026-06-20 03:05:39 +00:00
Sebastian 4563006b82 Merge pull request 'Update dependency vite to v8.0.16' (#21) from renovate/vite-8.x-lockfile into main
Reviewed-on: #21
2026-06-16 17:46:40 +00:00
Sebastian 7a5731c0e2 Merge pull request 'Update vue-language-tools monorepo to v3.3.4' (#23) from renovate/vue-language-tools-monorepo into main
Reviewed-on: #23
2026-06-16 17:46:29 +00:00
Sebastian cd476aea24 Merge pull request 'Update dependency vuetify to v4.1.1' (#24) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #24
2026-06-16 17:46:21 +00:00
Sebastian 4116b874fe Update dependency vuetify to v4.1.1 2026-06-09 03:02:06 +00:00
Sebastian 1b7de87479 Update vue-language-tools monorepo to v3.3.4 2026-06-09 03:02:02 +00:00
Sebastian cab7547f9a Update dependency vite to v8.0.16 2026-06-09 03:01:59 +00:00
Sebastian 20bc88087c Merge pull request 'Update vue-language-tools monorepo to v3.3.3' (#20) from renovate/vue-language-tools-monorepo into main
Renovate / renovate (push) Failing after 1m40s
Reviewed-on: #20
2026-06-06 04:10:46 +00:00
Sebastian ac0b65d0ea Merge pull request 'Update dependency vue-router to v5.1.0' (#22) from renovate/vue-router-5.x-lockfile into main
Reviewed-on: #22
2026-06-06 04:10:17 +00:00
Sebastian 347746188a Merge pull request 'Update vue monorepo to v3.5.35' (#19) from renovate/vue-monorepo into main
Reviewed-on: #19
2026-06-06 04:10:10 +00:00
Sebastian 3334f5d507 Update dependency vue-router to v5.1.0 2026-06-05 03:02:51 +00:00
Sebastian 632c57e453 Update dependency vue-tsc to v3.3.3 2026-06-01 03:03:02 +00:00
Sebastian d135b8ef79 Update dependency vue to v3.5.35 2026-06-01 03:02:59 +00:00
22 changed files with 2647 additions and 551 deletions
+42
View File
@@ -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
+42
View File
@@ -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
+8 -2
View File
@@ -25,11 +25,17 @@ jobs:
tools: composer:v2 tools: composer:v2
- name: Install Renovate - 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 - name: Run Renovate
env: env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_PLATFORM: gitea RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1 RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
run: renovate ${{ gitea.repository }} run: |
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
+2 -4
View File
@@ -12,12 +12,10 @@ node_modules/
/static/ /static/
# Backend development # Backend development
/vendor/
/lib/vendor/ /lib/vendor/
coverage/ coverage/
phpunit.xml.cache *.cache
.phpunit.result.cache
.php-cs-fixer.cache
.phpstan.cache
.phpactor/ .phpactor/
# Editors # Editors
Generated
+18
View File
@@ -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"
}
+84
View File
@@ -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"
}
}
+84
View File
@@ -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"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"nav": {
"preferences": "Preferences",
"profile": "Profile",
"profileSettings": "Profile & Settings",
"security": "Security"
}
}
+2104 -440
View File
File diff suppressed because it is too large Load Diff
+11 -3
View File
@@ -11,11 +11,15 @@
"dev": "vite build --mode development --config vite.config.ts", "dev": "vite build --mode development --config vite.config.ts",
"watch": "vite build --mode development --watch --config vite.config.ts", "watch": "vite build --mode development --watch --config vite.config.ts",
"typecheck": "vue-tsc --noEmit", "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": { "dependencies": {
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"pinia": "^3.0.0", "pinia": "^4.0.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^5.0.0", "vue-router": "^5.0.0",
"vuetify": "^4.0.0" "vuetify": "^4.0.0"
@@ -25,6 +29,10 @@
"@vue/tsconfig": "^0.9.0", "@vue/tsconfig": "^0.9.0",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"vite": "^8.0.0", "vite": "^8.0.0",
"vue-tsc": "^3.0.5" "vue-tsc": "^3.0.5",
"@vitest/coverage-v8": "^4.1.6",
"@vue/test-utils": "^2.4.10",
"jsdom": "^29.1.1",
"vitest": "^4.1.6"
} }
} }
+41 -39
View File
@@ -2,8 +2,10 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@KTXC/stores/userStore' import { useUserStore } from '@KTXC/stores/userStore'
import { userService } from '@KTXC/services/user/userService' import { userService } from '@KTXC/services/user/userService'
import { useL10n } from '@KTXC'
const userStore = useUserStore() const userStore = useUserStore()
const { t } = useL10n('user_profile')
const refInputEl = ref<HTMLElement>() const refInputEl = ref<HTMLElement>()
const isSaving = ref(false) const isSaving = ref(false)
@@ -134,27 +136,27 @@ const resetAvatar = () => {
} }
} }
const countries = [ const countries = computed(() => [
'United States', { value: 'United States', title: t('account.countries.unitedStates', 'United States') },
'United Kingdom', { value: 'United Kingdom', title: t('account.countries.unitedKingdom', 'United Kingdom') },
'Canada', { value: 'Canada', title: t('account.countries.canada', 'Canada') },
'Australia', { value: 'Australia', title: t('account.countries.australia', 'Australia') },
'Germany', { value: 'Germany', title: t('account.countries.germany', 'Germany') },
'France', { value: 'France', title: t('account.countries.france', 'France') },
'India', { value: 'India', title: t('account.countries.india', 'India') },
'China', { value: 'China', title: t('account.countries.china', 'China') },
'Japan', { value: 'Japan', title: t('account.countries.japan', 'Japan') },
'Brazil', { value: 'Brazil', title: t('account.countries.brazil', 'Brazil') },
'Mexico', { value: 'Mexico', title: t('account.countries.mexico', 'Mexico') },
'South Africa', { value: 'South Africa', title: t('account.countries.southAfrica', 'South Africa') },
] ])
</script> </script>
<template> <template>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard title="Account Details"> <VCard :title="t('account.title', 'Account Details')">
<VCardText class="d-flex"> <VCardText class="d-flex">
<!-- 👉 Avatar --> <!-- 👉 Avatar -->
<VAvatar <VAvatar
@@ -172,7 +174,7 @@ const countries = [
@click="refInputEl?.click()" @click="refInputEl?.click()"
> >
<VIcon icon="mdi-cloud-upload" class="d-sm-none" /> <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> </VBtn>
<input <input
@@ -190,13 +192,13 @@ const countries = [
variant="outlined" variant="outlined"
@click="resetAvatar" @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" /> <VIcon icon="mdi-refresh" class="d-sm-none" />
</VBtn> </VBtn>
</div> </div>
<p class="text-body-1 mb-0"> <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> </p>
</form> </form>
</VCardText> </VCardText>
@@ -214,10 +216,10 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.name_given" v-model="accountDataLocal.name_given"
placeholder="John" :placeholder="t('account.firstNamePlaceholder', 'John')"
label="First Name" :label="t('account.firstName', 'First Name')"
:readonly="!isFieldEditable('name_given')" :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 persistent-hint
/> />
</VCol> </VCol>
@@ -229,10 +231,10 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.name_family" v-model="accountDataLocal.name_family"
placeholder="Doe" :placeholder="t('account.lastNamePlaceholder', 'Doe')"
label="Last Name" :label="t('account.lastName', 'Last Name')"
:readonly="!isFieldEditable('name_family')" :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 persistent-hint
/> />
</VCol> </VCol>
@@ -244,11 +246,11 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.email" v-model="accountDataLocal.email"
label="E-mail" :label="t('account.email', 'E-mail')"
placeholder="john.doe@example.com" placeholder="john.doe@example.com"
type="email" type="email"
:readonly="!isFieldEditable('email')" :readonly="!isFieldEditable('email')"
:hint="!isFieldEditable('email') ? 'Managed by authentication provider' : ''" :hint="!isFieldEditable('email') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -260,10 +262,10 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.phone" v-model="accountDataLocal.phone"
label="Phone Number" :label="t('account.phoneNumber', 'Phone Number')"
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
:readonly="!isFieldEditable('phone')" :readonly="!isFieldEditable('phone')"
:hint="!isFieldEditable('phone') ? 'Managed by authentication provider' : ''" :hint="!isFieldEditable('phone') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -275,8 +277,8 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.address" v-model="accountDataLocal.address"
label="Address" :label="t('account.address', 'Address')"
placeholder="123 Main St" :placeholder="t('account.addressPlaceholder', '123 Main St')"
/> />
</VCol> </VCol>
@@ -287,8 +289,8 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.city" v-model="accountDataLocal.city"
label="City" :label="t('account.city', 'City')"
placeholder="New York" :placeholder="t('account.cityPlaceholder', 'New York')"
/> />
</VCol> </VCol>
@@ -299,8 +301,8 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.state" v-model="accountDataLocal.state"
label="State/Province" :label="t('account.stateOrProvince', 'State/Province')"
placeholder="State" :placeholder="t('account.statePlaceholder', 'State')"
/> />
</VCol> </VCol>
@@ -311,7 +313,7 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.zip" v-model="accountDataLocal.zip"
label="Zip Code" :label="t('account.zipCode', 'Zip Code')"
placeholder="10001" placeholder="10001"
/> />
</VCol> </VCol>
@@ -323,9 +325,9 @@ const countries = [
> >
<VSelect <VSelect
v-model="accountDataLocal.country" v-model="accountDataLocal.country"
label="Country" :label="t('account.country', 'Country')"
:items="countries" :items="countries"
placeholder="Select Country" :placeholder="t('account.selectCountry', 'Select Country')"
/> />
</VCol> </VCol>
@@ -339,7 +341,7 @@ const countries = [
:loading="isSaving" :loading="isSaving"
color="primary" color="primary"
> >
Save changes {{ t('actions.saveChanges', 'Save changes') }}
</VBtn> </VBtn>
<VBtn <VBtn
@@ -348,7 +350,7 @@ const countries = [
type="reset" type="reset"
@click.prevent="resetForm" @click.prevent="resetForm"
> >
Reset {{ t('actions.reset', 'Reset') }}
</VBtn> </VBtn>
</VCol> </VCol>
</VRow> </VRow>
+36 -43
View File
@@ -1,69 +1,62 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
import { useL10n } from '@KTXC'
const recentDevices = ref( const { t } = useL10n('user_profile')
[
{
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 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> </script>
<template> <template>
<VCard title="Notifications"> <VCard :title="t('notifications.title', 'Notifications')">
<VCardText> <VCardText>
We need permission from your browser to show notifications. {{ t('notifications.permissionDescription', 'We need permission from your browser to show notifications.') }}
<a href="javascript:void(0)">Request Permission</a> <a href="javascript:void(0)">{{ t('notifications.requestPermission', 'Request Permission') }}</a>
</VCardText> </VCardText>
<VTable class="text-no-wrap"> <VTable class="text-no-wrap">
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
Type {{ t('notifications.columns.type', 'Type') }}
</th> </th>
<th scope="col"> <th scope="col">
EMAIL {{ t('notifications.columns.email', 'EMAIL') }}
</th> </th>
<th scope="col"> <th scope="col">
BROWSER {{ t('notifications.columns.browser', 'BROWSER') }}
</th> </th>
<th scope="col"> <th scope="col">
App {{ t('notifications.columns.app', 'App') }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="device in recentDevices" v-for="device in recentDevices"
:key="device.type" :key="device.id"
> >
<td> <td>
{{ device.type }} {{ notificationTypeLabels[device.id] }}
</td> </td>
<td> <td>
<VCheckbox v-model="device.email" /> <VCheckbox v-model="device.email" />
@@ -82,7 +75,7 @@ const selectedNotification = ref('Only when I\'m online')
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<p class="text-base font-weight-medium"> <p class="text-base font-weight-medium">
When should we send you notifications? {{ t('notifications.timingQuestion', 'When should we send you notifications?') }}
</p> </p>
<VRow> <VRow>
@@ -93,21 +86,21 @@ const selectedNotification = ref('Only when I\'m online')
<VSelect <VSelect
v-model="selectedNotification" v-model="selectedNotification"
mandatory mandatory
:items="['Only when I\'m online', 'Anytime']" :items="notificationTimingOptions"
/> />
</VCol> </VCol>
</VRow> </VRow>
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit"> <VBtn type="submit">
Save Changes {{ t('actions.saveChanges', 'Save changes') }}
</VBtn> </VBtn>
<VBtn <VBtn
color="secondary" color="secondary"
variant="outlined" variant="outlined"
type="reset" type="reset"
> >
Reset {{ t('actions.reset', 'Reset') }}
</VBtn> </VBtn>
</div> </div>
</VForm> </VForm>
+45
View File
@@ -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>
+6 -2
View File
@@ -1,8 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useIntegrationStore } from '@KTXC/stores/integrationStore' import { useIntegrationStore } from '@KTXC/stores/integrationStore'
import { useL10n } from '@KTXC'
const integrationStore = useIntegrationStore() const integrationStore = useIntegrationStore()
const { t } = useL10n('user_profile')
// Get all registered security panel components from modules // Get all registered security panel components from modules
const securityPanels = computed(() => { const securityPanels = computed(() => {
@@ -59,9 +61,11 @@ onMounted(async () => {
class="mb-4" class="mb-4"
color="grey" 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"> <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> </p>
</VCardText> </VCardText>
</VCard> </VCard>
+4
View File
@@ -5,6 +5,7 @@ const integrations: ModuleIntegrations = {
{ {
id: 'user_profile', id: 'user_profile',
label: 'Profile & Settings', label: 'Profile & Settings',
l10n: 'nav.profileSettings',
path: '/me', path: '/me',
icon: 'mdi-account-outline', icon: 'mdi-account-outline',
priority: 90, priority: 90,
@@ -16,6 +17,7 @@ const integrations: ModuleIntegrations = {
{ {
id: 'profile', id: 'profile',
label: 'Profile', label: 'Profile',
l10n: 'nav.profile',
path: '/me/profile', path: '/me/profile',
icon: 'mdi-account-circle', icon: 'mdi-account-circle',
priority: 10, priority: 10,
@@ -23,6 +25,7 @@ const integrations: ModuleIntegrations = {
{ {
id: 'preferences', id: 'preferences',
label: 'Preferences', label: 'Preferences',
l10n: 'nav.preferences',
path: '/me/preferences', path: '/me/preferences',
icon: 'mdi-tune', icon: 'mdi-tune',
priority: 20, priority: 20,
@@ -30,6 +33,7 @@ const integrations: ModuleIntegrations = {
{ {
id: 'security', id: 'security',
label: 'Security', label: 'Security',
l10n: 'nav.security',
path: '/me/security', path: '/me/security',
icon: 'mdi-shield-lock', icon: 'mdi-shield-lock',
priority: 30, priority: 30,
+3
View File
@@ -1,4 +1,7 @@
import routes from '@/routes' import routes from '@/routes'
import integrations from '@/integrations' import integrations from '@/integrations'
// Module ships translation catalogs at static/l10n/{locale}.json
export const l10n = true
export { routes, integrations } export { routes, integrations }
@@ -1,27 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import ProfileAccount from '@/components/ProfileAccount.vue' import ProfileAccount from '@/components/ProfileAccount.vue'
import ProfileNotifications from '@/components/ProfileNotifications.vue' import ProfileNotifications from '@/components/ProfileNotifications.vue'
import ProfilePreferences from '@/components/ProfilePreferences.vue'
import ProfileSecurity from '@/components/ProfileSecurity.vue' import ProfileSecurity from '@/components/ProfileSecurity.vue'
import { useL10n } from '@KTXC'
const route = useRoute() const route = useRoute()
const { t } = useL10n('user_profile')
const activeTab = ref(route.params.tab || 'account') const activeTab = ref(route.params.tab || 'account')
// tabs // tabs
const tabs = [ const tabs = computed(() => [
{ title: 'Account', icon: 'mdi-account-outline', tab: 'account' }, { title: t('tabs.account', 'Account'), icon: 'mdi-account-outline', tab: 'account' },
{ title: 'Security', icon: 'mdi-lock-outline', tab: 'security' }, { title: t('tabs.security', 'Security'), icon: 'mdi-lock-outline', tab: 'security' },
{ title: 'Notifications', icon: 'mdi-bell-outline', tab: 'notifications' }, { title: t('tabs.preferences', 'Preferences'), icon: 'mdi-tune', tab: 'preferences' },
] { title: t('tabs.notifications', 'Notifications'), icon: 'mdi-bell-outline', tab: 'notifications' },
])
</script> </script>
<template> <template>
<PerfectScrollbar <div
class="pa-4" class="pa-4"
style="height: calc(100vh - 64px);"
:options="{ wheelPropagation: false }"
> >
<VTabs <VTabs
v-model="activeTab" v-model="activeTab"
@@ -61,6 +63,11 @@ const tabs = [
<VWindowItem value="notifications"> <VWindowItem value="notifications">
<ProfileNotifications /> <ProfileNotifications />
</VWindowItem> </VWindowItem>
<!-- Preferences -->
<VWindowItem value="preferences">
<ProfilePreferences />
</VWindowItem>
</VWindow> </VWindow>
</PerfectScrollbar> </div>
</template> </template>
+2 -2
View File
@@ -2,7 +2,7 @@ const routes = [
{ {
name: 'user_profile', name: 'user_profile',
path: '/me', path: '/me',
component: () => import('@/views/ProfilePage.vue'), component: () => import('@/pages/Main.vue'),
meta: { meta: {
requiresAuth: true requiresAuth: true
} }
@@ -10,7 +10,7 @@ const routes = [
{ {
name: 'user_profile-tab', name: 'user_profile-tab',
path: '/me/:tab', path: '/me/:tab',
component: () => import('@/views/ProfilePage.vue'), component: () => import('@/pages/Main.vue'),
meta: { meta: {
requiresAuth: true requiresAuth: true
} }
+30
View File
@@ -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)
})
})
+33
View File
@@ -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/**',
],
},
},
})
-2
View File
@@ -5,8 +5,6 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@KTXC/*": ["../../core/src/*"] "@KTXC/*": ["../../core/src/*"]
+26 -3
View File
@@ -1,10 +1,30 @@
import { defineConfig } from 'vite' import { defineConfig, type PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path' 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/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue(), copyL10nCatalogs()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@@ -20,7 +40,10 @@ export default defineConfig({
fileName: () => 'module.mjs', fileName: () => 'module.mjs',
}, },
rollupOptions: { rollupOptions: {
external: ['vue', 'vue-router', 'pinia'], external: ['vue', 'vue-router', 'pinia', '@KTXC'],
output: {
paths: (id) => (id === '@KTXC' ? '/js/ktxc.mjs' : id),
},
}, },
}, },
}) })