feat(vocab): add dashboard learning summary and related endpoints
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Introduced `getDashboardLearningSummary` method in `VocabService` to provide a compact overview of enrolled courses and current lessons for users. - Updated `vocabController` to include a new route for the dashboard widget, allowing users to access their learning summary. - Enhanced `vocabRouter` to route requests for the new dashboard widget endpoint. - Added localization support for the new dashboard features across multiple languages, improving user engagement and accessibility. - Updated UI components to integrate the new dashboard widget, ensuring a seamless user experience.
This commit is contained in:
@@ -28,10 +28,12 @@ import ListWidget from './widgets/ListWidget.vue';
|
||||
import BirthdayWidget from './widgets/BirthdayWidget.vue';
|
||||
import UpcomingEventsWidget from './widgets/UpcomingEventsWidget.vue';
|
||||
import MiniCalendarWidget from './widgets/MiniCalendarWidget.vue';
|
||||
import VocabCoursesWidget from './widgets/VocabCoursesWidget.vue';
|
||||
|
||||
function getWidgetComponent(endpoint) {
|
||||
if (!endpoint || typeof endpoint !== 'string') return ListWidget;
|
||||
const ep = endpoint.toLowerCase();
|
||||
if (ep.includes('vocab/dashboard-widget')) return VocabCoursesWidget;
|
||||
if (ep.includes('falukant')) return FalukantWidget;
|
||||
if (ep.includes('news')) return NewsWidget;
|
||||
if (ep.includes('calendar/widget/birthdays')) return BirthdayWidget;
|
||||
@@ -42,7 +44,7 @@ function getWidgetComponent(endpoint) {
|
||||
|
||||
export default {
|
||||
name: 'DashboardWidget',
|
||||
components: { FalukantWidget, NewsWidget, ListWidget, BirthdayWidget, UpcomingEventsWidget, MiniCalendarWidget },
|
||||
components: { FalukantWidget, NewsWidget, ListWidget, BirthdayWidget, UpcomingEventsWidget, MiniCalendarWidget, VocabCoursesWidget },
|
||||
props: {
|
||||
widgetId: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
|
||||
165
frontend/src/components/widgets/VocabCoursesWidget.vue
Normal file
165
frontend/src/components/widgets/VocabCoursesWidget.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="vocab-courses-widget">
|
||||
<p v-if="!courses.length" class="vocab-courses-widget__empty">
|
||||
{{ $t('widgets.vocabCourses.empty') }}
|
||||
<router-link class="vocab-courses-widget__link" to="/socialnetwork/vocab/courses">
|
||||
{{ $t('widgets.vocabCourses.browseCourses') }}
|
||||
</router-link>
|
||||
</p>
|
||||
<ul v-else class="vocab-courses-widget__list">
|
||||
<li v-for="c in courses" :key="c.courseId" class="vocab-courses-widget__item">
|
||||
<div class="vocab-courses-widget__main">
|
||||
<strong class="vocab-courses-widget__course-title">{{ c.title || $t('widgets.vocabCourses.unnamedCourse') }}</strong>
|
||||
<span v-if="c.currentLesson" class="vocab-courses-widget__lesson">
|
||||
{{ $t('widgets.vocabCourses.lessonLine', { number: c.currentLesson.lessonNumber, title: c.currentLesson.title }) }}
|
||||
</span>
|
||||
<span v-else class="vocab-courses-widget__lesson vocab-courses-widget__lesson--muted">
|
||||
{{ $t('widgets.vocabCourses.noLessons') }}
|
||||
</span>
|
||||
<span v-if="c.allLessonsCompleted && c.currentLesson" class="vocab-courses-widget__badge">
|
||||
{{ $t('widgets.vocabCourses.allDone') }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="c.currentLesson"
|
||||
type="button"
|
||||
class="vocab-courses-widget__btn"
|
||||
@click="goToLesson(c.courseId, c.currentLesson.id)"
|
||||
>
|
||||
{{ $t('widgets.vocabCourses.openLesson') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="vocab-courses-widget__btn vocab-courses-widget__btn--secondary"
|
||||
@click="goToCourse(c.courseId)"
|
||||
>
|
||||
{{ $t('widgets.vocabCourses.openCourse') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VocabCoursesWidget',
|
||||
props: {
|
||||
data: { type: Object, default: null }
|
||||
},
|
||||
computed: {
|
||||
courses() {
|
||||
const raw = this.data;
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return [];
|
||||
}
|
||||
const list = raw.courses ?? raw.Courses;
|
||||
return Array.isArray(list) ? list : [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToLesson(courseId, lessonId) {
|
||||
this.$router.push({
|
||||
name: 'VocabLesson',
|
||||
params: { courseId: String(courseId), lessonId: String(lessonId) }
|
||||
});
|
||||
},
|
||||
goToCourse(courseId) {
|
||||
this.$router.push({
|
||||
name: 'VocabCourse',
|
||||
params: { courseId: String(courseId) }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vocab-courses-widget__empty {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary, #555);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__link {
|
||||
display: inline-block;
|
||||
margin-top: 0.35rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-orange-dark, #c2410c);
|
||||
}
|
||||
|
||||
.vocab-courses-widget__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--dashboard-widget-border, #e9ecef);
|
||||
}
|
||||
|
||||
.vocab-courses-widget__item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__course-title {
|
||||
font-size: 0.92rem;
|
||||
color: var(--color-text-primary, #222);
|
||||
}
|
||||
|
||||
.vocab-courses-widget__lesson {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-secondary, #555);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__lesson--muted {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #198754;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__btn {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
background: var(--color-primary-orange, #f97316);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vocab-courses-widget__btn:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.vocab-courses-widget__btn--secondary {
|
||||
background: var(--color-text-secondary, #6c757d);
|
||||
}
|
||||
</style>
|
||||
@@ -433,7 +433,28 @@
|
||||
"bookmarkCandidate": "Timan-i kining kandidatura",
|
||||
"voteError": "Sayop sa paghatag sa boto",
|
||||
"voteAllError": "Sayop sa paghatag sa mga boto",
|
||||
"applyError": "Dili mapadala ang aplikasyon."
|
||||
"applyError": "Dili mapadala ang aplikasyon.",
|
||||
"benefits": {
|
||||
"daily_salary": "Adlaw-adlaw nga suhol (usa ra kada adlaw): mga {amount}",
|
||||
"tax_exemption": "Wa’y buhis: {regions}",
|
||||
"tax_exemption_all": "Wa’y buhis: tanang lebel sa rehiyon",
|
||||
"generic": "Benepisyo ({code})"
|
||||
},
|
||||
"regionLevels": {
|
||||
"city": "Siyudad",
|
||||
"county": "County",
|
||||
"shire": "Shire",
|
||||
"markgrave": "Margravate",
|
||||
"duchy": "Duchy",
|
||||
"country": "Nasud"
|
||||
},
|
||||
"current": {
|
||||
"benefit": "Benepisyo",
|
||||
"benefit_all": "Tanang rehiyon",
|
||||
"holder": "Tag-iya",
|
||||
"none": "Walay karon nga posisyon.",
|
||||
"termEnds": "Matapos sa"
|
||||
}
|
||||
},
|
||||
"family": {
|
||||
"title": "Pamilya",
|
||||
|
||||
@@ -81,6 +81,16 @@
|
||||
},
|
||||
"falukant": {
|
||||
"emptyValue": "—"
|
||||
},
|
||||
"vocabCourses": {
|
||||
"empty": "Wala ka pa nakasulod sa bisan unsang kurso sa bokabularyo.",
|
||||
"browseCourses": "Tan-awa ang mga kurso",
|
||||
"unnamedCourse": "Kurso nga walay titulo",
|
||||
"lessonLine": "Leksyon #{number}: {title}",
|
||||
"noLessons": "Walay mga leksyon niini nga kurso.",
|
||||
"allDone": "Natapos na ang tanang leksyon",
|
||||
"openLesson": "Adto sa leksyon",
|
||||
"openCourse": "Ablihi ang kurso"
|
||||
}
|
||||
},
|
||||
"gender": {
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"news": "Balita",
|
||||
"birthdays": "Mga adlawng natawhan",
|
||||
"upcoming": "Umaabot nga mga appointment",
|
||||
"calendar": "Kalendaryo"
|
||||
"calendar": "Kalendaryo",
|
||||
"vocabCourses": "Mga kurso sa pinulongan"
|
||||
},
|
||||
"overview": {
|
||||
"activeWidgetsLabel": "Aktibong mga widget",
|
||||
|
||||
@@ -1353,6 +1353,20 @@
|
||||
"voteError": "Fehler beim Abgeben der Stimme",
|
||||
"voteAllError": "Fehler beim Abgeben der Stimmen",
|
||||
"applyError": "Bewerbung konnte nicht eingereicht werden.",
|
||||
"benefits": {
|
||||
"daily_salary": "Tagesamtshonorar (einmal pro Tag): ca. {amount}",
|
||||
"tax_exemption": "Steuerbefreiung: {regions}",
|
||||
"tax_exemption_all": "Steuerbefreiung: alle Regionsebenen",
|
||||
"generic": "Vorteil ({code})"
|
||||
},
|
||||
"regionLevels": {
|
||||
"city": "Stadt",
|
||||
"county": "Landkreis",
|
||||
"shire": "Grafschaft",
|
||||
"markgrave": "Markgrafschaft",
|
||||
"duchy": "Herzogtum",
|
||||
"country": "Land"
|
||||
},
|
||||
"current": {
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
|
||||
@@ -92,6 +92,16 @@
|
||||
},
|
||||
"falukant": {
|
||||
"emptyValue": "—"
|
||||
},
|
||||
"vocabCourses": {
|
||||
"empty": "Du bist derzeit in keinem Vokabelkurs eingeschrieben.",
|
||||
"browseCourses": "Kurse entdecken",
|
||||
"unnamedCourse": "Kurs ohne Titel",
|
||||
"lessonLine": "Lektion #{number}: {title}",
|
||||
"noLessons": "Noch keine Lektionen in diesem Kurs.",
|
||||
"allDone": "Alle Lektionen absolviert",
|
||||
"openLesson": "Zur Lektion",
|
||||
"openCourse": "Zum Kurs"
|
||||
}
|
||||
},
|
||||
"gender": {
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"news": "News",
|
||||
"birthdays": "Geburtstage",
|
||||
"upcoming": "Nächste Termine",
|
||||
"calendar": "Kalender"
|
||||
"calendar": "Kalender",
|
||||
"vocabCourses": "Sprachkurse"
|
||||
},
|
||||
"overview": {
|
||||
"activeWidgetsLabel": "Aktive Widgets",
|
||||
|
||||
@@ -574,6 +574,20 @@
|
||||
"voteError": "Error while submitting the vote",
|
||||
"voteAllError": "Error while submitting the votes",
|
||||
"applyError": "Application could not be submitted.",
|
||||
"benefits": {
|
||||
"daily_salary": "Daily office stipend (once per day): about {amount}",
|
||||
"tax_exemption": "Tax exemption: {regions}",
|
||||
"tax_exemption_all": "Tax exemption: all regional levels",
|
||||
"generic": "Benefit ({code})"
|
||||
},
|
||||
"regionLevels": {
|
||||
"city": "City",
|
||||
"county": "County",
|
||||
"shire": "Shire",
|
||||
"markgrave": "Margravate",
|
||||
"duchy": "Duchy",
|
||||
"country": "Country"
|
||||
},
|
||||
"current": {
|
||||
"office": "Office",
|
||||
"region": "Region",
|
||||
|
||||
@@ -92,6 +92,16 @@
|
||||
},
|
||||
"falukant": {
|
||||
"emptyValue": "—"
|
||||
},
|
||||
"vocabCourses": {
|
||||
"empty": "You are not enrolled in any vocabulary course yet.",
|
||||
"browseCourses": "Browse courses",
|
||||
"unnamedCourse": "Untitled course",
|
||||
"lessonLine": "Lesson #{number}: {title}",
|
||||
"noLessons": "No lessons in this course yet.",
|
||||
"allDone": "All lessons completed",
|
||||
"openLesson": "Go to lesson",
|
||||
"openCourse": "Open course"
|
||||
}
|
||||
},
|
||||
"gender": {
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"news": "News",
|
||||
"birthdays": "Birthdays",
|
||||
"upcoming": "Upcoming appointments",
|
||||
"calendar": "Calendar"
|
||||
"calendar": "Calendar",
|
||||
"vocabCourses": "Language courses"
|
||||
},
|
||||
"overview": {
|
||||
"activeWidgetsLabel": "Active widgets",
|
||||
|
||||
@@ -1261,6 +1261,20 @@
|
||||
"upcoming": "Cargos pendientes de (re)elección",
|
||||
"elections": "Elecciones"
|
||||
},
|
||||
"benefits": {
|
||||
"daily_salary": "Estipendio diario (una vez al día): unos {amount}",
|
||||
"tax_exemption": "Exención fiscal: {regions}",
|
||||
"tax_exemption_all": "Exención fiscal: todos los niveles regionales",
|
||||
"generic": "Ventaja ({code})"
|
||||
},
|
||||
"regionLevels": {
|
||||
"city": "Ciudad",
|
||||
"county": "Condado",
|
||||
"shire": "Condado (shire)",
|
||||
"markgrave": "Margraviato",
|
||||
"duchy": "Ducado",
|
||||
"country": "País"
|
||||
},
|
||||
"current": {
|
||||
"office": "Cargo",
|
||||
"region": "Región",
|
||||
|
||||
@@ -92,6 +92,16 @@
|
||||
},
|
||||
"falukant": {
|
||||
"emptyValue": "—"
|
||||
},
|
||||
"vocabCourses": {
|
||||
"empty": "Aún no estás inscrito en ningún curso de vocabulario.",
|
||||
"browseCourses": "Ver cursos",
|
||||
"unnamedCourse": "Curso sin título",
|
||||
"lessonLine": "Lección n.º {number}: {title}",
|
||||
"noLessons": "Este curso aún no tiene lecciones.",
|
||||
"allDone": "Todas las lecciones completadas",
|
||||
"openLesson": "Ir a la lección",
|
||||
"openCourse": "Abrir curso"
|
||||
}
|
||||
},
|
||||
"gender": {
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"news": "Noticias",
|
||||
"birthdays": "Cumpleaños",
|
||||
"upcoming": "Próximas citas",
|
||||
"calendar": "Calendario"
|
||||
"calendar": "Calendario",
|
||||
"vocabCourses": "Cursos de idiomas"
|
||||
},
|
||||
"overview": {
|
||||
"activeWidgetsLabel": "Widgets activos",
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.benefit') }}:
|
||||
<template v-if="pos.benefit && pos.benefit.length">
|
||||
<span v-if="pos.benefit.includes('*')">{{ $t('falukant.politics.current.benefit_all') }}</span>
|
||||
<span v-else>{{ pos.benefit.join(', ') }}</span>
|
||||
<template v-if="politicsBenefitItems(pos).length">
|
||||
<template v-for="(b, i) in politicsBenefitItems(pos)" :key="i">
|
||||
<span>{{ formatPoliticsBenefitItem(b) }}</span>
|
||||
<span v-if="i < politicsBenefitItems(pos).length - 1">, </span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
@@ -178,6 +180,49 @@ export default {
|
||||
this.loadCurrentPositions();
|
||||
},
|
||||
methods: {
|
||||
politicsBenefitItems(pos) {
|
||||
const raw = pos?.benefit;
|
||||
if (!Array.isArray(raw) || !raw.length) {
|
||||
return [];
|
||||
}
|
||||
if (raw.every((x) => typeof x === 'string') && raw.includes('*')) {
|
||||
return [this.$t('falukant.politics.current.benefit_all')];
|
||||
}
|
||||
return raw;
|
||||
},
|
||||
|
||||
formatPoliticsRegionLevel(key) {
|
||||
const k = String(key || '');
|
||||
const path = `falukant.politics.regionLevels.${k}`;
|
||||
const t = this.$t(path);
|
||||
return t !== path ? t : k;
|
||||
},
|
||||
|
||||
formatPoliticsBenefitItem(b) {
|
||||
if (b == null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof b === 'string') {
|
||||
return b;
|
||||
}
|
||||
if (typeof b === 'object' && b.tr) {
|
||||
if (b.tr === 'tax_exemption' && b.params?.all) {
|
||||
return this.$t('falukant.politics.benefits.tax_exemption_all');
|
||||
}
|
||||
if (b.tr === 'tax_exemption' && Array.isArray(b.params?.regions)) {
|
||||
const labels = b.params.regions.map((r) => this.formatPoliticsRegionLevel(r)).join(', ');
|
||||
return this.$t('falukant.politics.benefits.tax_exemption', { regions: labels });
|
||||
}
|
||||
if (b.tr === 'daily_salary') {
|
||||
return this.$t('falukant.politics.benefits.daily_salary', { amount: b.params?.amount ?? '—' });
|
||||
}
|
||||
if (b.tr === 'generic_benefit') {
|
||||
return this.$t('falukant.politics.benefits.generic', { code: b.params?.code || '' });
|
||||
}
|
||||
}
|
||||
return String(b);
|
||||
},
|
||||
|
||||
onTabChange(tab) {
|
||||
if (tab === 'current') {
|
||||
this.loadCurrentPositions();
|
||||
|
||||
@@ -192,7 +192,8 @@ export default {
|
||||
'/api/news': 'home.dashboard.widgetLabels.news',
|
||||
'/api/calendar/widget/birthdays': 'home.dashboard.widgetLabels.birthdays',
|
||||
'/api/calendar/widget/upcoming': 'home.dashboard.widgetLabels.upcoming',
|
||||
'/api/calendar/widget/mini': 'home.dashboard.widgetLabels.calendar'
|
||||
'/api/calendar/widget/mini': 'home.dashboard.widgetLabels.calendar',
|
||||
'/api/vocab/dashboard-widget': 'home.dashboard.widgetLabels.vocabCourses'
|
||||
}[endpoint];
|
||||
return key ? this.$t(key) : fallbackLabel;
|
||||
},
|
||||
@@ -211,7 +212,8 @@ export default {
|
||||
'News',
|
||||
'Geburtstage',
|
||||
'Nächste Termine',
|
||||
'Kalender'
|
||||
'Kalender',
|
||||
'Sprachkurse'
|
||||
];
|
||||
return {
|
||||
...widget,
|
||||
|
||||
Reference in New Issue
Block a user