added user profile

This commit is contained in:
2025-12-29 08:37:02 +01:00
parent c5b3034a8a
commit 5d2b2308f2
9 changed files with 599 additions and 1 deletions
+26
View File
@@ -34,6 +34,27 @@
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
/* Additional semantic colors */
--color-primary: #007bff;
--color-primary-dark: #0056b3;
--color-bg-secondary: #f8f9fa;
--color-text-primary: #333;
--color-text-secondary: #495057;
/* Spacing variables */
--padding-xs: 4px;
--padding-sm: 8px;
--padding-md: 16px;
--padding-lg: 24px;
--margin-xs: 4px;
--margin-sm: 8px;
--margin-md: 16px;
--margin-lg: 24px;
/* Other utilities */
--border-radius: 6px;
--box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@media (prefers-color-scheme: dark) {
@@ -47,6 +68,11 @@
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
/* Dark mode adjustments for additional colors */
--color-bg-secondary: #2a2a2a;
--color-text-primary: #e9ecef;
--color-text-secondary: #adb5bd;
}
}
+28 -1
View File
@@ -17,7 +17,10 @@
<router-link to="/maintenance" active-class="active">Maintenance</router-link>
</li>
</ul>
<LanguageSwitcher />
<div class="menu-right">
<LanguageSwitcher />
<router-link v-if="isLoggedIn" to="/profile" active-class="active" class="profile-link">{{ $t('menu.Profile') }}</router-link>
</div>
</nav>
</template>
@@ -80,4 +83,28 @@ export default {
color: white;
cursor: pointer;
}
.menu-right {
display: flex;
align-items: center;
gap: 1rem;
}
.profile-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
transition: background-color 0.2s;
}
.profile-link:hover {
background-color: rgba(255, 255, 255, 0.1);
text-decoration: none;
}
.profile-link.active {
background-color: rgba(255, 255, 255, 0.2);
font-weight: bold;
}
</style>
+33
View File
@@ -36,6 +36,7 @@ export default {
Notes:'Notizen',
Campagne:'Kampagne',
DeleteChar:'Figur löschen',
Profile:'Profil',
//Character:'Charakter',
},
Equipment:'Ausrüstung',
@@ -437,5 +438,37 @@ export default {
generating: 'PDF wird generiert...',
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.'
},
profile: {
title: 'Benutzerprofil',
loading: 'Lade Profil...',
userInfo: 'Benutzerinformationen',
username: 'Benutzername',
currentEmail: 'Aktuelle E-Mail',
changeEmail: 'E-Mail ändern',
newEmail: 'Neue E-Mail',
emailPlaceholder: 'ihre.email@example.com',
updateEmail: 'E-Mail aktualisieren',
changePassword: 'Passwort ändern',
currentPassword: 'Aktuelles Passwort',
currentPasswordPlaceholder: 'Ihr aktuelles Passwort',
newPassword: 'Neues Passwort',
newPasswordPlaceholder: 'Mindestens 6 Zeichen',
confirmPassword: 'Passwort bestätigen',
confirmPasswordPlaceholder: 'Neues Passwort wiederholen',
updatePassword: 'Passwort aktualisieren',
updating: 'Aktualisiere...',
loadError: 'Fehler beim Laden des Profils',
emailRequired: 'E-Mail-Adresse ist erforderlich',
emailUnchanged: 'Die neue E-Mail ist identisch mit der aktuellen',
emailUpdateSuccess: 'E-Mail erfolgreich aktualisiert',
emailUpdateError: 'Fehler beim Aktualisieren der E-Mail',
emailInUse: 'Diese E-Mail-Adresse wird bereits verwendet',
allFieldsRequired: 'Alle Felder sind erforderlich',
passwordTooShort: 'Das Passwort muss mindestens 6 Zeichen lang sein',
passwordMismatch: 'Die Passwörter stimmen nicht überein',
passwordUpdateSuccess: 'Passwort erfolgreich aktualisiert',
passwordUpdateError: 'Fehler beim Aktualisieren des Passworts',
currentPasswordIncorrect: 'Das aktuelle Passwort ist falsch'
}
}
+33
View File
@@ -36,6 +36,7 @@ export default {
Notes:'Notes',
Campagne:'Campagne',
DeleteChar:'Delete Character',
Profile:'Profile',
//Character:'Charakter',
},
Equipment:'Equipment',
@@ -435,5 +436,37 @@ export default {
generating: 'Generating PDF...',
pleaseWait: 'Please wait, this may take a moment',
popupBlocked: 'Popup was blocked. Please allow popups for this site.'
},
profile: {
title: 'User Profile',
loading: 'Loading profile...',
userInfo: 'User Information',
username: 'Username',
currentEmail: 'Current Email',
changeEmail: 'Change Email',
newEmail: 'New Email',
emailPlaceholder: 'your.email@example.com',
updateEmail: 'Update Email',
changePassword: 'Change Password',
currentPassword: 'Current Password',
currentPasswordPlaceholder: 'Your current password',
newPassword: 'New Password',
newPasswordPlaceholder: 'At least 6 characters',
confirmPassword: 'Confirm Password',
confirmPasswordPlaceholder: 'Repeat new password',
updatePassword: 'Update Password',
updating: 'Updating...',
loadError: 'Failed to load profile',
emailRequired: 'Email address is required',
emailUnchanged: 'The new email is identical to the current one',
emailUpdateSuccess: 'Email updated successfully',
emailUpdateError: 'Failed to update email',
emailInUse: 'This email address is already in use',
allFieldsRequired: 'All fields are required',
passwordTooShort: 'Password must be at least 6 characters long',
passwordMismatch: 'Passwords do not match',
passwordUpdateSuccess: 'Password updated successfully',
passwordUpdateError: 'Failed to update password',
currentPasswordIncorrect: 'Current password is incorrect'
}
}
+2
View File
@@ -8,6 +8,7 @@ import DashboardView from "../views/DashboardView.vue";
import AusruestungView from "../views/AusruestungView.vue";
import MaintenanceView from "../views/MaintenanceView.vue";
import FileUploadPage from "../views/FileUploadPage.vue";
import UserProfileView from "../views/UserProfileView.vue";
import CharacterDetails from "@/components/CharacterDetails.vue";
import CharacterCreation from "@/components/CharacterCreation.vue";
@@ -21,6 +22,7 @@ const routes = [
{ path: "/forgot-password", name: "ForgotPassword", component: ForgotPasswordView },
{ path: "/reset-password", name: "ResetPassword", component: ResetPasswordView },
{ path: "/dashboard", name: "Dashboard", component: DashboardView, meta: { requiresAuth: true } },
{ path: "/profile", name: "UserProfile", component: UserProfileView, meta: { requiresAuth: true } },
{ path: "/ausruestung/:characterId", name: "Ausruestung", component: AusruestungView, meta: { requiresAuth: true } },
{ path: "/maintenance", name: "Maintenance", component: MaintenanceView, meta: { requiresAuth: true } },
{ path: "/upload", name: "FileUpload", component: FileUploadPage },
+325
View File
@@ -0,0 +1,325 @@
<template>
<div class="user-profile">
<div class="container">
<h1>{{ $t('profile.title') }}</h1>
<div v-if="loading" class="loading">
{{ $t('profile.loading') }}
</div>
<div v-else class="profile-sections">
<!-- User Information Section -->
<div class="profile-section">
<h2>{{ $t('profile.userInfo') }}</h2>
<div class="info-row">
<label>{{ $t('profile.username') }}:</label>
<span>{{ userProfile.username }}</span>
</div>
<div class="info-row">
<label>{{ $t('profile.currentEmail') }}:</label>
<span>{{ userProfile.email }}</span>
</div>
</div>
<!-- Change Email Section -->
<div class="profile-section">
<h2>{{ $t('profile.changeEmail') }}</h2>
<form @submit.prevent="updateEmail" class="profile-form">
<div class="form-group">
<label for="newEmail">{{ $t('profile.newEmail') }}:</label>
<input
type="email"
id="newEmail"
v-model="emailForm.newEmail"
:placeholder="$t('profile.emailPlaceholder')"
required
/>
</div>
<button type="submit" :disabled="isUpdating" class="btn-primary">
<span v-if="!isUpdating">{{ $t('profile.updateEmail') }}</span>
<span v-else>{{ $t('profile.updating') }}</span>
</button>
</form>
</div>
<!-- Change Password Section -->
<div class="profile-section">
<h2>{{ $t('profile.changePassword') }}</h2>
<form @submit.prevent="updatePassword" class="profile-form">
<div class="form-group">
<label for="currentPassword">{{ $t('profile.currentPassword') }}:</label>
<input
type="password"
id="currentPassword"
v-model="passwordForm.currentPassword"
:placeholder="$t('profile.currentPasswordPlaceholder')"
required
/>
</div>
<div class="form-group">
<label for="newPassword">{{ $t('profile.newPassword') }}:</label>
<input
type="password"
id="newPassword"
v-model="passwordForm.newPassword"
:placeholder="$t('profile.newPasswordPlaceholder')"
minlength="6"
required
/>
</div>
<div class="form-group">
<label for="confirmPassword">{{ $t('profile.confirmPassword') }}:</label>
<input
type="password"
id="confirmPassword"
v-model="passwordForm.confirmPassword"
:placeholder="$t('profile.confirmPasswordPlaceholder')"
minlength="6"
required
/>
</div>
<button type="submit" :disabled="isUpdating" class="btn-primary">
<span v-if="!isUpdating">{{ $t('profile.updatePassword') }}</span>
<span v-else>{{ $t('profile.updating') }}</span>
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.user-profile {
padding: var(--padding-lg);
margin-top: 2%;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: var(--color-primary);
margin-bottom: var(--margin-lg);
text-align: center;
}
.loading {
text-align: center;
padding: var(--padding-lg);
color: var(--color-text-secondary);
}
.profile-sections {
display: flex;
flex-direction: column;
gap: var(--margin-lg);
}
.profile-section {
background-color: var(--color-bg-secondary);
padding: var(--padding-md);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-light);
}
.profile-section h2 {
color: var(--color-primary);
margin-bottom: var(--margin-md);
font-size: 1.2em;
}
.info-row {
display: flex;
padding: var(--padding-sm) 0;
border-bottom: 1px solid var(--color-border);
}
.info-row:last-child {
border-bottom: none;
}
.info-row label {
font-weight: bold;
width: 200px;
color: var(--color-text-secondary);
}
.info-row span {
flex: 1;
color: var(--color-text-primary);
}
.profile-form {
display: flex;
flex-direction: column;
gap: var(--margin-md);
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--margin-xs);
}
.form-group label {
font-weight: bold;
color: var(--color-text-secondary);
}
.form-group input {
padding: var(--padding-sm);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
font-size: 1em;
}
.form-group input:focus {
outline: none;
border-color: var(--color-primary);
}
.btn-primary {
background-color: var(--color-primary);
color: white;
padding: var(--padding-sm) var(--padding-md);
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 1em;
align-self: flex-start;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
<script>
import API from '../utils/api'
export default {
name: 'UserProfileView',
data() {
return {
loading: true,
isUpdating: false,
userProfile: {
username: '',
email: ''
},
emailForm: {
newEmail: ''
},
passwordForm: {
currentPassword: '',
newPassword: '',
confirmPassword: ''
}
}
},
async created() {
await this.loadProfile()
},
methods: {
async loadProfile() {
this.loading = true
try {
const response = await API.get('/api/user/profile')
this.userProfile = response.data
this.emailForm.newEmail = this.userProfile.email
} catch (error) {
console.error('Failed to load profile:', error)
alert(this.$t('profile.loadError') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.loading = false
}
},
async updateEmail() {
if (!this.emailForm.newEmail) {
alert(this.$t('profile.emailRequired'))
return
}
if (this.emailForm.newEmail === this.userProfile.email) {
alert(this.$t('profile.emailUnchanged'))
return
}
this.isUpdating = true
try {
const response = await API.put('/api/user/email', {
email: this.emailForm.newEmail
})
this.userProfile.email = response.data.email
alert(this.$t('profile.emailUpdateSuccess'))
} catch (error) {
console.error('Failed to update email:', error)
let errorMsg = this.$t('profile.emailUpdateError')
if (error.response?.data?.error) {
if (error.response.data.error.includes('already in use')) {
errorMsg = this.$t('profile.emailInUse')
} else {
errorMsg += ': ' + error.response.data.error
}
}
alert(errorMsg)
} finally {
this.isUpdating = false
}
},
async updatePassword() {
if (!this.passwordForm.currentPassword || !this.passwordForm.newPassword || !this.passwordForm.confirmPassword) {
alert(this.$t('profile.allFieldsRequired'))
return
}
if (this.passwordForm.newPassword.length < 6) {
alert(this.$t('profile.passwordTooShort'))
return
}
if (this.passwordForm.newPassword !== this.passwordForm.confirmPassword) {
alert(this.$t('profile.passwordMismatch'))
return
}
this.isUpdating = true
try {
await API.put('/api/user/password', {
current_password: this.passwordForm.currentPassword,
new_password: this.passwordForm.newPassword
})
alert(this.$t('profile.passwordUpdateSuccess'))
// Clear password fields
this.passwordForm.currentPassword = ''
this.passwordForm.newPassword = ''
this.passwordForm.confirmPassword = ''
} catch (error) {
console.error('Failed to update password:', error)
let errorMsg = this.$t('profile.passwordUpdateError')
if (error.response?.data?.error) {
if (error.response.data.error.includes('incorrect')) {
errorMsg = this.$t('profile.currentPasswordIncorrect')
} else {
errorMsg += ': ' + error.response.data.error
}
}
alert(errorMsg)
} finally {
this.isUpdating = false
}
}
}
}
</script>