feat(news): implement news section with loading state and error handling in NoLoginView
All checks were successful
Deploy to production / deploy (push) Successful in 3m8s

This commit is contained in:
Torsten Schulz (local)
2026-09-03 14:18:14 +02:00
parent c4977ec826
commit d9a46dcb34
9 changed files with 168 additions and 9 deletions

View File

@@ -101,6 +101,28 @@
<PasswordResetDialog ref="passwordResetDialog" />
</div>
<section class="public-news surface-card" aria-labelledby="public-news-title">
<div class="public-news__header">
<div>
<span class="panel-kicker">{{ $t('home.nologin.news.kicker') }}</span>
<h2 id="public-news-title">{{ $t('home.nologin.news.title') }}</h2>
</div>
<span v-if="newsLoading" class="public-news__status">{{ $t('home.nologin.news.loading') }}</span>
</div>
<p v-if="newsError" class="public-news__status">{{ $t('home.nologin.news.unavailable') }}</p>
<div v-else-if="news.length" class="public-news__grid">
<article v-for="article in news" :key="article.article_id || article.link || article.title" class="public-news__article">
<span v-if="article.pubDate" class="public-news__date">{{ formatNewsDate(article.pubDate) }}</span>
<a v-if="article.link" :href="article.link" target="_blank" rel="noopener noreferrer" class="public-news__title">
{{ article.title }}
</a>
<h3 v-else class="public-news__title">{{ article.title }}</h3>
<p v-if="article.description" class="public-news__description">{{ summarizeNews(article.description) }}</p>
</article>
</div>
<p v-else-if="!newsLoading" class="public-news__status">{{ $t('home.nologin.news.empty') }}</p>
</section>
<section class="seo-content surface-card" aria-label="Sprachtrainer">
<h2>{{ $t('home.nologin.languageTrainerSeo.title') }}</h2>
<p>
@@ -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;
}
}