From d9a46dcb3490086cb0eba251af98a435e51b0a35 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 3 Sep 2026 14:18:14 +0200 Subject: [PATCH] feat(news): implement news section with loading state and error handling in NoLoginView --- backend/controllers/newsController.js | 3 +- backend/routers/newsRouter.js | 5 +- backend/services/newsService.js | 9 +- frontend/src/i18n/locales/ceb/home.json | 7 ++ frontend/src/i18n/locales/de/home.json | 7 ++ frontend/src/i18n/locales/en/home.json | 7 ++ frontend/src/i18n/locales/es/home.json | 7 ++ frontend/src/i18n/locales/fr/home.json | 7 ++ frontend/src/views/home/NoLoginView.vue | 125 +++++++++++++++++++++++- 9 files changed, 168 insertions(+), 9 deletions(-) diff --git a/backend/controllers/newsController.js b/backend/controllers/newsController.js index e7bda0e..15c53f8 100755 --- a/backend/controllers/newsController.js +++ b/backend/controllers/newsController.js @@ -7,11 +7,12 @@ import newsService from '../services/newsService.js'; export default { async getNews(req, res) { const counter = Math.max(0, parseInt(req.query.counter, 10) || 0); + const count = Math.min(6, Math.max(1, parseInt(req.query.count, 10) || 1)); const language = (req.query.language || 'de').slice(0, 10); const category = (req.query.category || 'top').slice(0, 50); try { - const { results, nextPage } = await newsService.getNews({ counter, language, category }); + const { results, nextPage } = await newsService.getNews({ counter, count, language, category }); res.json({ results, nextPage }); } catch (error) { console.error('News getNews:', error); diff --git a/backend/routers/newsRouter.js b/backend/routers/newsRouter.js index 7851805..935ee70 100755 --- a/backend/routers/newsRouter.js +++ b/backend/routers/newsRouter.js @@ -1,9 +1,10 @@ import { Router } from 'express'; -import { authenticate } from '../middleware/authMiddleware.js'; import newsController from '../controllers/newsController.js'; const router = Router(); -router.get('/', authenticate, newsController.getNews.bind(newsController)); +// News are shown on the public landing page as well as in the dashboard. +// The endpoint only exposes cached third-party headlines, no user data. +router.get('/', newsController.getNews.bind(newsController)); export default router; diff --git a/backend/services/newsService.js b/backend/services/newsService.js index b041458..014e8af 100755 --- a/backend/services/newsService.js +++ b/backend/services/newsService.js @@ -104,20 +104,21 @@ async function getCachedNews({ language = 'de', category = 'top', minArticles = * @param {number} options.counter - Index des Artikels (0 = erster, 1 = zweiter, …) * @param {string} [options.language] * @param {string} [options.category] + * @param {number} [options.count] - Anzahl aufeinanderfolgender Artikel * @returns {Promise<{ results: Array, nextPage: string|null }>} */ -async function getNews({ counter = 0, language = 'de', category = 'top' }) { +async function getNews({ counter = 0, count = 1, language = 'de', category = 'top' }) { const neededIndex = Math.max(0, counter); + const requestedCount = Math.min(6, Math.max(1, Number.parseInt(count, 10) || 1)); // Mindestens so viele Artikel laden wie benötigt const articles = await getCachedNews({ language, category, - minArticles: neededIndex + 1 + minArticles: neededIndex + requestedCount }); - const single = articles[neededIndex] ? [articles[neededIndex]] : []; - return { results: single, nextPage: null }; + return { results: articles.slice(neededIndex, neededIndex + requestedCount), nextPage: null }; } export default { getNews }; diff --git a/frontend/src/i18n/locales/ceb/home.json b/frontend/src/i18n/locales/ceb/home.json index 6e08f67..6863dcc 100755 --- a/frontend/src/i18n/locales/ceb/home.json +++ b/frontend/src/i18n/locales/ceb/home.json @@ -80,6 +80,13 @@ "title": "Sugdi na", "text": "Pwede na nimo gamiton, sulayan ug hatagan og feedback. Pagrehistro pinaagi sa “{register}” o sugdi ang random chat." }, + "news": { + "kicker": "Bag-ong balita", + "title": "Balita", + "loading": "Gikarga ang balita …", + "empty": "Walay balita nga anaa karon.", + "unavailable": "Dili magamit ang balita karon." + }, "languageTrainerSeo": { "title": "Language trainers para sa adlaw-adlaw (beginner)", "introBefore": "Ang YourPart adunay duha ka", diff --git a/frontend/src/i18n/locales/de/home.json b/frontend/src/i18n/locales/de/home.json index 75b0da7..8e7b66f 100755 --- a/frontend/src/i18n/locales/de/home.json +++ b/frontend/src/i18n/locales/de/home.json @@ -80,6 +80,13 @@ "title": "Mitmachen", "text": "Du kannst die Plattform bereits nutzen, testen und Feedback geben. Registriere dich über „{register}“ oder starte unverbindlich den Random‑Chat." }, + "news": { + "kicker": "Aktuelles", + "title": "News", + "loading": "News werden geladen …", + "empty": "Zurzeit sind keine News verfügbar.", + "unavailable": "News sind gerade nicht verfügbar." + }, "languageTrainerSeo": { "title": "Sprachtrainer fuer den Alltag (Anfaenger)", "introBefore": "YourPart bietet zwei", diff --git a/frontend/src/i18n/locales/en/home.json b/frontend/src/i18n/locales/en/home.json index 5b4b6b3..01e685a 100755 --- a/frontend/src/i18n/locales/en/home.json +++ b/frontend/src/i18n/locales/en/home.json @@ -80,6 +80,13 @@ "title": "Get started", "text": "You can already use, test and give feedback. Register via “{register}” or start the random chat." }, + "news": { + "kicker": "Latest", + "title": "News", + "loading": "Loading news …", + "empty": "There is no news available at the moment.", + "unavailable": "News are currently unavailable." + }, "languageTrainerSeo": { "title": "Language trainers for everyday use (beginners)", "introBefore": "YourPart offers two", diff --git a/frontend/src/i18n/locales/es/home.json b/frontend/src/i18n/locales/es/home.json index f20507e..90eb68c 100755 --- a/frontend/src/i18n/locales/es/home.json +++ b/frontend/src/i18n/locales/es/home.json @@ -80,6 +80,13 @@ "title": "Participa", "text": "Ya puedes usar la plataforma, probarla y darnos tu opinión. Regístrate mediante “{register}” o inicia el chat aleatorio sin compromiso." }, + "news": { + "kicker": "Actualidad", + "title": "Noticias", + "loading": "Cargando noticias …", + "empty": "No hay noticias disponibles en este momento.", + "unavailable": "Las noticias no están disponibles en este momento." + }, "languageTrainerSeo": { "title": "Entrenadores de idiomas para el dia a dia (principiantes)", "introBefore": "YourPart ofrece dos", diff --git a/frontend/src/i18n/locales/fr/home.json b/frontend/src/i18n/locales/fr/home.json index 74be80a..6a2b170 100755 --- a/frontend/src/i18n/locales/fr/home.json +++ b/frontend/src/i18n/locales/fr/home.json @@ -80,6 +80,13 @@ "title": "Se joindre à", "text": "Vous pouvez déjà utiliser la plateforme, la tester et donner votre avis. Inscrivez-vous via « {register} » ou démarrez le chat aléatoire sans engagement." }, + "news": { + "kicker": "Actualités", + "title": "Nouvelles", + "loading": "Chargement des nouvelles …", + "empty": "Aucune nouvelle n’est disponible pour le moment.", + "unavailable": "Les nouvelles ne sont pas disponibles actuellement." + }, "languageTrainerSeo": { "title": "Formations langues pour le quotidien (debutants)", "introBefore": "YourPart propose deux", diff --git a/frontend/src/views/home/NoLoginView.vue b/frontend/src/views/home/NoLoginView.vue index 9864e32..fd86d20 100755 --- a/frontend/src/views/home/NoLoginView.vue +++ b/frontend/src/views/home/NoLoginView.vue @@ -101,6 +101,28 @@ +
+
+
+ {{ $t('home.nologin.news.kicker') }} +

{{ $t('home.nologin.news.title') }}

+
+ {{ $t('home.nologin.news.loading') }} +
+

{{ $t('home.nologin.news.unavailable') }}

+
+
+ {{ formatNewsDate(article.pubDate) }} + + {{ article.title }} + +

{{ article.title }}

+

{{ summarizeNews(article.description) }}

+
+
+

{{ $t('home.nologin.news.empty') }}

+
+

{{ $t('home.nologin.languageTrainerSeo.title') }}

@@ -160,6 +182,9 @@ export default { oauthProviders: [], oauthLoading: false, isStoryCollapsed: true, + news: [], + newsLoading: true, + newsError: false, }; }, components: { @@ -190,6 +215,36 @@ export default { this.oauthProviders = []; } }, + getNewsLanguage() { + const language = String(this.$i18n?.locale || 'de').toLowerCase(); + return ['de', 'en', 'es', 'fr'].includes(language) ? language : 'en'; + }, + async loadNews() { + this.newsLoading = true; + this.newsError = false; + try { + const { data } = await apiClient.get('/api/news', { + params: { language: this.getNewsLanguage(), category: 'top', count: 3 } + }); + this.news = Array.isArray(data?.results) ? data.results.filter((article) => article?.title) : []; + } catch (error) { + this.news = []; + this.newsError = true; + } finally { + this.newsLoading = false; + } + }, + formatNewsDate(dateStr) { + const date = new Date(dateStr); + if (Number.isNaN(date.getTime())) return ''; + return date.toLocaleDateString(this.$i18n?.locale || 'de-DE', { + day: 'numeric', month: 'short', year: 'numeric' + }); + }, + summarizeNews(description) { + const text = String(description || '').replace(/\s+/g, ' ').trim(); + return text.length > 210 ? `${text.slice(0, 207).trimEnd()}…` : text; + }, startOAuthLogin(providerSlug) { if (this.oauthLoading) { return; @@ -229,7 +284,7 @@ export default { } }, async created() { - await this.loadOAuthProviders(); + await Promise.all([this.loadOAuthProviders(), this.loadNews()]); }, mounted() { this.$nextTick(() => { @@ -554,6 +609,71 @@ export default { border-radius: 4px; } +.public-news { + width: min(100%, 1120px); + margin: 24px auto 0; + padding: 1.25rem; +} + +.public-news__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.public-news__header .panel-kicker { + margin-bottom: 0.45rem; +} + +.public-news__header h2 { + margin: 0; +} + +.public-news__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.9rem; +} + +.public-news__article { + display: flex; + flex-direction: column; + gap: 0.45rem; + padding: 1rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.7); +} + +.public-news__date, +.public-news__status { + color: var(--color-text-secondary); + font-size: 0.85rem; +} + +.public-news__title { + margin: 0; + color: var(--color-text-primary); + font-size: 1rem; + line-height: 1.35; + font-weight: 700; + text-decoration: none; +} + +a.public-news__title:hover { + color: var(--color-primary); + text-decoration: underline; +} + +.public-news__description { + margin: 0; + color: var(--color-text-secondary); + font-size: 0.9rem; + line-height: 1.45; +} + .seo-content h1 { font-size: 28px; margin: 0 0 8px 0; @@ -626,7 +746,8 @@ export default { .story-columns, .access-split, .login-fields, - .oauth-provider-list { + .oauth-provider-list, + .public-news__grid { grid-template-columns: 1fr; } }