1 Commits

Author SHA1 Message Date
Sebastian e9bb013381 Update dependency vite to v8 2026-05-15 12:50:12 +00:00
22 changed files with 491 additions and 3030 deletions
-42
View File
@@ -1,42 +0,0 @@
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
@@ -1,42 +0,0 @@
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
+2 -8
View File
@@ -25,17 +25,11 @@ jobs:
tools: composer:v2 tools: composer:v2
- name: Install Renovate - name: Install Renovate
run: | run: npm install -g renovate
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: | run: renovate ${{ gitea.repository }}
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
+4 -2
View File
@@ -12,10 +12,12 @@ node_modules/
/static/ /static/
# Backend development # Backend development
/vendor/
/lib/vendor/ /lib/vendor/
coverage/ coverage/
*.cache phpunit.xml.cache
.phpunit.result.cache
.php-cs-fixer.cache
.phpstan.cache
.phpactor/ .phpactor/
# Editors # Editors
Generated
-18
View File
@@ -1,18 +0,0 @@
{
"_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
@@ -1,84 +0,0 @@
{
"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
@@ -1,84 +0,0 @@
{
"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
@@ -1,8 +0,0 @@
{
"nav": {
"preferences": "Preferences",
"profile": "Profile",
"profileSettings": "Profile & Settings",
"security": "Security"
}
}
+379 -2486
View File
File diff suppressed because it is too large Load Diff
+4 -12
View File
@@ -11,17 +11,13 @@
"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": "^4.0.0", "pinia": "^3.0.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^5.0.0", "vue-router": "^4.5.1",
"vuetify": "^4.0.0" "vuetify": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -29,10 +25,6 @@
"@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": "^30.0.0",
"vitest": "^4.1.6"
} }
} }
+39 -41
View File
@@ -2,10 +2,8 @@
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)
@@ -136,27 +134,27 @@ const resetAvatar = () => {
} }
} }
const countries = computed(() => [ const countries = [
{ value: 'United States', title: t('account.countries.unitedStates', 'United States') }, 'United States',
{ value: 'United Kingdom', title: t('account.countries.unitedKingdom', 'United Kingdom') }, 'United Kingdom',
{ value: 'Canada', title: t('account.countries.canada', 'Canada') }, 'Canada',
{ value: 'Australia', title: t('account.countries.australia', 'Australia') }, 'Australia',
{ value: 'Germany', title: t('account.countries.germany', 'Germany') }, 'Germany',
{ value: 'France', title: t('account.countries.france', 'France') }, 'France',
{ value: 'India', title: t('account.countries.india', 'India') }, 'India',
{ value: 'China', title: t('account.countries.china', 'China') }, 'China',
{ value: 'Japan', title: t('account.countries.japan', 'Japan') }, 'Japan',
{ value: 'Brazil', title: t('account.countries.brazil', 'Brazil') }, 'Brazil',
{ value: 'Mexico', title: t('account.countries.mexico', 'Mexico') }, 'Mexico',
{ value: 'South Africa', title: t('account.countries.southAfrica', 'South Africa') }, 'South Africa',
]) ]
</script> </script>
<template> <template>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard :title="t('account.title', 'Account Details')"> <VCard title="Account Details">
<VCardText class="d-flex"> <VCardText class="d-flex">
<!-- 👉 Avatar --> <!-- 👉 Avatar -->
<VAvatar <VAvatar
@@ -174,7 +172,7 @@ const countries = computed(() => [
@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">{{ t('account.uploadPhoto', 'Upload new photo') }}</span> <span class="d-none d-sm-block">Upload new photo</span>
</VBtn> </VBtn>
<input <input
@@ -192,13 +190,13 @@ const countries = computed(() => [
variant="outlined" variant="outlined"
@click="resetAvatar" @click="resetAvatar"
> >
<span class="d-none d-sm-block">{{ t('actions.reset', 'Reset') }}</span> <span class="d-none d-sm-block">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">
{{ t('account.photoRequirements', 'Allowed JPG, GIF or PNG. Max size of 800K') }} Allowed JPG, GIF or PNG. Max size of 800K
</p> </p>
</form> </form>
</VCardText> </VCardText>
@@ -216,10 +214,10 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.name_given" v-model="accountDataLocal.name_given"
:placeholder="t('account.firstNamePlaceholder', 'John')" placeholder="John"
:label="t('account.firstName', 'First Name')" label="First Name"
:readonly="!isFieldEditable('name_given')" :readonly="!isFieldEditable('name_given')"
:hint="!isFieldEditable('name_given') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''" :hint="!isFieldEditable('name_given') ? 'Managed by authentication provider' : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -231,10 +229,10 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.name_family" v-model="accountDataLocal.name_family"
:placeholder="t('account.lastNamePlaceholder', 'Doe')" placeholder="Doe"
:label="t('account.lastName', 'Last Name')" label="Last Name"
:readonly="!isFieldEditable('name_family')" :readonly="!isFieldEditable('name_family')"
:hint="!isFieldEditable('name_family') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''" :hint="!isFieldEditable('name_family') ? 'Managed by authentication provider' : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -246,11 +244,11 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.email" v-model="accountDataLocal.email"
:label="t('account.email', 'E-mail')" label="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') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''" :hint="!isFieldEditable('email') ? 'Managed by authentication provider' : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -262,10 +260,10 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.phone" v-model="accountDataLocal.phone"
:label="t('account.phoneNumber', 'Phone Number')" label="Phone Number"
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
:readonly="!isFieldEditable('phone')" :readonly="!isFieldEditable('phone')"
:hint="!isFieldEditable('phone') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''" :hint="!isFieldEditable('phone') ? 'Managed by authentication provider' : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -277,8 +275,8 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.address" v-model="accountDataLocal.address"
:label="t('account.address', 'Address')" label="Address"
:placeholder="t('account.addressPlaceholder', '123 Main St')" placeholder="123 Main St"
/> />
</VCol> </VCol>
@@ -289,8 +287,8 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.city" v-model="accountDataLocal.city"
:label="t('account.city', 'City')" label="City"
:placeholder="t('account.cityPlaceholder', 'New York')" placeholder="New York"
/> />
</VCol> </VCol>
@@ -301,8 +299,8 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.state" v-model="accountDataLocal.state"
:label="t('account.stateOrProvince', 'State/Province')" label="State/Province"
:placeholder="t('account.statePlaceholder', 'State')" placeholder="State"
/> />
</VCol> </VCol>
@@ -313,7 +311,7 @@ const countries = computed(() => [
> >
<VTextField <VTextField
v-model="accountDataLocal.zip" v-model="accountDataLocal.zip"
:label="t('account.zipCode', 'Zip Code')" label="Zip Code"
placeholder="10001" placeholder="10001"
/> />
</VCol> </VCol>
@@ -325,9 +323,9 @@ const countries = computed(() => [
> >
<VSelect <VSelect
v-model="accountDataLocal.country" v-model="accountDataLocal.country"
:label="t('account.country', 'Country')" label="Country"
:items="countries" :items="countries"
:placeholder="t('account.selectCountry', 'Select Country')" placeholder="Select Country"
/> />
</VCol> </VCol>
@@ -341,7 +339,7 @@ const countries = computed(() => [
:loading="isSaving" :loading="isSaving"
color="primary" color="primary"
> >
{{ t('actions.saveChanges', 'Save changes') }} Save changes
</VBtn> </VBtn>
<VBtn <VBtn
@@ -350,7 +348,7 @@ const countries = computed(() => [
type="reset" type="reset"
@click.prevent="resetForm" @click.prevent="resetForm"
> >
{{ t('actions.reset', 'Reset') }} Reset
</VBtn> </VBtn>
</VCol> </VCol>
</VRow> </VRow>
+43 -36
View File
@@ -1,62 +1,69 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { ref } from 'vue'
import { useL10n } from '@KTXC'
const { t } = useL10n('user_profile') 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 recentDevices = ref([ const selectedNotification = ref('Only when I\'m online')
{ 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="t('notifications.title', 'Notifications')"> <VCard title="Notifications">
<VCardText> <VCardText>
{{ t('notifications.permissionDescription', 'We need permission from your browser to show notifications.') }} We need permission from your browser to show notifications.
<a href="javascript:void(0)">{{ t('notifications.requestPermission', 'Request Permission') }}</a> <a href="javascript:void(0)">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">
{{ t('notifications.columns.type', 'Type') }} Type
</th> </th>
<th scope="col"> <th scope="col">
{{ t('notifications.columns.email', 'EMAIL') }} EMAIL
</th> </th>
<th scope="col"> <th scope="col">
{{ t('notifications.columns.browser', 'BROWSER') }} BROWSER
</th> </th>
<th scope="col"> <th scope="col">
{{ t('notifications.columns.app', 'App') }} App
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="device in recentDevices" v-for="device in recentDevices"
:key="device.id" :key="device.type"
> >
<td> <td>
{{ notificationTypeLabels[device.id] }} {{ device.type }}
</td> </td>
<td> <td>
<VCheckbox v-model="device.email" /> <VCheckbox v-model="device.email" />
@@ -75,7 +82,7 @@ const selectedNotification = ref('online')
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<p class="text-base font-weight-medium"> <p class="text-base font-weight-medium">
{{ t('notifications.timingQuestion', 'When should we send you notifications?') }} When should we send you notifications?
</p> </p>
<VRow> <VRow>
@@ -86,21 +93,21 @@ const selectedNotification = ref('online')
<VSelect <VSelect
v-model="selectedNotification" v-model="selectedNotification"
mandatory mandatory
:items="notificationTimingOptions" :items="['Only when I\'m online', 'Anytime']"
/> />
</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">
{{ t('actions.saveChanges', 'Save changes') }} Save Changes
</VBtn> </VBtn>
<VBtn <VBtn
color="secondary" color="secondary"
variant="outlined" variant="outlined"
type="reset" type="reset"
> >
{{ t('actions.reset', 'Reset') }} Reset
</VBtn> </VBtn>
</div> </div>
</VForm> </VForm>
-45
View File
@@ -1,45 +0,0 @@
<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>
+2 -6
View File
@@ -1,10 +1,8 @@
<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(() => {
@@ -61,11 +59,9 @@ onMounted(async () => {
class="mb-4" class="mb-4"
color="grey" color="grey"
/> />
<p class="text-h6 mb-2"> <p class="text-h6 mb-2">No Security Settings Available</p>
{{ t('security.emptyTitle', 'No Security Settings Available') }}
</p>
<p class="text-body-2 text-medium-emphasis"> <p class="text-body-2 text-medium-emphasis">
{{ t('security.emptyDescription', 'No security modules are currently enabled or installed.') }} No security modules are currently enabled or installed.
</p> </p>
</VCardText> </VCardText>
</VCard> </VCard>
+1 -5
View File
@@ -5,19 +5,17 @@ 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,
}, },
], ],
// User personal settings // User personal settings
user_settings_menu: [ user_settings_menu: [
{ {
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,
@@ -25,7 +23,6 @@ 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,
@@ -33,7 +30,6 @@ 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,7 +1,4 @@
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 }
+2 -2
View File
@@ -2,7 +2,7 @@ const routes = [
{ {
name: 'user_profile', name: 'user_profile',
path: '/me', path: '/me',
component: () => import('@/pages/Main.vue'), component: () => import('@/views/ProfilePage.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('@/pages/Main.vue'), component: () => import('@/views/ProfilePage.vue'),
meta: { meta: {
requiresAuth: true requiresAuth: true
} }
@@ -1,29 +1,27 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { 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 = computed(() => [ const tabs = [
{ title: t('tabs.account', 'Account'), icon: 'mdi-account-outline', tab: 'account' }, { title: 'Account', icon: 'mdi-account-outline', tab: 'account' },
{ title: t('tabs.security', 'Security'), icon: 'mdi-lock-outline', tab: 'security' }, { title: 'Security', icon: 'mdi-lock-outline', tab: 'security' },
{ title: t('tabs.preferences', 'Preferences'), icon: 'mdi-tune', tab: 'preferences' }, { title: 'Notifications', icon: 'mdi-bell-outline', tab: 'notifications' },
{ title: t('tabs.notifications', 'Notifications'), icon: 'mdi-bell-outline', tab: 'notifications' }, ]
])
</script> </script>
<template> <template>
<div <PerfectScrollbar
class="pa-4" class="pa-4"
style="height: calc(100vh - 64px);"
:options="{ wheelPropagation: false }"
> >
<VTabs <VTabs
v-model="activeTab" v-model="activeTab"
@@ -63,11 +61,6 @@ const tabs = computed(() => [
<VWindowItem value="notifications"> <VWindowItem value="notifications">
<ProfileNotifications /> <ProfileNotifications />
</VWindowItem> </VWindowItem>
<!-- Preferences -->
<VWindowItem value="preferences">
<ProfilePreferences />
</VWindowItem>
</VWindow> </VWindow>
</div> </PerfectScrollbar>
</template> </template>
-30
View File
@@ -1,30 +0,0 @@
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
@@ -1,33 +0,0 @@
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,6 +5,8 @@
"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/*"]
+3 -26
View File
@@ -1,30 +1,10 @@
import { defineConfig, type PluginOption } from 'vite' import { defineConfig } 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(), copyL10nCatalogs()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@@ -40,10 +20,7 @@ export default defineConfig({
fileName: () => 'module.mjs', fileName: () => 'module.mjs',
}, },
rollupOptions: { rollupOptions: {
external: ['vue', 'vue-router', 'pinia', '@KTXC'], external: ['vue', 'vue-router', 'pinia'],
output: {
paths: (id) => (id === '@KTXC' ? '/js/ktxc.mjs' : id),
},
}, },
}, },
}) })