Refactor backend CORS settings to include default origins and improve error handling in chat services: Introduce dynamic CORS origin handling, enhance RabbitMQ message sending with fallback mechanisms, and update WebSocket service to manage pending messages. Update UI components for better accessibility and responsiveness, including adjustments to dialog and navigation elements. Enhance styling for improved user experience across various components.
This commit is contained in:
@@ -3,9 +3,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import HomeNoLoginView from './home/NoLoginView.vue';
|
||||
import HomeLoggedInView from './home/LoggedInView.vue';
|
||||
|
||||
const HomeNoLoginView = defineAsyncComponent(() => import('./home/NoLoginView.vue'));
|
||||
const HomeLoggedInView = defineAsyncComponent(() => import('./home/LoggedInView.vue'));
|
||||
|
||||
export default {
|
||||
name: 'HomeView',
|
||||
@@ -20,4 +22,4 @@ export default {
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,36 @@
|
||||
|
||||
<!-- Match3 Levels Tab -->
|
||||
<div v-if="activeTab === 'match3-levels'" class="match3-admin">
|
||||
<section class="workflow-hero surface-card">
|
||||
<div>
|
||||
<span class="workflow-hero__eyebrow">Arbeitsfluss</span>
|
||||
<h2>{{ $t('admin.match3.title') }}</h2>
|
||||
<p>Erst Level waehlen, dann Spielfeld und Ziele anpassen und erst am Ende speichern.</p>
|
||||
</div>
|
||||
<div class="workflow-hero__meta">
|
||||
<span class="workflow-pill">{{ currentModeLabel }}</span>
|
||||
<span class="workflow-pill">{{ levels.length }} Level</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workflow-grid">
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">1</span>
|
||||
<h3>Level waehlen</h3>
|
||||
<p>Bestehendes Level oeffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">2</span>
|
||||
<h3>Spielfeld bauen</h3>
|
||||
<p>Groesse, Zuege, Kacheln und Layout zuerst festziehen, bevor Ziele folgen.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">3</span>
|
||||
<h3>Ziele speichern</h3>
|
||||
<p>Objectives nur dann scharf stellen, wenn Grunddaten und Board bereits stimmen.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="section-header">
|
||||
<h2>{{ $t('admin.match3.title') }}</h2>
|
||||
</div>
|
||||
@@ -31,13 +61,38 @@
|
||||
{{ $t('admin.match3.levelFormat', { number: level.order, name: level.name }) }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-secondary level-select-action" @click="createLevel">
|
||||
{{ $t('admin.match3.newLevel') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="level-selection__hint">
|
||||
{{ isCreatingLevel ? 'Du erstellst gerade ein neues Level.' : 'Du bearbeitest ein bestehendes Level mit allen verbundenen Objectives.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="admin-summary-grid">
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Modus</span>
|
||||
<strong>{{ currentModeLabel }}</strong>
|
||||
<p>{{ selectedLevel ? selectedLevel.name : 'Neue Vorlage mit leerem Spielfeld' }}</p>
|
||||
</article>
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Spielfeld</span>
|
||||
<strong>{{ levelForm.boardWidth }} x {{ levelForm.boardHeight }}</strong>
|
||||
<p>{{ levelForm.moveLimit }} Zuege, {{ levelForm.tileTypes.length }} aktive Tile-Typen.</p>
|
||||
</article>
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Objectives</span>
|
||||
<strong>{{ objectiveCount }}</strong>
|
||||
<p>{{ objectiveCount ? 'Ziele vorhanden und bearbeitbar.' : 'Noch keine Zieldefinition hinterlegt.' }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- Level Details -->
|
||||
<div v-if="selectedLevelId !== 'new' && selectedLevel" class="level-details">
|
||||
<div class="details-header">
|
||||
<h3>{{ selectedLevel.name }}</h3>
|
||||
<p>Bestehendes Level anpassen, ohne den Kontext des aktuellen Spielflusses zu verlieren.</p>
|
||||
</div>
|
||||
<div class="details-content">
|
||||
<div class="form-group">
|
||||
@@ -185,7 +240,7 @@
|
||||
<button type="button" class="btn btn-danger" @click="deleteSelectedLevel">
|
||||
{{ $t('admin.match3.delete') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="saveLevel">
|
||||
<button type="button" class="btn btn-primary" :disabled="!isLevelFormValid" @click="saveLevel">
|
||||
{{ $t('admin.match3.update') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -539,7 +594,7 @@
|
||||
<button type="button" class="btn btn-secondary" @click="cancelEdit">
|
||||
{{ $t('admin.match3.cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<button type="submit" class="btn btn-primary" :disabled="!isLevelFormValid">
|
||||
{{ $t('admin.match3.create') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -553,6 +608,7 @@
|
||||
<script>
|
||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||
import apiClient from '../../utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminMinigamesView',
|
||||
@@ -593,10 +649,29 @@ export default {
|
||||
gridTemplateRows: `repeat(${this.levelForm.boardHeight}, 1fr)`
|
||||
};
|
||||
},
|
||||
|
||||
isCreatingLevel() {
|
||||
return this.selectedLevelId === 'new';
|
||||
},
|
||||
selectedLevel() {
|
||||
if (this.selectedLevelId === 'new') return null;
|
||||
return this.levels.find(l => l.id === this.selectedLevelId);
|
||||
},
|
||||
objectiveCount() {
|
||||
return this.levelForm.objectives?.length || 0;
|
||||
},
|
||||
currentModeLabel() {
|
||||
return this.isCreatingLevel ? 'Neues Level' : 'Level bearbeiten';
|
||||
},
|
||||
isLevelFormValid() {
|
||||
return Boolean(
|
||||
this.levelForm.name?.trim() &&
|
||||
this.levelForm.description?.trim() &&
|
||||
this.levelForm.boardWidth >= 3 &&
|
||||
this.levelForm.boardHeight >= 3 &&
|
||||
this.levelForm.moveLimit >= 5 &&
|
||||
this.levelForm.order >= 1 &&
|
||||
this.levelForm.tileTypes?.length
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -730,20 +805,14 @@ export default {
|
||||
},
|
||||
|
||||
setTileType(index, tileType) {
|
||||
console.log('setTileType called with:', index, tileType);
|
||||
if (tileType === 'o') {
|
||||
// Leer
|
||||
this.boardMatrix[index] = { active: false, tileType: 'o', index: index };
|
||||
} else if (tileType === 'r') {
|
||||
// Zufällig
|
||||
this.boardMatrix[index] = { active: true, tileType: 'r', index: index };
|
||||
console.log('Set random tile at index:', index, this.boardMatrix[index]);
|
||||
} else {
|
||||
// Spezifischer Tile-Typ
|
||||
this.boardMatrix[index] = { active: true, tileType: tileType, index: index };
|
||||
}
|
||||
this.selectedCellIndex = null; // Auswahl aufheben
|
||||
console.log('Board matrix after update:', this.boardMatrix);
|
||||
this.selectedCellIndex = null;
|
||||
},
|
||||
|
||||
// Mapping für Tile-Typen zu Zeichen
|
||||
@@ -785,7 +854,6 @@ export default {
|
||||
objectives: []
|
||||
};
|
||||
this.updateBoardMatrix();
|
||||
console.log('Bearbeitung abgebrochen, Objectives zurückgesetzt:', this.levelForm.objectives);
|
||||
},
|
||||
|
||||
updateBoardMatrix() {
|
||||
@@ -905,6 +973,7 @@ export default {
|
||||
...this.levelForm,
|
||||
boardLayout: this.generateBoardLayout()
|
||||
};
|
||||
const wasCreating = this.selectedLevelId === 'new';
|
||||
|
||||
let savedLevel;
|
||||
if (this.selectedLevelId !== 'new') {
|
||||
@@ -939,9 +1008,10 @@ export default {
|
||||
this.selectedLevelId = 'new';
|
||||
this.selectedCellIndex = null;
|
||||
this.loadLevels();
|
||||
showSuccess(this, wasCreating ? 'Level wurde erstellt.' : 'Level wurde aktualisiert.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern des Levels:', error);
|
||||
alert('Fehler beim Speichern des Levels');
|
||||
showError(this, 'Fehler beim Speichern des Levels');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -950,8 +1020,10 @@ export default {
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
||||
this.loadLevels();
|
||||
showSuccess(this, 'Level wurde geloescht.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Levels:', error);
|
||||
showError(this, 'Fehler beim Loeschen des Levels');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1025,6 +1097,94 @@ export default {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.workflow-hero {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 22px 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workflow-hero h2 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.workflow-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.workflow-hero__eyebrow,
|
||||
.admin-summary-card__label {
|
||||
display: inline-flex;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workflow-hero__meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workflow-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
color: #8a5411;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workflow-grid,
|
||||
.admin-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workflow-card,
|
||||
.admin-summary-card {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.workflow-card__step {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
color: #8a5411;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workflow-card h3,
|
||||
.admin-summary-card strong {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.admin-summary-card strong {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.workflow-card p,
|
||||
.admin-summary-card p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
@@ -1055,6 +1215,8 @@ export default {
|
||||
.level-dropdown {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.level-select {
|
||||
@@ -1072,6 +1234,15 @@ export default {
|
||||
border-color: #F9A22C;
|
||||
}
|
||||
|
||||
.level-select-action {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.level-selection__hint {
|
||||
margin: 12px 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Level Details & Form */
|
||||
.level-details,
|
||||
.level-form {
|
||||
@@ -1096,6 +1267,11 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.details-header p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1479,6 +1655,16 @@ export default {
|
||||
.match3-admin {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.workflow-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.workflow-grid,
|
||||
.admin-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
<template>
|
||||
<div class="admin-users">
|
||||
<h1>{{ $t('navigation.m-administration.useradministration') }}</h1>
|
||||
<section class="admin-users__hero surface-card">
|
||||
<span class="admin-users__eyebrow">Administration</span>
|
||||
<h1>{{ $t('navigation.m-administration.useradministration') }}</h1>
|
||||
<p>Benutzer suchen, Kerndaten anpassen und Sperrstatus direkt im System pflegen.</p>
|
||||
</section>
|
||||
|
||||
<AdminUserSearch @select="select" />
|
||||
<section class="admin-users__search surface-card">
|
||||
<AdminUserSearch @select="select" />
|
||||
</section>
|
||||
|
||||
<div v-if="selected" class="edit">
|
||||
<h2>{{ selected.username }}</h2>
|
||||
<label>
|
||||
{{ $t('admin.user.name') }}
|
||||
<section v-if="selected" class="edit surface-card">
|
||||
<div class="edit__header">
|
||||
<h2>{{ selected.username }}</h2>
|
||||
<span class="edit__badge">{{ form.active ? 'Aktiv' : 'Gesperrt' }}</span>
|
||||
</div>
|
||||
|
||||
<label class="edit__field">
|
||||
<span>{{ $t('admin.user.name') }}</span>
|
||||
<input v-model="form.username" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('admin.user.blocked') }}
|
||||
|
||||
<label class="edit__toggle">
|
||||
<input type="checkbox" :checked="!form.active" @change="toggleBlocked($event)" />
|
||||
<span>{{ $t('admin.user.blocked') }}</span>
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="save">{{ $t('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -57,12 +69,105 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-users { padding: 20px; }
|
||||
.results table { width: 100%; border-collapse: collapse; }
|
||||
.results th, .results td { border: 1px solid #ddd; padding: 8px; }
|
||||
.edit { margin-top: 16px; display: grid; gap: 10px; max-width: 480px; }
|
||||
.actions { display: flex; gap: 8px; }
|
||||
button { cursor: pointer; }
|
||||
.admin-users {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.admin-users__hero,
|
||||
.admin-users__search,
|
||||
.edit {
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.admin-users__hero,
|
||||
.admin-users__search,
|
||||
.edit {
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.admin-users__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users__hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.edit {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.edit__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.edit__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.edit__field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit__field span {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.edit__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.admin-users__hero,
|
||||
.admin-users__search,
|
||||
.edit {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.edit__header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
<!-- Benutzer-Suche -->
|
||||
<div class="search-section">
|
||||
<label>{{ $t('admin.falukant.edituser.username') }}: <input type="text" v-model="user.username" @keyup.enter="searchUser" /></label>
|
||||
<label>{{ $t('admin.falukant.edituser.characterName') }}: <input type="text" v-model="user.characterName" @keyup.enter="searchUser" /></label>
|
||||
<button @click="searchUser">{{ $t('admin.falukant.edituser.search') }}</button>
|
||||
<label class="form-field">{{ $t('admin.falukant.edituser.username') }} <input type="text" v-model="user.username" @keyup.enter="searchUser" /></label>
|
||||
<label class="form-field">{{ $t('admin.falukant.edituser.characterName') }} <input type="text" v-model="user.characterName" @keyup.enter="searchUser" /></label>
|
||||
<button @click="searchUser" :disabled="!canSearch">{{ $t('admin.falukant.edituser.search') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer-Liste -->
|
||||
@@ -40,8 +40,8 @@
|
||||
</select>
|
||||
</label>
|
||||
<div class="action-buttons">
|
||||
<button @click="saveUser">{{ $t('common.save') }}</button>
|
||||
<button @click="deleteUser">{{ $t('common.delete') }}</button>
|
||||
<button @click="saveUser" :disabled="!hasUserChanges">{{ $t('common.save') }}</button>
|
||||
<button @click="deleteUser" class="button-secondary">{{ $t('common.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,6 +122,7 @@ import { mapState } from 'vuex';
|
||||
import { mapActions } from 'vuex';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminFalukantEditUserView',
|
||||
@@ -162,6 +163,15 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState('falukant', ['user']),
|
||||
canSearch() {
|
||||
return this.user.username.trim().length > 0 || this.user.characterName.trim().length > 0;
|
||||
},
|
||||
hasUserChanges() {
|
||||
if (!this.editableUser || !this.originalUser) return false;
|
||||
return this.editableUser.falukantData[0].money != this.originalUser.falukantData[0].money
|
||||
|| this.editableUser.falukantData[0].character.title_of_nobility != this.originalUser.falukantData[0].character.title_of_nobility
|
||||
|| this.originalAge != this.age;
|
||||
},
|
||||
availableStockTypes() {
|
||||
if (!this.newStock.branchId || !this.stockTypes.length) {
|
||||
return this.stockTypes;
|
||||
@@ -191,6 +201,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async searchUser() {
|
||||
if (!this.canSearch) {
|
||||
showError(this, 'Bitte Benutzername oder Charaktername eingeben.');
|
||||
return;
|
||||
}
|
||||
const userResult = await apiClient.post('/api/admin/falukant/searchuser', {
|
||||
userName: this.user.username,
|
||||
characterName: this.user.characterName
|
||||
@@ -221,9 +235,9 @@ export default {
|
||||
}
|
||||
try {
|
||||
await apiClient.post(`/api/admin/falukant/edituser`, dataToChange);
|
||||
this.$root.$refs.messageDialog.open('tr:admin.falukant.edituser.success');
|
||||
showSuccess(this, 'tr:admin.falukant.edituser.success');
|
||||
} catch (error) {
|
||||
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.error');
|
||||
showApiError(this, error, 'tr:admin.falukant.edituser.error');
|
||||
}
|
||||
},
|
||||
async deleteUser() {
|
||||
@@ -245,7 +259,7 @@ export default {
|
||||
this.userBranches = branchesResult.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading user branches:', error);
|
||||
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.errorLoadingBranches');
|
||||
showApiError(this, error, 'tr:admin.falukant.edituser.errorLoadingBranches');
|
||||
} finally {
|
||||
this.loading.branches = false;
|
||||
}
|
||||
@@ -255,7 +269,7 @@ export default {
|
||||
await apiClient.put(`/api/admin/falukant/stock/${stock.id}`, {
|
||||
quantity: stock.quantity
|
||||
});
|
||||
this.$root.$refs.messageDialog.open('tr:admin.falukant.edituser.stockUpdated');
|
||||
showSuccess(this, 'tr:admin.falukant.edituser.stockUpdated');
|
||||
} catch (error) {
|
||||
console.error('Error updating stock:', error);
|
||||
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.errorUpdatingStock');
|
||||
@@ -675,4 +689,4 @@ export default {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<template>
|
||||
<div class="blog-list">
|
||||
<h1>Blogs</h1>
|
||||
<div class="toolbar">
|
||||
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
|
||||
</div>
|
||||
<div v-if="loading">Laden…</div>
|
||||
<div v-else>
|
||||
<div v-if="!blogs.length">Keine Blogs gefunden.</div>
|
||||
<ul>
|
||||
<li v-for="b in blogs" :key="b.id">
|
||||
<router-link :to="blogUrl(b)">{{ b.title }}</router-link>
|
||||
<small> – {{ b.owner?.username }}</small>
|
||||
</li>
|
||||
</ul>
|
||||
<section class="blog-list__hero surface-card">
|
||||
<div>
|
||||
<span class="blog-list__kicker">Community-Blogs</span>
|
||||
<h1>Blogs</h1>
|
||||
<p>Artikel, Projektstaende und persoenliche Einblicke aus der YourPart-Community.</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="loading" class="blog-list__state surface-card">Laden…</div>
|
||||
<div v-else-if="!blogs.length" class="blog-list__state surface-card">Keine Blogs gefunden.</div>
|
||||
<div v-else class="blog-grid">
|
||||
<article v-for="b in blogs" :key="b.id" class="blog-card surface-card">
|
||||
<div class="blog-card__meta">von {{ b.owner?.username || 'Unbekannt' }}</div>
|
||||
<h2><router-link :to="blogUrl(b)">{{ b.title }}</router-link></h2>
|
||||
<p>{{ blogExcerpt(b) }}</p>
|
||||
<router-link class="blog-card__link" :to="blogUrl(b)">Zum Blog</router-link>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,6 +38,90 @@ export default {
|
||||
const slug = createBlogSlug(blog?.owner?.username, blog?.title);
|
||||
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
|
||||
},
|
||||
blogExcerpt(blog) {
|
||||
const source = blog?.description || 'Oeffentliche Eintraege, Gedanken und Projektstaende aus der Community.';
|
||||
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.blog-list {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.blog-list__hero {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.blog-list__kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.blog-list__hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.blog-list__state {
|
||||
padding: 26px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.blog-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.blog-card__meta {
|
||||
margin-bottom: 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.blog-card h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.blog-card p {
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.blog-card__link {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.blog-list__hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
<template>
|
||||
<div class="blog-view">
|
||||
<div v-if="loading">Laden…</div>
|
||||
<div v-else>
|
||||
<h1>{{ blog.title }}</h1>
|
||||
<p v-if="blog.description">{{ blog.description }}</p>
|
||||
<div class="meta">von {{ blog.owner?.username }}</div>
|
||||
<div v-if="$store.getters.isLoggedIn" class="actions">
|
||||
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">Bearbeiten</router-link>
|
||||
</div>
|
||||
<div class="posts">
|
||||
<h2>{{ $t('blog.posts') }}</h2>
|
||||
<div v-if="!items.length">{{ $t('blog.noPosts') }}</div>
|
||||
<article v-for="p in items" :key="p.id" class="post">
|
||||
<h3>{{ p.title }}</h3>
|
||||
<div class="content" v-html="sanitize(p.content)" />
|
||||
</article>
|
||||
<div class="pagination" v-if="total > pageSize">
|
||||
<div v-if="loading" class="blog-view__state surface-card">Laden…</div>
|
||||
<div v-else-if="blog" class="blog-layout">
|
||||
<section class="blog-hero surface-card">
|
||||
<div>
|
||||
<div class="meta">von {{ blog.owner?.username }}</div>
|
||||
<h1>{{ blog.title }}</h1>
|
||||
<p v-if="blog.description" class="blog-description">{{ blog.description }}</p>
|
||||
</div>
|
||||
<div v-if="$store.getters.isLoggedIn" class="actions">
|
||||
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">Bearbeiten</router-link>
|
||||
</div>
|
||||
</section>
|
||||
<div class="blog-content">
|
||||
<section class="posts surface-card">
|
||||
<div class="posts__header">
|
||||
<h2>{{ $t('blog.posts') }}</h2>
|
||||
<span class="posts__count">{{ total }} Eintraege</span>
|
||||
</div>
|
||||
<div v-if="!items.length" class="blog-view__state">Keine Eintraege vorhanden.</div>
|
||||
<article v-for="p in items" :key="p.id" class="post">
|
||||
<h3>{{ p.title }}</h3>
|
||||
<div class="content" v-html="sanitize(p.content)" />
|
||||
</article>
|
||||
<div class="pagination" v-if="total > pageSize">
|
||||
<button :disabled="page===1" @click="go(page-1)">«</button>
|
||||
<span>{{ page }} / {{ pages }}</span>
|
||||
<button :disabled="page===pages" @click="go(page+1)">»</button>
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="isOwner" class="post-editor surface-card">
|
||||
<h3>{{ $t('blog.newPost') }}</h3>
|
||||
<form @submit.prevent="addPost">
|
||||
<input v-model="newPost.title" :placeholder="$t('blog.title')" required />
|
||||
<RichTextEditor v-model="newPost.content" :blog-id="blog.id" />
|
||||
<button class="btn" type="submit">{{ $t('blog.publish') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isOwner" class="post-editor">
|
||||
<h3>{{ $t('blog.newPost') }}</h3>
|
||||
<form @submit.prevent="addPost">
|
||||
<input v-model="newPost.title" :placeholder="$t('blog.title')" required />
|
||||
<RichTextEditor v-model="newPost.content" :blog-id="blog.id" />
|
||||
<button class="btn" type="submit">{{ $t('blog.publish') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -169,12 +178,109 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.blog-view {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.blog-layout {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.blog-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 26px;
|
||||
}
|
||||
|
||||
.blog-description {
|
||||
margin: 0;
|
||||
max-width: 70ch;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-bottom: 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.blog-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(280px, 0.95fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.posts,
|
||||
.post-editor {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.posts__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.posts__count {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.post + .post {
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.blog-view__state {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.editbutton {
|
||||
border: 1px solid #000;
|
||||
background-color: #f9a22c;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.5em;
|
||||
margin-bottom: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.blog-hero,
|
||||
.blog-content {
|
||||
grid-template-columns: 1fr;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.blog-hero {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.posts,
|
||||
.post-editor {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
<div class="contenthidden">
|
||||
<StatusBar ref="statusBar" />
|
||||
<div class="contentscroll">
|
||||
<h2>{{ $t('falukant.branch.title') }}</h2>
|
||||
<div class="falukant-branch">
|
||||
<section class="branch-hero surface-card">
|
||||
<div>
|
||||
<span class="branch-kicker">Niederlassung</span>
|
||||
<h2>{{ $t('falukant.branch.title') }}</h2>
|
||||
<p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerflaeche.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BranchSelection
|
||||
:branches="branches"
|
||||
@@ -308,6 +315,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1110,8 +1118,49 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
.falukant-branch {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.branch-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(248, 162, 43, 0.16), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 224, 0.98) 100%);
|
||||
}
|
||||
|
||||
.branch-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.branch-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.branch-tab-content {
|
||||
margin-top: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 252, 247, 0.86);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.branch-tab-pane {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.send-all-vehicles {
|
||||
@@ -1161,11 +1210,12 @@ h2 {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
background: rgba(255,255,255,0.98);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-lg);
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.send-vehicle-form {
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
<StatusBar />
|
||||
<div class="contentscroll family-layout">
|
||||
<div class="family-content">
|
||||
<h2>{{ $t('falukant.family.title') }}</h2>
|
||||
<section class="family-hero surface-card">
|
||||
<div>
|
||||
<span class="family-kicker">Familie</span>
|
||||
<h2>{{ $t('falukant.family.title') }}</h2>
|
||||
<p>Beziehungen, Kinder und familiäre Entwicklung in einer eigenen Spielweltansicht.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="spouse-section">
|
||||
<h3>{{ $t('falukant.family.spouse.title') }}</h3>
|
||||
@@ -36,7 +42,7 @@
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-inner" :style="{
|
||||
width: relationships[0].progress + '%',
|
||||
width: normalizeWooingProgress(relationships[0].progress) + '%',
|
||||
backgroundColor: progressColor(relationships[0].progress)
|
||||
}"></div>
|
||||
</div>
|
||||
@@ -200,6 +206,8 @@ import Character3D from '@/components/Character3D.vue'
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const WOOING_PROGRESS_TARGET = 70
|
||||
|
||||
export default {
|
||||
name: 'FamilyView',
|
||||
components: {
|
||||
@@ -342,6 +350,8 @@ export default {
|
||||
},
|
||||
|
||||
async cancelWooing() {
|
||||
const confirmed = window.confirm(this.$t('falukant.family.spouse.wooing.cancelConfirm'));
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.post('/api/falukant/family/cancel-wooing');
|
||||
await this.loadFamilyData();
|
||||
@@ -409,11 +419,16 @@ export default {
|
||||
},
|
||||
|
||||
progressColor(p) {
|
||||
const pct = Math.max(0, Math.min(100, p)) / 100;
|
||||
const pct = this.normalizeWooingProgress(p) / 100;
|
||||
const red = Math.round(255 * (1 - pct));
|
||||
const green = Math.round(255 * pct);
|
||||
return `rgb(${red}, ${green}, 0)`;
|
||||
},
|
||||
normalizeWooingProgress(p) {
|
||||
const raw = Number(p) || 0
|
||||
const normalized = (raw / WOOING_PROGRESS_TARGET) * 100
|
||||
return Math.max(0, Math.min(100, normalized))
|
||||
},
|
||||
|
||||
jumpToPartyForm() {
|
||||
this.$router.push({
|
||||
@@ -469,7 +484,33 @@ export default {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
padding-top: 24px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.family-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(248, 162, 43, 0.16), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 224, 0.98) 100%);
|
||||
}
|
||||
|
||||
.family-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.family-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.self-character-3d {
|
||||
@@ -483,15 +524,20 @@ export default {
|
||||
|
||||
.family-content {
|
||||
flex: 1;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.spouse-section,
|
||||
.children-section,
|
||||
.lovers-section {
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
margin: 12px 0;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
background: rgba(255, 252, 247, 0.86);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.relationship-container {
|
||||
@@ -513,8 +559,8 @@ export default {
|
||||
.partner-character-3d {
|
||||
width: 200px;
|
||||
height: 280px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: #fdf1db;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -537,8 +583,8 @@ export default {
|
||||
.child-character-3d {
|
||||
width: 200px;
|
||||
height: 280px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: #fdf1db;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -597,10 +643,6 @@ export default {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.relationship>table,
|
||||
.relationship>ul {
|
||||
display: inline-block;
|
||||
@@ -648,4 +690,11 @@ h2 {
|
||||
.set-heir-button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
</style>
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.relationship-row,
|
||||
.children-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="falukant-overview">
|
||||
<StatusBar />
|
||||
<h2>{{ $t('falukant.overview.title') }}</h2>
|
||||
<section class="falukant-hero surface-card">
|
||||
<div>
|
||||
<span class="falukant-kicker">Falukant</span>
|
||||
<h2>{{ $t('falukant.overview.title') }}</h2>
|
||||
<p>Dein Stand in Wirtschaft, Familie und Besitz in einer verdichteten Uebersicht.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="falukantUser?.character" class="falukant-summary-grid">
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Niederlassungen</span>
|
||||
<strong>{{ branchCount }}</strong>
|
||||
<p>Direkter Zugriff auf deine wichtigsten Geschaeftsstandorte.</p>
|
||||
</article>
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Produktionen aktiv</span>
|
||||
<strong>{{ productionCount }}</strong>
|
||||
<p>Laufende Produktionen, die zeitnah Abschluss oder Kontrolle brauchen.</p>
|
||||
</article>
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Lagerpositionen</span>
|
||||
<strong>{{ stockEntryCount }}</strong>
|
||||
<p>Verdichteter Blick auf Warenbestand ueber alle Regionen.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section v-if="falukantUser?.character" class="falukant-routine-grid">
|
||||
<article
|
||||
v-for="action in routineActions"
|
||||
:key="action.title"
|
||||
class="routine-card surface-card"
|
||||
>
|
||||
<span class="routine-card__eyebrow">{{ action.kicker }}</span>
|
||||
<h3>{{ action.title }}</h3>
|
||||
<p>{{ action.description }}</p>
|
||||
<button type="button" :class="action.secondary ? 'button-secondary' : ''" @click="openRoute(action.route)">
|
||||
{{ action.cta }}
|
||||
</button>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- Erben-Auswahl wenn kein Charakter vorhanden -->
|
||||
<div v-if="!falukantUser?.character" class="heir-selection-container">
|
||||
@@ -136,6 +175,7 @@
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import Character3D from '@/components/Character3D.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
const AVATAR_POSITIONS = {
|
||||
@@ -233,6 +273,50 @@ export default {
|
||||
const m = this.falukantUser?.money;
|
||||
return typeof m === 'string' ? parseFloat(m) : m;
|
||||
},
|
||||
branchCount() {
|
||||
return this.falukantUser?.branches?.length || 0;
|
||||
},
|
||||
productionCount() {
|
||||
return this.productions.length;
|
||||
},
|
||||
stockEntryCount() {
|
||||
return this.allStock.length;
|
||||
},
|
||||
routineActions() {
|
||||
return [
|
||||
{
|
||||
kicker: 'Routine',
|
||||
title: 'Niederlassung oeffnen',
|
||||
description: 'Die schnellste Route zu Produktion, Lager, Verkauf und Transport.',
|
||||
cta: 'Zu den Betrieben',
|
||||
route: 'BranchView',
|
||||
},
|
||||
{
|
||||
kicker: 'Ueberblick',
|
||||
title: 'Finanzen pruefen',
|
||||
description: 'Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.',
|
||||
cta: 'Geldhistorie',
|
||||
route: 'MoneyHistoryView',
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
kicker: 'Charakter',
|
||||
title: 'Familie und Nachfolge',
|
||||
description: 'Wichtige persoenliche Entscheidungen und Haushaltsstatus gesammelt.',
|
||||
cta: 'Familie oeffnen',
|
||||
route: 'FalukantFamily',
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
kicker: 'Besitz',
|
||||
title: 'Haus und Umfeld',
|
||||
description: 'Wohnsitz und alltaeglicher Status als eigener Arbeitsbereich.',
|
||||
cta: 'Zum Haus',
|
||||
route: 'HouseView',
|
||||
secondary: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
locale() {
|
||||
return window.navigator.language || 'en-US';
|
||||
},
|
||||
@@ -369,6 +453,16 @@ export default {
|
||||
openBranch(branchId) {
|
||||
this.$router.push({ name: 'BranchView', params: { branchId } });
|
||||
},
|
||||
openRoute(routeName) {
|
||||
if (routeName === 'BranchView') {
|
||||
const firstBranch = this.falukantUser?.branches?.[0];
|
||||
if (firstBranch?.id) {
|
||||
this.openBranch(firstBranch.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.$router.push({ name: routeName });
|
||||
},
|
||||
async fetchProductions() {
|
||||
try {
|
||||
const response = await apiClient.get('/api/falukant/productions');
|
||||
@@ -399,15 +493,15 @@ export default {
|
||||
async selectHeir(heirId) {
|
||||
try {
|
||||
await apiClient.post('/api/falukant/heirs/select', { heirId });
|
||||
// Lade User-Daten neu
|
||||
await this.fetchFalukantUser();
|
||||
if (this.falukantUser?.character) {
|
||||
await this.fetchAllStock();
|
||||
await this.fetchProductions();
|
||||
}
|
||||
showSuccess(this, 'Erbe wurde uebernommen.');
|
||||
} catch (error) {
|
||||
console.error('Error selecting heir:', error);
|
||||
alert(this.$t('falukant.overview.heirSelection.error'));
|
||||
showError(this, this.$t('falukant.overview.heirSelection.error'));
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -415,16 +509,99 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.falukant-overview {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.falukant-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(248, 162, 43, 0.16), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 224, 0.98) 100%);
|
||||
}
|
||||
|
||||
.falukant-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.falukant-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.falukant-summary-grid,
|
||||
.falukant-routine-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.falukant-routine-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summary-card,
|
||||
.routine-card {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
display: block;
|
||||
margin: 6px 0 8px;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-card p,
|
||||
.routine-card p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.summary-card__label,
|
||||
.routine-card__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.routine-card h3 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.routine-card button {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.overviewcontainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-gap: 5px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overviewcontainer>div {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 253, 249, 0.82);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.imagecontainer {
|
||||
@@ -438,10 +615,12 @@ export default {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: rgba(255,255,255,0.72);
|
||||
background-repeat: no-repeat;
|
||||
image-rendering: crisp-edges;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.house-with-character {
|
||||
@@ -453,8 +632,8 @@ export default {
|
||||
.house {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background-repeat: no-repeat;
|
||||
image-rendering: crisp-edges;
|
||||
z-index: 1;
|
||||
@@ -470,16 +649,13 @@ export default {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.heir-selection-container {
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(177, 59, 53, 0.18);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
background-color: #fff3cd;
|
||||
background-color: rgba(255, 243, 205, 0.92);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.heir-selection-container h3 {
|
||||
@@ -495,10 +671,10 @@ h2 {
|
||||
}
|
||||
|
||||
.heir-card {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
background-color: rgba(255,255,255,0.86);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -518,6 +694,20 @@ h2 {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.falukant-routine-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.falukant-summary-grid,
|
||||
.falukant-routine-grid,
|
||||
.overviewcontainer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.select-heir-button {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
@@ -535,6 +725,16 @@ h2 {
|
||||
.loading, .no-heirs {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.overviewcontainer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.imagecontainer {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<div class="home-logged-in">
|
||||
<header class="dashboard-header">
|
||||
<h1>Willkommen zurück!</h1>
|
||||
<p class="dashboard-subtitle">Schön, dass du wieder da bist.</p>
|
||||
<div class="dashboard-toolbar">
|
||||
<section class="dashboard-hero surface-card">
|
||||
<div class="dashboard-hero__copy">
|
||||
<span class="dashboard-kicker">Dein Bereich</span>
|
||||
<h1>Willkommen zurück!</h1>
|
||||
<p class="dashboard-subtitle">
|
||||
Dein persönlicher Einstieg in Community, Termine, Falukant und laufende Aktivitäten.
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-toolbar surface-card">
|
||||
<button
|
||||
v-if="!editMode"
|
||||
type="button"
|
||||
@@ -42,7 +47,25 @@
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-overview">
|
||||
<article class="overview-card surface-card">
|
||||
<span class="overview-card__label">Aktive Widgets</span>
|
||||
<strong>{{ widgets.length }}</strong>
|
||||
<p>Dein Dashboard ist modular aufgebaut und kann jederzeit umsortiert werden.</p>
|
||||
</article>
|
||||
<article class="overview-card surface-card">
|
||||
<span class="overview-card__label">Verfügbare Module</span>
|
||||
<strong>{{ widgetTypeOptions.length }}</strong>
|
||||
<p>Du kannst Community-, Kalender-, News- und Falukant-Module kombinieren.</p>
|
||||
</article>
|
||||
<article class="overview-card surface-card">
|
||||
<span class="overview-card__label">Bearbeitungsmodus</span>
|
||||
<strong>{{ editMode ? 'Aktiv' : 'Aus' }}</strong>
|
||||
<p>{{ editMode ? 'Widgets können gerade ergänzt und angepasst werden.' : 'Inhalte bleiben fokussiert und ruhig lesbar.' }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="loadError"
|
||||
@@ -58,11 +81,20 @@
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
ref="dashboardGridRef"
|
||||
class="dashboard-grid"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onAnyDrop($event)"
|
||||
class="dashboard-shell"
|
||||
>
|
||||
<div class="dashboard-shell__header">
|
||||
<div>
|
||||
<h2>Deine Übersicht</h2>
|
||||
<p>Widgets lassen sich verschieben und im Bearbeitungsmodus anpassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="dashboardGridRef"
|
||||
class="dashboard-grid"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onAnyDrop($event)"
|
||||
>
|
||||
<template v-for="(w, index) in widgets" :key="w.id">
|
||||
<div
|
||||
class="dashboard-grid-cell"
|
||||
@@ -103,6 +135,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="widgets.length === 0 && !loading" class="dashboard-empty">
|
||||
@@ -306,98 +339,170 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.home-logged-in {
|
||||
max-width: 1200px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding: 8px 0 24px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 24px;
|
||||
.dashboard-hero {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 26px;
|
||||
margin-bottom: 18px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(248, 162, 43, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255, 251, 246, 0.96) 0%, rgba(250, 243, 233, 0.98) 100%);
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
color: #333;
|
||||
margin: 0 0 4px 0;
|
||||
.dashboard-hero__copy {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.dashboard-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.dashboard-hero h1 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #666;
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.dashboard-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.overview-card__label {
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.overview-card strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.9rem;
|
||||
line-height: 1;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.overview-card p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dashboard-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
min-width: 300px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.btn-edit,
|
||||
.btn-done {
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--color-primary-orange);
|
||||
color: var(--color-text-on-orange);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.btn-edit:hover,
|
||||
.btn-done:hover {
|
||||
background: var(--color-primary-orange-light);
|
||||
color: var(--color-text-secondary);
|
||||
border-color: var(--color-text-secondary);
|
||||
color: #2b1f14;
|
||||
}
|
||||
|
||||
.widget-add-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-add-again {
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-text-secondary);
|
||||
background: #fff;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border-color: var(--color-border-strong);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-add-again:hover {
|
||||
background: var(--color-primary-orange-light);
|
||||
border-color: var(--color-primary-orange);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.widget-type-select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-text-secondary);
|
||||
background: #fff;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.9rem;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.dashboard-message {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
background: rgba(177, 59, 53, 0.12);
|
||||
color: #7a241f;
|
||||
border: 1px solid rgba(177, 59, 53, 0.18);
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 252, 247, 0.94) 0%, rgba(248, 241, 231, 0.96) 100%);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.dashboard-shell__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-shell__header h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.dashboard-shell__header p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
grid-auto-rows: 200px;
|
||||
gap: 20px;
|
||||
grid-auto-rows: 220px;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.dashboard-grid-cell {
|
||||
@@ -415,9 +520,9 @@ export default {
|
||||
}
|
||||
|
||||
.dashboard-grid-cell.drop-target {
|
||||
outline: 2px dashed #0d6efd;
|
||||
outline: 2px dashed rgba(248, 162, 43, 0.82);
|
||||
outline-offset: 4px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.dashboard-grid-cell.drag-source {
|
||||
@@ -426,13 +531,14 @@ export default {
|
||||
|
||||
.dashboard-widget-edit {
|
||||
min-height: 200px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.widget-edit-fields {
|
||||
@@ -441,40 +547,58 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.widget-edit-input {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
align-self: flex-start;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--color-primary-orange);
|
||||
color: var(--color-text-on-orange);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
min-height: 36px;
|
||||
background: rgba(177, 59, 53, 0.12);
|
||||
color: #7a241f;
|
||||
border-color: rgba(177, 59, 53, 0.18);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: var(--color-primary-orange-light);
|
||||
color: var(--color-text-secondary);
|
||||
border-color: var(--color-text-secondary);
|
||||
background: rgba(177, 59, 53, 0.18);
|
||||
}
|
||||
|
||||
.dashboard-empty {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #dee2e6;
|
||||
color: var(--color-text-secondary);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px dashed var(--color-border-strong);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.home-logged-in {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.dashboard-hero {
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-toolbar {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-overview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,44 +8,56 @@
|
||||
<Character3D gender="male" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div>
|
||||
<h2>{{ $t('home.nologin.welcome') }}</h2>
|
||||
<p>{{ $t('home.nologin.description') }}</p>
|
||||
<section class="actions-panel actions-panel--story surface-card">
|
||||
<div class="panel-intro">
|
||||
<span class="panel-kicker">Dein Einstieg</span>
|
||||
<h2>{{ $t('home.nologin.welcome') }}</h2>
|
||||
<p>{{ $t('home.nologin.description') }}</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
YourPart ist eine wachsende Online‑Plattform, die Community‑Funktionen, Echtzeit‑Chat, Foren,
|
||||
ein soziales Netzwerk mit Bildergalerie sowie das Aufbauspiel <em>Falukant</em> vereint.
|
||||
Aktuell befindet sich die Seite in der Beta‑Phase – wir erweitern Funktionen, Inhalte und
|
||||
Stabilität
|
||||
kontinuierlich.
|
||||
</p>
|
||||
<div class="story-highlight">
|
||||
<p>
|
||||
YourPart verbindet Community, Echtzeit-Chat, Foren, Bildergalerie und das Aufbauspiel
|
||||
<em>Falukant</em> in einer Plattform. Der Fokus liegt auf Austausch, spielerischer Tiefe und
|
||||
einer wachsenden Produktwelt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t('home.nologin.expected.title') }}</h3>
|
||||
<ul>
|
||||
<div class="story-block">
|
||||
<h3>{{ $t('home.nologin.expected.title') }}</h3>
|
||||
<ul class="feature-list">
|
||||
<li v-html="$t('home.nologin.expected.items.chat')"></li>
|
||||
<li v-html="$t('home.nologin.expected.items.social')"></li>
|
||||
<li v-html="$t('home.nologin.expected.items.forum')"></li>
|
||||
<li v-html="$t('home.nologin.expected.items.falukant')"></li>
|
||||
<li v-html="$t('home.nologin.expected.items.minigames')"></li>
|
||||
<li v-html="$t('home.nologin.expected.items.multilingual')"></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t('home.nologin.falukantShort.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.falukantShort.text') }}</p>
|
||||
<div class="story-columns">
|
||||
<article>
|
||||
<h3>{{ $t('home.nologin.falukantShort.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.falukantShort.text') }}</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>{{ $t('home.nologin.privacyBeta.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.privacyBeta.text') }}</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t('home.nologin.privacyBeta.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.privacyBeta.text') }}</p>
|
||||
|
||||
<h3>{{ $t('home.nologin.getStarted.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.getStarted.text', { register: $t('home.nologin.login.register') }) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div class="story-cta">
|
||||
<h3>{{ $t('home.nologin.getStarted.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.getStarted.text', { register: $t('home.nologin.login.register') }) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="actions-panel actions-panel--access surface-card">
|
||||
<div class="login-panel">
|
||||
<span class="panel-kicker">Direkt starten</span>
|
||||
<h2>{{ $t('home.nologin.login.submit') }}</h2>
|
||||
<div class="login-fields">
|
||||
<input v-model="username" size="20" type="text" :placeholder="$t('home.nologin.login.name')"
|
||||
:title="$t('home.nologin.login.namedescription')" @keydown.enter="focusPassword">
|
||||
</div>
|
||||
<div>
|
||||
<input v-model="password" size="20" type="password"
|
||||
:placeholder="$t('home.nologin.login.password')"
|
||||
:title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin"
|
||||
@@ -57,20 +69,26 @@
|
||||
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
|
||||
|
||||
<div class="access-split">
|
||||
<article class="access-card">
|
||||
<h3>{{ $t('home.nologin.randomchat') }}</h3>
|
||||
<p>Ohne lange Vorbereitung direkt in spontane Begegnungen und offene Gespraeche starten.</p>
|
||||
<button type="button" class="secondary-action" @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button>
|
||||
</article>
|
||||
<article class="access-card">
|
||||
<h3>Konto und Zugang</h3>
|
||||
<p>Neu hier oder Passwort vergessen? Von hier aus gelangst du direkt in Registrierung und Wiederherstellung.</p>
|
||||
<div class="access-links">
|
||||
<span @click="openPasswordResetDialog" class="link">{{
|
||||
$t('home.nologin.login.lostpassword') }}</span>
|
||||
<span @click="openRegisterDialog" class="link">{{ $t('home.nologin.login.register') }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ $t('home.nologin.randomchat') }}</h2>
|
||||
<button @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<span @click="openPasswordResetDialog" class="link">{{
|
||||
$t('home.nologin.login.lostpassword') }}</span> | <span id="o1p5iry1"
|
||||
@click="openRegisterDialog" class="link">{{ $t('home.nologin.login.register') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="mascot">
|
||||
<Character3D gender="female" />
|
||||
@@ -138,19 +156,22 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.beta-banner {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeeba;
|
||||
color: #856404;
|
||||
width: min(100%, var(--content-max-width));
|
||||
background: linear-gradient(180deg, #fff2cf 0%, #fde7b2 100%);
|
||||
border: 1px solid rgba(201, 130, 31, 0.24);
|
||||
color: #8a5a12;
|
||||
padding: 10px 14px;
|
||||
margin: 0 0 12px 0;
|
||||
margin: 0 0 14px 0;
|
||||
text-align: center;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.home-structure {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 2em;
|
||||
gap: 1.4rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
@@ -169,7 +190,7 @@ export default {
|
||||
align-items: stretch;
|
||||
background: linear-gradient(180deg, #fff5e8 0%, #fce7ca 100%);
|
||||
border: 1px solid rgba(248, 162, 43, 0.16);
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 10px 24px rgba(93, 64, 55, 0.08);
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
@@ -181,26 +202,124 @@ export default {
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2em;
|
||||
gap: 1rem;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.actions>div {
|
||||
.actions-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background-color: #FFF4F0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 251, 246, 0.96) 0%, rgba(248, 240, 231, 0.96) 100%);
|
||||
color: #5D4037;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 0.5rem;
|
||||
padding: 1.2rem 1.25rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions>div>h2 {
|
||||
color: var(--color-primary-orange);
|
||||
.actions-panel h2,
|
||||
.actions-panel h3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.7rem;
|
||||
padding: 0.3rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
color: #8a5411;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.panel-intro,
|
||||
.story-highlight,
|
||||
.story-block,
|
||||
.story-columns,
|
||||
.story-cta,
|
||||
.login-panel,
|
||||
.access-split {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.story-highlight {
|
||||
padding: 1rem 1.1rem;
|
||||
margin: 0.8rem 0 1rem;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(248, 162, 43, 0.08);
|
||||
border: 1px solid rgba(248, 162, 43, 0.12);
|
||||
}
|
||||
|
||||
.story-block {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.feature-list li + li {
|
||||
margin-top: 0.55rem;
|
||||
}
|
||||
|
||||
.story-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.story-columns article,
|
||||
.story-cta,
|
||||
.access-card {
|
||||
padding: 1rem 1.05rem;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
padding: 1rem 1.05rem;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-fields {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.primary-action,
|
||||
.secondary-action {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.access-split {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.access-card p {
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.access-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.stay-logged-in-row {
|
||||
@@ -299,8 +418,13 @@ export default {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.actions>div {
|
||||
.actions-panel {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.story-columns,
|
||||
.access-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="contentscroll match3-view">
|
||||
<!-- Spiel-Titel -->
|
||||
<div class="game-title">
|
||||
<section class="game-title surface-card">
|
||||
<span class="game-title__eyebrow">Minispiele</span>
|
||||
<h1>{{ $t('minigames.match3.title') }}</h1>
|
||||
<p>{{ $t('minigames.match3.campaignDescription') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="play-focus surface-card">
|
||||
<div class="play-focus__main">
|
||||
<span class="play-focus__eyebrow">Naechster Schritt</span>
|
||||
<h2>{{ playFocusTitle }}</h2>
|
||||
<p>{{ playFocusDescription }}</p>
|
||||
</div>
|
||||
<div class="play-focus__stats">
|
||||
<span class="play-focus__pill">Level {{ currentLevel }}</span>
|
||||
<span class="play-focus__pill">{{ completedObjectivesCount }}/{{ totalObjectivesCount || 0 }} Ziele</span>
|
||||
<span class="play-focus__pill">{{ safeMovesLeft }} Zuege uebrig</span>
|
||||
</div>
|
||||
<div class="play-focus__actions">
|
||||
<button class="btn btn-primary" @click="isPaused ? resumeGame() : pauseGame()">
|
||||
{{ isPaused ? $t('minigames.match3.resume') : $t('minigames.match3.pause') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="toggleLevelDescription">
|
||||
{{ levelDescriptionExpanded ? 'Ziele einklappen' : 'Ziele anzeigen' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="restartLevel">
|
||||
{{ $t('minigames.match3.restartLevel') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Kampagnen-Status -->
|
||||
<div class="game-layout">
|
||||
@@ -45,13 +70,13 @@
|
||||
|
||||
<div class="game-content">
|
||||
<!-- Verbleibende Züge -->
|
||||
<div class="moves-left-display">
|
||||
<div class="moves-left-display surface-card">
|
||||
<span class="moves-left-label">{{ $t('minigames.match3.movesLeft') }}:</span>
|
||||
<span class="moves-left-value">{{ safeMovesLeft }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Level-Info -->
|
||||
<div class="level-info-card" v-if="currentLevelData">
|
||||
<div class="level-info-card surface-card" v-if="currentLevelData">
|
||||
<div class="level-header">
|
||||
<div class="level-header-content">
|
||||
<h3 class="level-title">
|
||||
@@ -6010,6 +6035,42 @@ export default {
|
||||
},
|
||||
safeMovesLeft() {
|
||||
return this.movesLeft || 0;
|
||||
},
|
||||
totalObjectivesCount() {
|
||||
return this.currentLevelData?.objectives?.length || 0;
|
||||
},
|
||||
completedObjectivesCount() {
|
||||
if (!this.currentLevelData?.objectives?.length) {
|
||||
return 0;
|
||||
}
|
||||
return this.currentLevelData.objectives.filter((objective) => objective.completed).length;
|
||||
},
|
||||
nextPendingObjective() {
|
||||
return this.currentLevelData?.objectives?.find((objective) => !objective.completed) || null;
|
||||
},
|
||||
playFocusTitle() {
|
||||
if (this.isPaused) {
|
||||
return 'Spiel ist pausiert';
|
||||
}
|
||||
if (!this.currentLevelData) {
|
||||
return 'Level wird vorbereitet';
|
||||
}
|
||||
if (this.nextPendingObjective) {
|
||||
return this.nextPendingObjective.description || 'Aktuelles Ziel abschliessen';
|
||||
}
|
||||
return 'Level sauber zu Ende spielen';
|
||||
},
|
||||
playFocusDescription() {
|
||||
if (this.isPaused) {
|
||||
return 'Setze das Level fort oder starte es kontrolliert neu, ohne den aktuellen Kontext zu verlieren.';
|
||||
}
|
||||
if (!this.currentLevelData) {
|
||||
return 'Sobald das Level geladen ist, erscheinen hier das naechste Ziel und die passende Hauptaktion.';
|
||||
}
|
||||
if (this.nextPendingObjective) {
|
||||
return `Konzentriere dich zuerst auf dieses Ziel. Bereits erledigt: ${this.completedObjectivesCount} von ${this.totalObjectivesCount}.`;
|
||||
}
|
||||
return 'Alle sichtbaren Ziele sind erledigt. Jetzt zaehlt nur noch der saubere Abschluss des Levels.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6019,26 +6080,94 @@ export default {
|
||||
/* Minimalistischer Style - nur für Match3Game */
|
||||
/* Verwendet globale Scroll-Klassen: .contenthidden und .contentscroll */
|
||||
|
||||
.match3-view {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-top: 20px;
|
||||
margin: 16px auto 30px;
|
||||
max-width: 980px;
|
||||
padding: 28px;
|
||||
background: linear-gradient(135deg, rgba(255, 247, 233, 0.98), rgba(245, 237, 225, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.game-title__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.game-title h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.game-title p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.play-focus {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto 20px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.play-focus__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.play-focus h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.play-focus p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.play-focus__stats,
|
||||
.play-focus__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.play-focus__pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
color: #8a5411;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Spiel-Layout */
|
||||
.game-layout {
|
||||
display: flex;
|
||||
@@ -6062,12 +6191,12 @@ export default {
|
||||
|
||||
/* Verbleibende Züge Anzeige */
|
||||
.moves-left-display {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
background: rgba(255, 251, 246, 0.96);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px 14px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow-soft);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -6078,7 +6207,7 @@ export default {
|
||||
.moves-left-label {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.moves-left-value {
|
||||
@@ -6089,17 +6218,17 @@ export default {
|
||||
|
||||
/* Statistik-Bereich */
|
||||
.stats-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
background: rgba(255, 251, 246, 0.96);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 10px;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
@@ -6107,7 +6236,7 @@ export default {
|
||||
}
|
||||
|
||||
.stats-header:hover {
|
||||
background-color: #f8f9fa;
|
||||
background-color: rgba(248, 162, 43, 0.08);
|
||||
}
|
||||
|
||||
.stats-header-content {
|
||||
@@ -6121,7 +6250,7 @@ export default {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -6174,7 +6303,7 @@ export default {
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Statistik-Werte Farben */
|
||||
@@ -6185,12 +6314,12 @@ export default {
|
||||
|
||||
/* Level-Info */
|
||||
.level-info-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
background: rgba(255, 251, 246, 0.96);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow-soft);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
@@ -6198,7 +6327,7 @@ export default {
|
||||
.level-header {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 10px;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
@@ -6206,7 +6335,7 @@ export default {
|
||||
}
|
||||
|
||||
.level-header:hover {
|
||||
background-color: #f8f9fa;
|
||||
background-color: rgba(248, 162, 43, 0.08);
|
||||
}
|
||||
|
||||
.level-header-content {
|
||||
@@ -6220,7 +6349,7 @@ export default {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
@@ -6233,7 +6362,7 @@ export default {
|
||||
.level-info-card p {
|
||||
margin: 0 0 15px 0;
|
||||
text-align: left;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -6248,8 +6377,8 @@ export default {
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.64);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.objective-icon {
|
||||
@@ -6265,7 +6394,7 @@ export default {
|
||||
.objective-progress {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -6273,10 +6402,10 @@ export default {
|
||||
.game-board-container {
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
background: rgba(255, 251, 246, 0.96);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
margin-bottom: 20px;
|
||||
position: relative; /* Für absolute Positionierung der Animationen */
|
||||
}
|
||||
@@ -6461,6 +6590,17 @@ export default {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.game-title {
|
||||
margin-top: 12px;
|
||||
padding: 22px 18px;
|
||||
}
|
||||
|
||||
.play-focus {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
@@ -6875,4 +7015,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<div class="calendar-view">
|
||||
<h2>{{ $t('personal.calendar.title') }}</h2>
|
||||
<section class="calendar-hero surface-card">
|
||||
<div>
|
||||
<span class="calendar-kicker">Planung</span>
|
||||
<h2>{{ $t('personal.calendar.title') }}</h2>
|
||||
<p>Termine, Geburtstage und eigene Eintraege in einer strukturierten Uebersicht.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="calendar-toolbar">
|
||||
<div class="calendar-toolbar surface-card">
|
||||
<div class="nav-buttons">
|
||||
<button @click="openNewEventDialog()" class="btn-new-event">
|
||||
+ {{ $t('personal.calendar.newEntry') }}
|
||||
@@ -27,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Selection info -->
|
||||
<div v-if="selectedDates.length > 1" class="selection-info">
|
||||
<div v-if="selectedDates.length > 1" class="selection-info surface-card">
|
||||
{{ $t('personal.calendar.selectedDays', { count: selectedDates.length }) }}
|
||||
<button @click="createEventFromSelection" class="btn-create-from-selection">
|
||||
{{ $t('personal.calendar.createEventForSelection') }}
|
||||
@@ -839,16 +845,39 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.calendar-view {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
padding: 0 0 24px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
.calendar-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.calendar-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.calendar-hero h2 {
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calendar-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
// Toolbar
|
||||
.calendar-toolbar {
|
||||
display: flex;
|
||||
@@ -857,6 +886,7 @@ h2 {
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
@@ -1471,7 +1501,7 @@ h2 {
|
||||
.category-btn {
|
||||
padding: 6px 12px;
|
||||
border: 2px solid;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.hero {
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(135deg, #f7e0bb 0%, #f6c27d 45%, #e8924d 100%);
|
||||
box-shadow: 0 20px 60px rgba(106, 56, 20, 0.18);
|
||||
}
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
.grid article {
|
||||
padding: 24px;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: #fff7ef;
|
||||
border: 1px solid rgba(64, 38, 26, 0.08);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
.hero {
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.7), transparent 35%),
|
||||
linear-gradient(135deg, #d4f0e6 0%, #7dd0be 40%, #2e8b83 100%);
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
.cards article {
|
||||
padding: 24px;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: #effaf6;
|
||||
border: 1px solid rgba(23, 50, 58, 0.08);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.hero {
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
background:
|
||||
radial-gradient(circle at right top, rgba(255, 255, 255, 0.78), transparent 30%),
|
||||
linear-gradient(135deg, #eef6c8 0%, #bddd74 45%, #6b9d34 100%);
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
.features article {
|
||||
padding: 24px;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: #f7fbe9;
|
||||
border: 1px solid rgba(31, 47, 29, 0.08);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>{{ $t("settings.account.title") }}</h2>
|
||||
<div>
|
||||
<label><span>{{ $t("settings.account.username") }} </span><input type="text" v-model="username"
|
||||
:placeholder="$t('settings.account.username')" /></label>
|
||||
</div>
|
||||
<div>
|
||||
<label><span>{{ $t("settings.account.email") }} </span><input type="text" v-model="email"
|
||||
:placeholder="$t('settings.account.email')" /></label>
|
||||
</div>
|
||||
<div>
|
||||
<label><span>{{ $t("settings.account.newpassword") }} </span><input type="password" v-model="newpassword"
|
||||
:placeholder="$t('settings.account.newpassword')" autocomplete="new-password" /></label>
|
||||
</div>
|
||||
<div>
|
||||
<label><span>{{ $t("settings.account.newpasswordretype") }} </span><input type="password"
|
||||
v-model="newpasswordretype" :placeholder="$t('settings.account.newpasswordretype')" autocomplete="new-password" /></label>
|
||||
</div>
|
||||
<div>
|
||||
<label><span>{{ $t("settings.account.oldpassword") }} </span><input type="password"
|
||||
v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')" autocomplete="current-password" /></label>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="changeAccount">{{ $t("settings.account.changeaction") }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<label><input type="checkbox" v-model="showInSearch" /> {{ $t("settings.account.showinsearch") }}</label>
|
||||
</div>
|
||||
<div class="account-settings">
|
||||
<section class="account-settings__hero surface-card">
|
||||
<span class="account-settings__eyebrow">Einstellungen</span>
|
||||
<h2>{{ $t("settings.account.title") }}</h2>
|
||||
<p>Benutzername, E-Mail, Passwort und Sichtbarkeit an einer Stelle pflegen.</p>
|
||||
</section>
|
||||
|
||||
<section class="account-settings__panel surface-card">
|
||||
<div class="account-settings__grid">
|
||||
<label class="account-settings__field">
|
||||
<span>{{ $t("settings.account.username") }}</span>
|
||||
<input type="text" v-model="username" :placeholder="$t('settings.account.username')" />
|
||||
</label>
|
||||
|
||||
<label class="account-settings__field">
|
||||
<span>{{ $t("settings.account.email") }}</span>
|
||||
<input type="text" v-model="email" :placeholder="$t('settings.account.email')" />
|
||||
</label>
|
||||
|
||||
<label class="account-settings__field">
|
||||
<span>{{ $t("settings.account.newpassword") }}</span>
|
||||
<input type="password" v-model="newpassword" :placeholder="$t('settings.account.newpassword')"
|
||||
autocomplete="new-password" :class="{ 'field-error': newpassword && !isNewPasswordValid }" />
|
||||
<span v-if="newpassword && !isNewPasswordValid" class="form-error">Das neue Passwort sollte mindestens 8 Zeichen haben.</span>
|
||||
</label>
|
||||
|
||||
<label class="account-settings__field">
|
||||
<span>{{ $t("settings.account.newpasswordretype") }}</span>
|
||||
<input type="password" v-model="newpasswordretype"
|
||||
:placeholder="$t('settings.account.newpasswordretype')" autocomplete="new-password"
|
||||
:class="{ 'field-error': newpasswordretype && !passwordsMatch }" />
|
||||
<span v-if="newpasswordretype && !passwordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
|
||||
</label>
|
||||
|
||||
<label class="account-settings__field account-settings__field--full">
|
||||
<span>{{ $t("settings.account.oldpassword") }}</span>
|
||||
<input type="password" v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')"
|
||||
autocomplete="current-password" :class="{ 'field-error': requiresOldPassword && !oldpassword.trim() }" />
|
||||
<span v-if="requiresOldPassword && !oldpassword.trim()" class="form-error">Zum Passwortwechsel wird das aktuelle Passwort benoetigt.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="account-settings__toggle">
|
||||
<input type="checkbox" v-model="showInSearch" />
|
||||
<span>{{ $t("settings.account.showinsearch") }}</span>
|
||||
</label>
|
||||
|
||||
<div class="account-settings__actions">
|
||||
<button @click="changeAccount">{{ $t("settings.account.changeaction") }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { showApiError, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: "AccountSettingsView",
|
||||
components: {},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: "",
|
||||
@@ -50,6 +71,18 @@ export default {
|
||||
oldpassword: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
requiresOldPassword() {
|
||||
return this.newpassword.trim().length > 0;
|
||||
},
|
||||
isNewPasswordValid() {
|
||||
return this.newpassword.length === 0 || this.newpassword.length >= 8;
|
||||
},
|
||||
passwordsMatch() {
|
||||
return this.newpassword === this.newpasswordretype;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async changeAccount() {
|
||||
try {
|
||||
@@ -57,15 +90,19 @@ export default {
|
||||
const hasNewPassword = this.newpassword && this.newpassword.trim() !== '';
|
||||
|
||||
if (hasNewPassword) {
|
||||
if (!this.isNewPasswordValid) {
|
||||
showError(this, 'Das neue Passwort ist noch zu kurz.');
|
||||
return;
|
||||
}
|
||||
// Validiere Passwort-Wiederholung nur wenn ein neues Passwort eingegeben wurde
|
||||
if (this.newpassword !== this.newpasswordretype) {
|
||||
alert('Die Passwörter stimmen nicht überein.');
|
||||
if (!this.passwordsMatch) {
|
||||
showError(this, 'Die Passwoerter stimmen nicht ueberein.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob das alte Passwort eingegeben wurde
|
||||
if (!this.oldpassword || this.oldpassword.trim() === '') {
|
||||
alert('Bitte geben Sie Ihr aktuelles Passwort ein, um das Passwort zu ändern.');
|
||||
showError(this, 'Bitte geben Sie Ihr aktuelles Passwort ein, um das Passwort zu aendern.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -89,7 +126,7 @@ export default {
|
||||
// API-Aufruf zum Speichern der Account-Einstellungen
|
||||
await apiClient.post('/api/settings/set-account', accountData);
|
||||
|
||||
alert('Account-Einstellungen erfolgreich gespeichert!');
|
||||
showSuccess(this, 'Account-Einstellungen erfolgreich gespeichert.');
|
||||
|
||||
// Leere die Passwort-Felder nach erfolgreichem Speichern
|
||||
this.newpassword = '';
|
||||
@@ -98,17 +135,12 @@ export default {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Account-Einstellungen:', error);
|
||||
if (error.response && error.response.data && error.response.data.error) {
|
||||
alert('Fehler: ' + error.response.data.error);
|
||||
} else {
|
||||
alert('Ein Fehler ist aufgetreten beim Speichern der Account-Einstellungen.');
|
||||
}
|
||||
showApiError(this, error, 'Ein Fehler ist beim Speichern der Account-Einstellungen aufgetreten.');
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const response = await apiClient.post('/api/settings/account', { userId: this.user.id });
|
||||
console.log(response.data);
|
||||
this.username = response.data.username;
|
||||
this.showInSearch = response.data.showinsearch;
|
||||
this.email = response.data.email;
|
||||
@@ -117,18 +149,101 @@ export default {
|
||||
this.newpassword = '';
|
||||
this.newpasswordretype = '';
|
||||
this.oldpassword = '';
|
||||
|
||||
console.log(this.showInSearch);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
white-space: nowrap;
|
||||
.account-settings {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 960px;
|
||||
}
|
||||
label > span {
|
||||
width: 15em;
|
||||
display: inline-block;
|
||||
|
||||
.account-settings__hero,
|
||||
.account-settings__panel {
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
</style>
|
||||
|
||||
.account-settings__hero {
|
||||
padding: 26px 28px;
|
||||
}
|
||||
|
||||
.account-settings__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.account-settings__hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.account-settings__panel {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.account-settings__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.account-settings__field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account-settings__field span {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.account-settings__field--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.account-settings__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.account-settings__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.account-settings__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.account-settings__hero,
|
||||
.account-settings__panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.account-settings__actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.account-settings__actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.diary.title') }}</h2>
|
||||
|
||||
<div class="new-entry-section">
|
||||
<h3>{{ isEditing ? $t('socialnetwork.diary.editEntry') : $t('socialnetwork.diary.newEntry') }}</h3>
|
||||
<textarea v-model="newEntryText" placeholder="Write your diary entry..."></textarea>
|
||||
<div class="form-actions">
|
||||
<button @click="saveEntry">{{ isEditing ? $t('socialnetwork.diary.update') : $t('socialnetwork.diary.save')
|
||||
}}</button>
|
||||
<button v-if="isEditing" @click="cancelEdit">{{ $t('socialnetwork.diary.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="diaryEntries.length === 0">{{ $t('socialnetwork.diary.noEntries') }}</div>
|
||||
<div v-else class="diary-entries">
|
||||
<div v-for="entry in diaryEntries" :key="entry.id" class="diary-entry">
|
||||
<p v-html="sanitizedText(entry)"></p>
|
||||
<div class="entry-info">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
<span class="entry-actions">
|
||||
<span @click="editEntry(entry)" class="button" :title="$t('socialnetwork.diary.edit')">✎</span>
|
||||
<span @click="deleteEntry(entry.id)" class="button" :title="$t('socialnetwork.diary.delete')">✖</span>
|
||||
</span>
|
||||
<div class="diary-view">
|
||||
<section class="diary-hero surface-card">
|
||||
<div>
|
||||
<span class="diary-kicker">Persoenliche Eintraege</span>
|
||||
<h2>{{ $t('socialnetwork.diary.title') }}</h2>
|
||||
<p>Gedanken, Notizen und kurze Updates in einer ruhigen, persoenlichen Ansicht.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class=" pagination">
|
||||
<button @click="loadDiaryEntries(currentPage - 1)" v-if="currentPage !== 1">{{
|
||||
$t('socialnetwork.diary.prevPage') }}</button>
|
||||
<span>{{ $t('socialnetwork.diary.page') }} {{ currentPage }} / {{ totalPages }}</span>
|
||||
<button @click="loadDiaryEntries(currentPage + 1)" v-if="currentPage < totalPages">{{
|
||||
$t('socialnetwork.diary.nextPage') }}</button>
|
||||
<section class="new-entry-section surface-card">
|
||||
<h3>{{ isEditing ? $t('socialnetwork.diary.editEntry') : $t('socialnetwork.diary.newEntry') }}</h3>
|
||||
<textarea v-model="newEntryText" placeholder="Write your diary entry..."></textarea>
|
||||
<div class="form-actions">
|
||||
<button @click="saveEntry">{{ isEditing ? $t('socialnetwork.diary.update') : $t('socialnetwork.diary.save')
|
||||
}}</button>
|
||||
<button v-if="isEditing" @click="cancelEdit">{{ $t('socialnetwork.diary.cancel') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="diaryEntries.length === 0" class="diary-empty surface-card">{{ $t('socialnetwork.diary.noEntries') }}</div>
|
||||
<section v-else class="diary-entries">
|
||||
<article v-for="entry in diaryEntries" :key="entry.id" class="diary-entry surface-card">
|
||||
<p v-html="sanitizedText(entry)"></p>
|
||||
<div class="entry-info">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
<span class="entry-actions">
|
||||
<span @click="editEntry(entry)" class="button" :title="$t('socialnetwork.diary.edit')">✎</span>
|
||||
<span @click="deleteEntry(entry.id)" class="button" :title="$t('socialnetwork.diary.delete')">✖</span>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="pagination">
|
||||
<button @click="loadDiaryEntries(currentPage - 1)" v-if="currentPage !== 1">{{
|
||||
$t('socialnetwork.diary.prevPage') }}</button>
|
||||
<span>{{ $t('socialnetwork.diary.page') }} {{ currentPage }} / {{ totalPages }}</span>
|
||||
<button @click="loadDiaryEntries(currentPage + 1)" v-if="currentPage < totalPages">{{
|
||||
$t('socialnetwork.diary.nextPage') }}</button>
|
||||
</div>
|
||||
<ChooseDialog ref="chooseDialog" />
|
||||
</div>
|
||||
<ChooseDialog ref="chooseDialog" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -137,13 +145,38 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.diary-view {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.diary-hero,
|
||||
.new-entry-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.diary-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.diary-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
height: 140px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -152,13 +185,12 @@ textarea {
|
||||
}
|
||||
|
||||
.diary-entry {
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-bottom: 1em;
|
||||
padding-bottom: 1em;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
color: gray;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -176,13 +208,23 @@ textarea {
|
||||
|
||||
.pagination {
|
||||
margin-top: 1em;
|
||||
background-color: #7BBE55;
|
||||
color: #fff;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.diary-entries {
|
||||
width: 400px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.diary-empty {
|
||||
padding: 22px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
<template>
|
||||
<h2 class="link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
||||
<h3 v-if="forumTopic">{{ forumTopic }}</h3>
|
||||
<ul class="messages">
|
||||
<li v-for="message in messages" :key="message.id">
|
||||
<div v-html="sanitizedMessage(message)"></div>
|
||||
<div class="footer">
|
||||
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
|
||||
{{ message.lastMessageUser.username }}
|
||||
</span>
|
||||
<span>{{ new Date(message.createdAt).toLocaleString() }}</span>
|
||||
<div class="forum-topic-view">
|
||||
<section class="forum-topic-hero surface-card">
|
||||
<div>
|
||||
<div class="forum-topic-back link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</div>
|
||||
<h2 v-if="forumTopic">{{ forumTopic }}</h2>
|
||||
<p>Diskussionen, Antworten und neue Beitraege in einer fokussierten Leseflaeche.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="editor-container">
|
||||
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
||||
<section class="forum-topic-messages">
|
||||
<ul class="messages">
|
||||
<li v-for="message in messages" :key="message.id" class="surface-card">
|
||||
<div v-html="sanitizedMessage(message)"></div>
|
||||
<div class="footer">
|
||||
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
|
||||
{{ message.lastMessageUser.username }}
|
||||
</span>
|
||||
<span>{{ new Date(message.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="editor-container surface-card">
|
||||
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="saveNewMessage">{{ $t('socialnetwork.forum.createNewMesssage') }}</button>
|
||||
</div>
|
||||
<button @click="saveNewMessage">{{ $t('socialnetwork.forum.createNewMesssage') }}</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -98,6 +108,27 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.forum-topic-view {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.forum-topic-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.forum-topic-back {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.forum-topic-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.messages {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
@@ -105,14 +136,13 @@ export default {
|
||||
}
|
||||
|
||||
.messages > li {
|
||||
border: 1px solid #7BBE55;
|
||||
margin-bottom: 0.25em;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.75em;
|
||||
padding: 1rem 1.1rem;
|
||||
}
|
||||
|
||||
.messages > li > .footer {
|
||||
color: #F9A22C;
|
||||
font-size: 0.7em;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
}
|
||||
@@ -127,10 +157,10 @@ export default {
|
||||
|
||||
.editor-container {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0;
|
||||
min-height: 260px;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor {
|
||||
@@ -141,7 +171,7 @@ export default {
|
||||
.editor :deep(.ProseMirror) {
|
||||
min-height: 260px;
|
||||
outline: none;
|
||||
padding: 10px;
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,54 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
||||
<div class="creationtoggler">
|
||||
<button @click="createNewTopic">
|
||||
{{ $t(!inCreation
|
||||
? 'socialnetwork.forum.showNewTopic'
|
||||
: 'socialnetwork.forum.hideNewTopic') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="forum-view">
|
||||
<section class="forum-hero surface-card">
|
||||
<div>
|
||||
<span class="forum-kicker">Community-Forum</span>
|
||||
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
||||
<p>Themen, Diskussionen und neue Beitraege an einem strukturierten Ort.</p>
|
||||
</div>
|
||||
<div class="creationtoggler">
|
||||
<button @click="createNewTopic">
|
||||
{{ $t(!inCreation
|
||||
? 'socialnetwork.forum.showNewTopic'
|
||||
: 'socialnetwork.forum.hideNewTopic') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="inCreation">
|
||||
<div>
|
||||
<section v-if="inCreation" class="forum-creation surface-card">
|
||||
<label class="newtitle">
|
||||
{{ $t('socialnetwork.forum.topic') }}
|
||||
<span>{{ $t('socialnetwork.forum.topic') }}</span>
|
||||
<input type="text" v-model="newTitle" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="saveNewTopic">
|
||||
{{ $t('socialnetwork.forum.createNewTopic') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="saveNewTopic">
|
||||
{{ $t('socialnetwork.forum.createNewTopic') }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div v-else-if="titles.length > 0">
|
||||
<!-- hier kommt deine bestehende TABLE + PAGINATION hin -->
|
||||
<table>
|
||||
<!-- Kopfzeile, Spalten etc. -->
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<button @click="goToPage(page-1)" :disabled="page<=1">‹</button>
|
||||
<span>{{ page }} / {{ totalPages }}</span>
|
||||
<button @click="goToPage(page+1)" :disabled="page>=totalPages">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<section v-else-if="titles.length > 0" class="forum-topics surface-card">
|
||||
<ul class="topic-list">
|
||||
<li v-for="topic in titles" :key="topic.id" class="topic-card">
|
||||
<button type="button" class="topic-card__main" @click="openTopic(topic.id)">
|
||||
<strong>{{ topic.title }}</strong>
|
||||
<span class="topic-card__meta">
|
||||
{{ topic.user?.username || topic.owner?.username || 'Community' }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="pagination">
|
||||
<button @click="goToPage(page-1)" :disabled="page<=1">‹</button>
|
||||
<span>{{ page }} / {{ totalPages }}</span>
|
||||
<button @click="goToPage(page+1)" :disabled="page>=totalPages">›</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-else>
|
||||
{{ $t('socialnetwork.forum.noTitles') }}
|
||||
<div v-else class="forum-empty surface-card">
|
||||
{{ $t('socialnetwork.forum.noTitles') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -156,20 +168,60 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.creationtoggler {
|
||||
margin-bottom: 1em;
|
||||
.forum-view {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.forum-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 18px;
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.forum-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.forum-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.creationtoggler {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.forum-creation,
|
||||
.forum-topics,
|
||||
.forum-empty {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.newtitle {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.newtitle input {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 0.6em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: 1em 0;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0;
|
||||
min-height: 260px;
|
||||
background-color: white;
|
||||
@@ -189,16 +241,62 @@ export default {
|
||||
.editor :deep(.ProseMirror p) { margin: 0 0 .6rem; }
|
||||
.editor :deep(.ProseMirror p:first-child) { margin-top: 0; }
|
||||
.editor :deep(.ProseMirror-focused) { outline: 2px solid rgba(100,150,255,.35); }
|
||||
|
||||
.topic-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topic-card + .topic-card {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.topic-card__main {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: none;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.topic-card__main strong {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.topic-card__meta {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.pagination button {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.forum-empty {
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.forum-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.topic-card__main {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>{{ $t('friends.title') }}</h2>
|
||||
<div class="friends-view">
|
||||
<section class="friends-hero surface-card">
|
||||
<div>
|
||||
<span class="friends-kicker">Community</span>
|
||||
<h2>{{ $t('friends.title') }}</h2>
|
||||
<p>Freundschaften, offene Anfragen und laufende Kontakte an einem Ort.</p>
|
||||
</div>
|
||||
<div class="friends-stats">
|
||||
<div class="friends-stat surface-card">
|
||||
<strong>{{ tabs[0].data.length }}</strong>
|
||||
<span>Bestehend</span>
|
||||
</div>
|
||||
<div class="friends-stat surface-card">
|
||||
<strong>{{ tabs[1].data.length + tabs[2].data.length }}</strong>
|
||||
<span>Offen</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="tabs-container">
|
||||
<div class="tab" v-for="(tab, index) in tabs" :key="tab.name" :class="{ active: activeTab === index }"
|
||||
@click="selectTab(index)">
|
||||
{{ $t(tab.label) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(tab, index) in tabs" v-show="activeTab === index" :key="tab.name">
|
||||
<div v-for="(tab, index) in tabs" v-show="activeTab === index" :key="tab.name" class="friends-panel surface-card">
|
||||
<v-data-table :items="paginatedData(tab.data, tab.pagination.page)" :headers="headers"
|
||||
:items-per-page="tab.pagination.itemsPerPage" class="elevation-1">
|
||||
<template v-slot:body="{ items }">
|
||||
@@ -167,25 +183,85 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.friends-view {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.friends-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 18px;
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.friends-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.friends-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.friends-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.friends-stat {
|
||||
min-width: 120px;
|
||||
padding: 14px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.friends-stat strong {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.friends-stat span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #999;
|
||||
padding: 5px 0;
|
||||
gap: 8px;
|
||||
padding: 0 0 12px;
|
||||
border-bottom: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 2px 4px;
|
||||
padding: 8px 14px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: bold;
|
||||
border: 1px solid #999;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.tab:not(.active):hover {
|
||||
background-color: #ddd;
|
||||
background-color: rgba(248, 162, 43, 0.12);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
@@ -194,6 +270,10 @@ export default {
|
||||
border-color: #F9A22C;
|
||||
}
|
||||
|
||||
.friends-panel {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.font-color-gender-male {
|
||||
color: #1E90FF;
|
||||
}
|
||||
@@ -205,4 +285,11 @@ export default {
|
||||
.font-color-gender-nonbinary {
|
||||
color: #DAA520;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.friends-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.gallery.title') }}</h2>
|
||||
<div class="gallery-view">
|
||||
<div class="sidebar">
|
||||
<div class="gallery-page">
|
||||
<section class="gallery-hero surface-card">
|
||||
<div>
|
||||
<span class="gallery-kicker">Bilder und Ordner</span>
|
||||
<h2>{{ $t('socialnetwork.gallery.title') }}</h2>
|
||||
<p>Eigene Inhalte organisieren, sichtbar machen und in Ordnern strukturieren.</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="gallery-view">
|
||||
<div class="sidebar surface-card">
|
||||
<h3>{{ $t('socialnetwork.gallery.folders') }}</h3>
|
||||
<ul class="tree">
|
||||
<folder-item v-for="folder in [folders]" :key="folder.id" :folder="folder"
|
||||
@@ -13,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="upload-section">
|
||||
<div class="upload-section surface-card">
|
||||
<div class="upload-header" @click="toggleUploadSection">
|
||||
<span>
|
||||
<i class="icon-upload-toggle">{{ isUploadVisible ? '▲' : '▼' }}</i>
|
||||
@@ -63,9 +70,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-list">
|
||||
<div class="image-list surface-card">
|
||||
<h3>{{ $t('socialnetwork.gallery.images') }}</h3>
|
||||
<ul v-if="images.length > 0">
|
||||
<ul v-if="images.length > 0" class="image-grid">
|
||||
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
|
||||
<img :src="image.url || image.placeholder" alt="Loading..." />
|
||||
<p>{{ image.title }}</p>
|
||||
@@ -75,6 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -265,35 +273,95 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gallery-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.gallery-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.gallery-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.gallery-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.gallery-view {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
margin-right: 20px;
|
||||
width: 240px;
|
||||
margin-right: 0;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.image-list li {
|
||||
margin: 4px;
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 14px;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image-grid li {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-grid p {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image-list li img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.icon-upload-toggle {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -302,51 +370,23 @@ export default {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-item.selected {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.image-list > ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.image-list > ul > li {
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
border: 1px solid #F9A22C;
|
||||
}
|
||||
|
||||
.image-list > ul > li > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-list li img {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
color: red;
|
||||
.upload-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tree {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.gallery-view {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
|
||||
<div>
|
||||
<div v-if="guestbookEntries.length === 0">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
|
||||
<div class="guestbook-view">
|
||||
<section class="guestbook-hero surface-card">
|
||||
<div>
|
||||
<span class="guestbook-kicker">Gaestebuch</span>
|
||||
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
|
||||
<p>Nachrichten, Rueckmeldungen und kleine Einblicke aus deinem Netzwerk.</p>
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="guestbookEntries.length === 0" class="guestbook-empty surface-card">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
|
||||
</div>
|
||||
<div v-else class="guestbook-entries">
|
||||
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
|
||||
<article v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry surface-card">
|
||||
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
|
||||
style="max-width: 400px; max-height: 400px;" />
|
||||
class="guestbook-image" />
|
||||
<p v-html="sanitizedContent(entry)"></p>
|
||||
<div class="entry-info">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
@@ -14,7 +20,7 @@
|
||||
<span @click="openProfile(entry.senderUsername)">{{ entry.sender }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button @click="loadGuestbookEntries(currentPage - 1)" v-if="currentPage !== 1">{{
|
||||
@@ -85,10 +91,72 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.guestbook-view {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.guestbook-hero,
|
||||
.guestbook-empty {
|
||||
padding: 22px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.guestbook-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.guestbook-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.guestbook-entries {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guestbook-entry {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.guestbook-image {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
border-radius: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.entry-user span {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 1em;
|
||||
background-color: #7BBE55;
|
||||
color: #fff;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,52 @@
|
||||
<template>
|
||||
<div class="search-view">
|
||||
<h2>{{ $t('socialnetwork.usersearch.title') }}</h2>
|
||||
<form @submit.prevent="performSearch">
|
||||
<div class="form-group">
|
||||
<label for="username">{{ $t('socialnetwork.usersearch.username') }}:</label>
|
||||
<input type="text" id="username" v-model="searchCriteria.username"
|
||||
:placeholder="$t('socialnetwork.usersearch.username')" />
|
||||
<section class="search-hero surface-card">
|
||||
<div>
|
||||
<span class="search-kicker">Community-Suche</span>
|
||||
<h2>{{ $t('socialnetwork.usersearch.title') }}</h2>
|
||||
<p>Mit Namen, Alter und Geschlecht gezielt passende Kontakte in der Community finden.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ageFrom">{{ $t('socialnetwork.usersearch.age_from') }}:</label>
|
||||
<input type="number" id="ageFrom" v-model="searchCriteria.ageFrom" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_from')" class="age-input" />
|
||||
<label for="ageTo">{{ $t('socialnetwork.usersearch.age_to') }}:</label>
|
||||
<input type="number" id="ageTo" v-model="searchCriteria.ageTo" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_to')" class="age-input" />
|
||||
<section class="search-form surface-card">
|
||||
<form @submit.prevent="performSearch">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="username">{{ $t('socialnetwork.usersearch.username') }}</label>
|
||||
<input type="text" id="username" v-model="searchCriteria.username"
|
||||
:placeholder="$t('socialnetwork.usersearch.username')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group--age">
|
||||
<label for="ageFrom">{{ $t('socialnetwork.usersearch.age_from') }}</label>
|
||||
<div class="age-range">
|
||||
<input type="number" id="ageFrom" v-model="searchCriteria.ageFrom" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_from')" class="age-input" />
|
||||
<span class="age-separator">bis</span>
|
||||
<input type="number" id="ageTo" v-model="searchCriteria.ageTo" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_to')" class="age-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gender">{{ $t('socialnetwork.usersearch.gender') }}</label>
|
||||
<multiselect v-model="searchCriteria.gender" :options="genderOptions" :multiple="true"
|
||||
:close-on-select="false" :placeholder="$t('socialnetwork.usersearch.gender')" label="name"
|
||||
track-by="name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="search-button">{{ $t('socialnetwork.usersearch.search_button') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="search-results surface-card" v-if="searchResults.length">
|
||||
<div class="results-header">
|
||||
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
||||
<span class="results-count">{{ searchResults.length }} Treffer</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gender">{{ $t('socialnetwork.usersearch.gender') }}:</label>
|
||||
<multiselect v-model="searchCriteria.gender" :options="genderOptions" :multiple="true"
|
||||
:close-on-select="false" :placeholder="$t('socialnetwork.usersearch.gender')" label="name"
|
||||
track-by="name" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="search-button">{{ $t('socialnetwork.usersearch.search_button') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="search-results" v-if="searchResults.length">
|
||||
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -47,8 +63,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="no-results">
|
||||
</section>
|
||||
<div v-else class="no-results surface-card">
|
||||
{{ $t('socialnetwork.usersearch.no_results') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,83 +130,117 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.search-view {
|
||||
max-width: 600px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
.search-hero,
|
||||
.search-form,
|
||||
.search-results,
|
||||
.no-results {
|
||||
padding: 22px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.search-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
input,
|
||||
.multiselect__input {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
.age-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.age-input {
|
||||
width: 70px;
|
||||
margin-right: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 20px;
|
||||
.age-separator {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.88rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-results ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
.form-actions {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.search-results li {
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
.results-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #7BBE55;
|
||||
color: #42634e;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding-right: 1em;
|
||||
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
th, td:not:last-child {
|
||||
border-bottom: 1px solid #7E471B;
|
||||
tbody tr + tr td {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.g-male {
|
||||
@@ -200,4 +250,14 @@ th, td:not:last-child {
|
||||
.g-female {
|
||||
color: #ff3377;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.age-range {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,59 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
|
||||
<div class="vocab-chapter-view">
|
||||
<section class="vocab-chapter-hero surface-card">
|
||||
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
|
||||
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Uebung wechseln.</p>
|
||||
</section>
|
||||
|
||||
<div class="box">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="!chapter">{{ $t('socialnetwork.vocab.notFound') }}</div>
|
||||
<div v-else>
|
||||
<div v-show="!practiceOpen">
|
||||
<div class="row">
|
||||
<button @click="back">{{ $t('general.back') }}</button>
|
||||
<button v-if="vocabs.length" @click="openPractice">{{ $t('socialnetwork.vocab.practice.open') }}</button>
|
||||
<button @click="openSearch">{{ $t('socialnetwork.vocab.search.open') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="chapter.isOwner">
|
||||
<h3>{{ $t('socialnetwork.vocab.addVocab') }}</h3>
|
||||
<div class="grid">
|
||||
<label>
|
||||
{{ $t('socialnetwork.vocab.learningWord') }}
|
||||
<input v-model="learning" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('socialnetwork.vocab.referenceWord') }}
|
||||
<input v-model="reference" type="text" />
|
||||
</label>
|
||||
<section class="box surface-card">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="!chapter">{{ $t('socialnetwork.vocab.notFound') }}</div>
|
||||
<div v-else>
|
||||
<div v-show="!practiceOpen">
|
||||
<div class="row row--actions">
|
||||
<button @click="back">{{ $t('general.back') }}</button>
|
||||
<button v-if="vocabs.length" @click="openPractice">{{ $t('socialnetwork.vocab.practice.open') }}</button>
|
||||
<button @click="openSearch">{{ $t('socialnetwork.vocab.search.open') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-card" v-if="chapter.isOwner">
|
||||
<h3>{{ $t('socialnetwork.vocab.addVocab') }}</h3>
|
||||
<div class="grid">
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.vocab.learningWord') }}</span>
|
||||
<input v-model="learning" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.vocab.referenceWord') }}</span>
|
||||
<input v-model="reference" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<button :disabled="saving || !canSave" @click="add">
|
||||
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.add') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="vocabs.length === 0" class="empty-state">{{ $t('socialnetwork.vocab.noVocabs') }}</div>
|
||||
<div v-else class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('socialnetwork.vocab.learningWord') }}</th>
|
||||
<th>{{ $t('socialnetwork.vocab.referenceWord') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="v in vocabs" :key="v.id">
|
||||
<td>{{ v.learning }}</td>
|
||||
<td>{{ v.reference }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button :disabled="saving || !canSave" @click="add">
|
||||
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.add') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div v-if="vocabs.length === 0">{{ $t('socialnetwork.vocab.noVocabs') }}</div>
|
||||
<table v-else class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('socialnetwork.vocab.learningWord') }}</th>
|
||||
<th>{{ $t('socialnetwork.vocab.referenceWord') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="v in vocabs" :key="v.id">
|
||||
<td>{{ v.learning }}</td>
|
||||
<td>{{ v.reference }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<VocabPracticeDialog ref="practiceDialog" />
|
||||
@@ -147,30 +153,120 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
display: inline-block;
|
||||
.vocab-chapter-view {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.vocab-chapter-hero,
|
||||
.box {
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.vocab-chapter-hero,
|
||||
.box {
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.vocab-chapter-hero__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.vocab-chapter-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.row--actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.editor-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin: 18px 0 20px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.grid label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.grid span {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 18px;
|
||||
border: 1px dashed var(--color-border-strong);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-secondary);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tbl {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
.tbl th,
|
||||
.tbl td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tbl th {
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.vocab-chapter-hero,
|
||||
.box {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div class="vocab-course-list">
|
||||
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
|
||||
<section class="vocab-courses-hero surface-card">
|
||||
<div>
|
||||
<span class="vocab-courses-kicker">Kurse</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
|
||||
<p>Oeffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="box">
|
||||
<div class="box surface-card">
|
||||
<div class="actions">
|
||||
<button @click="showCreateDialog = true">{{ $t('socialnetwork.vocab.courses.create') }}</button>
|
||||
<button @click="loadMyCourses">{{ $t('socialnetwork.vocab.courses.myCourses') }}</button>
|
||||
@@ -361,14 +367,37 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.vocab-course-list {
|
||||
padding: 20px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.vocab-courses-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vocab-courses-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.vocab-courses-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<div class="vocab-course-view">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-if="loading" class="surface-card course-state">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="course">
|
||||
<h2>{{ course.title }}</h2>
|
||||
<p v-if="course.description">{{ course.description }}</p>
|
||||
<section class="course-hero surface-card">
|
||||
<div>
|
||||
<span class="course-kicker">Lernkurs</span>
|
||||
<h2>{{ course.title }}</h2>
|
||||
<p v-if="course.description">{{ course.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="course-info">
|
||||
<div class="course-info surface-card">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
|
||||
<span v-if="course.isPublic">{{ $t('socialnetwork.vocab.courses.public') }}</span>
|
||||
<span v-if="course.shareCode && isOwner" class="share-code">
|
||||
@@ -18,7 +23,7 @@
|
||||
<button @click="editCourse">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="course.lessons && course.lessons.length > 0" class="lessons-list">
|
||||
<div v-if="course.lessons && course.lessons.length > 0" class="lessons-list surface-card">
|
||||
<div class="current-lesson-section" v-if="currentLesson">
|
||||
<button @click="openLesson(currentLesson.id)" class="btn-current-lesson">
|
||||
{{ $t('socialnetwork.vocab.courses.continueCurrentLesson') }}
|
||||
@@ -75,7 +80,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
||||
<p class="surface-card course-state">{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,28 +89,29 @@
|
||||
<div class="dialog" @click.stop>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.addLesson') }}</h3>
|
||||
<form @submit.prevent="addLesson">
|
||||
<div class="form-group">
|
||||
<div class="form-group form-field">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.lessonNumber') }}</label>
|
||||
<input type="number" v-model.number="newLesson.lessonNumber" min="1" required />
|
||||
<input type="number" v-model.number="newLesson.lessonNumber" min="1" required :class="{ 'field-error': lessonFormTouched && !isLessonNumberValid }" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group form-field">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.title') }}</label>
|
||||
<input v-model="newLesson.title" required />
|
||||
<input v-model="newLesson.title" required :class="{ 'field-error': lessonFormTouched && !isLessonTitleValid }" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group form-field">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.description') }}</label>
|
||||
<textarea v-model="newLesson.description"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group form-field">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.chapter') }}</label>
|
||||
<select v-model="newLesson.chapterId" required>
|
||||
<select v-model="newLesson.chapterId" required :class="{ 'field-error': lessonFormTouched && !isLessonChapterValid }">
|
||||
<option value="">{{ $t('socialnetwork.vocab.courses.selectChapter') }}</option>
|
||||
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">{{ $t('general.create') }}</button>
|
||||
<button type="button" @click="showAddLessonDialog = false">{{ $t('general.cancel') }}</button>
|
||||
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollstaendig angeben.</span>
|
||||
<div class="form-actions form-actions-row">
|
||||
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
|
||||
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -116,6 +122,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'VocabCourseView',
|
||||
@@ -132,6 +139,7 @@ export default {
|
||||
progress: [],
|
||||
chapters: [],
|
||||
showAddLessonDialog: false,
|
||||
lessonFormTouched: false,
|
||||
newLesson: {
|
||||
lessonNumber: 1,
|
||||
title: '',
|
||||
@@ -163,6 +171,18 @@ export default {
|
||||
|
||||
// Alle Lektionen abgeschlossen - zeige die letzte Lektion
|
||||
return sortedLessons[sortedLessons.length - 1];
|
||||
},
|
||||
isLessonNumberValid() {
|
||||
return Number(this.newLesson.lessonNumber) > 0;
|
||||
},
|
||||
isLessonTitleValid() {
|
||||
return this.newLesson.title.trim().length >= 3;
|
||||
},
|
||||
isLessonChapterValid() {
|
||||
return Boolean(this.newLesson.chapterId);
|
||||
},
|
||||
canCreateLesson() {
|
||||
return this.isLessonNumberValid && this.isLessonTitleValid && this.isLessonChapterValid;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -232,9 +252,14 @@ export default {
|
||||
return false;
|
||||
},
|
||||
async addLesson() {
|
||||
this.lessonFormTouched = true;
|
||||
if (!this.canCreateLesson) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.post(`/api/vocab/courses/${this.courseId}/lessons`, this.newLesson);
|
||||
this.showAddLessonDialog = false;
|
||||
this.lessonFormTouched = false;
|
||||
this.newLesson = {
|
||||
lessonNumber: 1,
|
||||
title: '',
|
||||
@@ -242,9 +267,10 @@ export default {
|
||||
chapterId: null
|
||||
};
|
||||
await this.loadCourse();
|
||||
showSuccess(this, 'Lektion erfolgreich angelegt.');
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Hinzufügen der Lektion:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Hinzufügen der Lektion');
|
||||
showApiError(this, e, 'Fehler beim Hinzufuegen der Lektion');
|
||||
}
|
||||
},
|
||||
async deleteLesson(lessonId) {
|
||||
@@ -254,9 +280,10 @@ export default {
|
||||
try {
|
||||
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
||||
await this.loadCourse();
|
||||
showSuccess(this, 'Lektion erfolgreich geloescht.');
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Löschen der Lektion:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Löschen der Lektion');
|
||||
showApiError(this, e, 'Fehler beim Loeschen der Lektion');
|
||||
}
|
||||
},
|
||||
openLesson(lessonId) {
|
||||
@@ -278,15 +305,47 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.vocab-course-view {
|
||||
padding: 20px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.course-hero,
|
||||
.course-info,
|
||||
.lessons-list,
|
||||
.course-state {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.course-hero {
|
||||
padding: 24px 26px;
|
||||
}
|
||||
|
||||
.course-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.course-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.course-info {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin: 15px 0;
|
||||
margin: 0 0 16px;
|
||||
color: #666;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.share-code {
|
||||
@@ -307,7 +366,14 @@ export default {
|
||||
}
|
||||
|
||||
.lessons-list {
|
||||
margin-top: 30px;
|
||||
margin-top: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.course-state {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.current-lesson-section {
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
|
||||
<div class="vocab-language-view">
|
||||
<section class="vocab-language-hero surface-card">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="!language">{{ $t('socialnetwork.vocab.notFound') }}</div>
|
||||
<div v-else>
|
||||
<span class="vocab-language-kicker">Sprache</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
|
||||
<p>Kapitel, Suchfunktionen und Freigaben fuer diese Sprache an einem Ort.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="box">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="!language">{{ $t('socialnetwork.vocab.notFound') }}</div>
|
||||
<div v-else>
|
||||
<div class="row">
|
||||
<strong>{{ $t('socialnetwork.vocab.languageName') }}:</strong>
|
||||
<div class="box surface-card" v-if="language">
|
||||
<div class="row row--meta">
|
||||
<strong>{{ $t('socialnetwork.vocab.languageName') }}</strong>
|
||||
<span>{{ language.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="language.isOwner && language.shareCode">
|
||||
<strong>{{ $t('socialnetwork.vocab.shareCode') }}:</strong>
|
||||
<div class="row row--meta" v-if="language.isOwner && language.shareCode">
|
||||
<strong>{{ $t('socialnetwork.vocab.shareCode') }}</strong>
|
||||
<code>{{ language.shareCode }}</code>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row row--actions">
|
||||
<button @click="goSubscribe">{{ $t('socialnetwork.vocab.subscribeByCode') }}</button>
|
||||
<button @click="openSearch">{{ $t('socialnetwork.vocab.search.open') }}</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<h3>{{ $t('socialnetwork.vocab.chapters') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="language.isOwner">
|
||||
<div class="row row--create" v-if="language.isOwner">
|
||||
<label>
|
||||
{{ $t('socialnetwork.vocab.newChapter') }}
|
||||
<input v-model="newChapterTitle" type="text" />
|
||||
@@ -39,11 +43,12 @@
|
||||
<div v-if="chaptersLoading">{{ $t('general.loading') }}</div>
|
||||
<div v-else>
|
||||
<div v-if="chapters.length === 0">{{ $t('socialnetwork.vocab.noChapters') }}</div>
|
||||
<ul v-else>
|
||||
<ul v-else class="chapter-list">
|
||||
<li v-for="c in chapters" :key="c.id">
|
||||
<span class="click" @click="openChapter(c.id)">
|
||||
{{ c.title }} <span class="count">({{ c.vocabCount }})</span>
|
||||
</span>
|
||||
<button type="button" class="chapter-card" @click="openChapter(c.id)">
|
||||
<span>{{ c.title }}</span>
|
||||
<span class="count">{{ c.vocabCount }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -139,22 +144,81 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vocab-language-view {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.vocab-language-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vocab-language-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.vocab-language-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
}
|
||||
.row {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.row--meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.row--actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.row--create {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.click {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chapter-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.chapter-card {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: none;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.count {
|
||||
color: #666;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +1,50 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.vocab.newLanguageTitle') }}</h2>
|
||||
<div class="vocab-new-language-view">
|
||||
<section class="vocab-new-language-hero surface-card">
|
||||
<span class="vocab-new-language-hero__eyebrow">Vokabeltrainer</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.newLanguageTitle') }}</h2>
|
||||
<p>Neue Sprache anlegen, Freigabecode erzeugen und direkt in die Bearbeitung wechseln.</p>
|
||||
</section>
|
||||
|
||||
<div class="box">
|
||||
<label class="label">
|
||||
{{ $t('socialnetwork.vocab.languageName') }}
|
||||
<input v-model="name" type="text" />
|
||||
</label>
|
||||
<section class="box surface-card">
|
||||
<label class="label form-field">
|
||||
<span>{{ $t('socialnetwork.vocab.languageName') }}</span>
|
||||
<input v-model="name" type="text" :class="{ 'field-error': nameTouched && !canSave }" />
|
||||
<span class="form-hint">Ein kurzer, klarer Sprachname reicht fuer den Start.</span>
|
||||
<span v-if="nameTouched && !canSave" class="form-error">Der Name sollte mindestens 2 Zeichen haben.</span>
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button :disabled="saving || !canSave" @click="create">
|
||||
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.create') }}
|
||||
</button>
|
||||
<button :disabled="saving" @click="cancel">{{ $t('Cancel') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="created" class="created">
|
||||
<div><strong>{{ $t('socialnetwork.vocab.created') }}</strong></div>
|
||||
<div>
|
||||
{{ $t('socialnetwork.vocab.shareCode') }}:
|
||||
<code>{{ created.shareCode }}</code>
|
||||
<div class="actions form-actions-row">
|
||||
<button :disabled="saving || !canSave" @click="create">
|
||||
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.create') }}
|
||||
</button>
|
||||
<button :disabled="saving" @click="cancel" class="button-secondary">{{ $t('Cancel') }}</button>
|
||||
</div>
|
||||
<div class="hint">{{ $t('socialnetwork.vocab.shareHint') }}</div>
|
||||
<button @click="openLanguage(created.id)">{{ $t('socialnetwork.vocab.openLanguage') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="created" class="created">
|
||||
<div><strong>{{ $t('socialnetwork.vocab.created') }}</strong></div>
|
||||
<div>
|
||||
{{ $t('socialnetwork.vocab.shareCode') }}:
|
||||
<code>{{ created.shareCode }}</code>
|
||||
</div>
|
||||
<div class="hint">{{ $t('socialnetwork.vocab.shareHint') }}</div>
|
||||
<button @click="openLanguage(created.id)">{{ $t('socialnetwork.vocab.openLanguage') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'VocabNewLanguageView',
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
nameTouched: false,
|
||||
saving: false,
|
||||
created: null,
|
||||
};
|
||||
@@ -53,22 +63,20 @@ export default {
|
||||
this.$router.push(`/socialnetwork/vocab/${id}`);
|
||||
},
|
||||
async create() {
|
||||
this.nameTouched = true;
|
||||
if (!this.canSave) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
try {
|
||||
const res = await apiClient.post('/api/vocab/languages', { name: this.name });
|
||||
this.created = res.data;
|
||||
// Menü sofort lokal aktualisieren (zusätzlich zum serverseitigen reloadmenu event)
|
||||
try { await this.loadMenu(); } catch (_) {}
|
||||
this.$root.$refs.messageDialog?.open(
|
||||
this.$t('socialnetwork.vocab.createdMessage'),
|
||||
this.$t('socialnetwork.vocab.createdTitle')
|
||||
);
|
||||
showSuccess(this, this.$t('socialnetwork.vocab.createdMessage'), this.$t('socialnetwork.vocab.createdTitle'));
|
||||
} catch (e) {
|
||||
console.error('Create vocab language failed:', e);
|
||||
this.$root.$refs.messageDialog?.open(
|
||||
this.$t('socialnetwork.vocab.createError'),
|
||||
this.$t('error.title')
|
||||
);
|
||||
showApiError(this, e, this.$t('socialnetwork.vocab.createError'));
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -78,29 +86,88 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vocab-new-language-view {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.vocab-new-language-hero,
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.vocab-new-language-hero,
|
||||
.box {
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.vocab-new-language-hero__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.vocab-new-language-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label span {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.created {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #bbb;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
padding: 18px;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.hint {
|
||||
margin-top: 6px;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.vocab-new-language-hero,
|
||||
.box {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.actions button,
|
||||
.created button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.vocab.subscribeTitle') }}</h2>
|
||||
<div class="vocab-subscribe-view">
|
||||
<section class="vocab-subscribe-hero surface-card">
|
||||
<span class="vocab-subscribe-hero__eyebrow">Vokabeltrainer</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.subscribeTitle') }}</h2>
|
||||
<p>{{ $t('socialnetwork.vocab.subscribeHint') }}</p>
|
||||
</section>
|
||||
|
||||
<div class="box">
|
||||
<p>{{ $t('socialnetwork.vocab.subscribeHint') }}</p>
|
||||
<section class="box surface-card">
|
||||
<label class="label">
|
||||
<span>{{ $t('socialnetwork.vocab.shareCode') }}</span>
|
||||
<input v-model="shareCode" type="text" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
{{ $t('socialnetwork.vocab.shareCode') }}
|
||||
<input v-model="shareCode" type="text" />
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button :disabled="saving || !canSave" @click="subscribe">
|
||||
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.subscribe') }}
|
||||
</button>
|
||||
<button :disabled="saving" @click="back">{{ $t('general.back') }}</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button :disabled="saving || !canSave" @click="subscribe">
|
||||
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.subscribe') }}
|
||||
</button>
|
||||
<button :disabled="saving" @click="back">{{ $t('general.back') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,19 +79,69 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vocab-subscribe-view {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.vocab-subscribe-hero,
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.vocab-subscribe-hero,
|
||||
.box {
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.vocab-subscribe-hero__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.vocab-subscribe-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label span {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.vocab-subscribe-hero,
|
||||
.box {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +1,101 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.vocab.title') }}</h2>
|
||||
<div class="vocab-view">
|
||||
<section class="vocab-hero surface-card">
|
||||
<div>
|
||||
<span class="vocab-kicker">Sprachenlernen</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.title') }}</h2>
|
||||
<p>{{ $t('socialnetwork.vocab.description') }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
|
||||
<button @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="box">
|
||||
<p>{{ $t('socialnetwork.vocab.description') }}</p>
|
||||
<section class="vocab-summary-grid">
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Sprachen gesamt</span>
|
||||
<strong>{{ languages.length }}</strong>
|
||||
<p>Alle aktiven Sprachbereiche, in denen du Inhalte nutzt oder verwaltest.</p>
|
||||
</article>
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Eigene Bereiche</span>
|
||||
<strong>{{ ownedLanguages.length }}</strong>
|
||||
<p>Hier legst du Inhalte, Kapitel und Lernmaterial aktiv selbst an.</p>
|
||||
</article>
|
||||
<article class="summary-card surface-card">
|
||||
<span class="summary-card__label">Abonniert</span>
|
||||
<strong>{{ subscribedLanguages.length }}</strong>
|
||||
<p>Diese Bereiche sind eher fuer Lernen und Fortschritt statt Verwaltung gedacht.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
|
||||
<button @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
|
||||
</div>
|
||||
<section class="vocab-task-grid">
|
||||
<article class="task-card surface-card">
|
||||
<span class="task-card__eyebrow">Schnellstart</span>
|
||||
<h3>Neue Sprache anlegen</h3>
|
||||
<p>Der beste Einstieg, wenn du Inhalte selbst strukturieren und pflegen willst.</p>
|
||||
<button type="button" @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
|
||||
</article>
|
||||
<article class="task-card surface-card">
|
||||
<span class="task-card__eyebrow">Weiterlernen</span>
|
||||
<h3>Kurse und Kapitel oeffnen</h3>
|
||||
<p>Springe direkt in bestehende Lernpfade und arbeite mit vorhandenen Kursen weiter.</p>
|
||||
<button type="button" class="button-secondary" @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else>
|
||||
<div v-if="languages.length === 0">
|
||||
<section class="vocab-box surface-card">
|
||||
<div v-if="loading" class="vocab-state">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="languages.length === 0" class="vocab-state">
|
||||
{{ $t('socialnetwork.vocab.none') }}
|
||||
</div>
|
||||
<ul v-else>
|
||||
<li v-for="l in languages" :key="l.id">
|
||||
<span class="langname" @click="openLanguage(l.id)">{{ l.name }}</span>
|
||||
<span class="role" v-if="l.isOwner">({{ $t('socialnetwork.vocab.owner') }})</span>
|
||||
<span class="role" v-else>({{ $t('socialnetwork.vocab.subscribed') }})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else class="language-sections">
|
||||
<section class="language-section">
|
||||
<div class="language-section__header">
|
||||
<div>
|
||||
<h3>Eigene Sprachen</h3>
|
||||
<p>Direkter Einstieg in Bearbeitung, Kapitel und Kursverwaltung.</p>
|
||||
</div>
|
||||
<span class="language-section__count">{{ ownedLanguages.length }}</span>
|
||||
</div>
|
||||
<ul v-if="ownedLanguages.length" class="language-list">
|
||||
<li v-for="l in ownedLanguages" :key="l.id" class="language-card">
|
||||
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
|
||||
<div class="language-card__info">
|
||||
<span class="langname">{{ l.name }}</span>
|
||||
<span class="language-card__hint">Verwalten und Inhalte pflegen</span>
|
||||
</div>
|
||||
<span class="role">{{ $t('socialnetwork.vocab.owner') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="language-empty">Noch keine eigenen Sprachbereiche vorhanden.</p>
|
||||
</section>
|
||||
|
||||
<section class="language-section">
|
||||
<div class="language-section__header">
|
||||
<div>
|
||||
<h3>Abonnierte Sprachen</h3>
|
||||
<p>Gut fuer schnellen Wiedereinstieg ins Lernen ohne Verwaltungsaufwand.</p>
|
||||
</div>
|
||||
<span class="language-section__count">{{ subscribedLanguages.length }}</span>
|
||||
</div>
|
||||
<ul v-if="subscribedLanguages.length" class="language-list">
|
||||
<li v-for="l in subscribedLanguages" :key="l.id" class="language-card">
|
||||
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
|
||||
<div class="language-card__info">
|
||||
<span class="langname">{{ l.name }}</span>
|
||||
<span class="language-card__hint">Lernen, ueben und Fortschritt ansehen</span>
|
||||
</div>
|
||||
<span class="role">{{ $t('socialnetwork.vocab.subscribed') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="language-empty">Keine abonnierten Sprachen vorhanden.</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -39,6 +113,12 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
ownedLanguages() {
|
||||
return this.languages.filter((language) => language.isOwner);
|
||||
},
|
||||
subscribedLanguages() {
|
||||
return this.languages.filter((language) => !language.isOwner);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goNewLanguage() {
|
||||
@@ -69,22 +149,197 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
.vocab-view {
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.vocab-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 18px;
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vocab-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
color: #8a5411;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.vocab-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.vocab-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.vocab-summary-grid,
|
||||
.vocab-task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vocab-task-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summary-card,
|
||||
.task-card {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
display: block;
|
||||
margin: 6px 0 10px;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-card p,
|
||||
.task-card p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.summary-card__label,
|
||||
.task-card__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.task-card h3 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.task-card button {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vocab-state {
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.language-sections {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.language-section {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.language-section__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.language-section__header h3 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.language-section__header p,
|
||||
.language-empty {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.language-section__count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.12);
|
||||
color: #8a5411;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.language-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.language-card__main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: none;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.language-card__info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.langname {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.language-card__hint {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.role {
|
||||
margin-left: 6px;
|
||||
color: #666;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.vocab-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.vocab-summary-grid,
|
||||
.vocab-task-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user