Files
yourpart3/frontend/src/views/home/NoLoginView.vue
Torsten Schulz (local) a7b51365a0
All checks were successful
Deploy to production / deploy (push) Successful in 1m53s
refactor(NoLoginView): remove unused RandomChatDialog and clean up login panel
2026-05-21 11:24:41 +02:00

635 lines
19 KiB
Vue

<template>
<div class="no-login-view">
<div class="home-structure">
<div class="mascot">
<Character3D gender="male" :lightweight="true" />
</div>
<div class="actions">
<section class="actions-panel actions-panel--story surface-card" :class="{ 'collapsed': isStoryCollapsed }">
<div class="panel-intro">
<span class="panel-kicker">Dein Einstieg</span>
<h1 class="home-main-headline">{{ $t('home.nologin.welcome') }}</h1>
<p>{{ $t('home.nologin.description') }}</p>
<button type="button" class="toggle-story" @click="isStoryCollapsed = !isStoryCollapsed">
{{ isStoryCollapsed ? 'Mehr anzeigen' : 'Weniger anzeigen' }}
</button>
</div>
<div class="story-highlight">
<p v-html="$t('home.nologin.storyTeaser')" />
</div>
<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>
</div>
<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.privacyInfo.title') }}</h3>
<p>{{ $t('home.nologin.privacyInfo.text') }}</p>
</article>
</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">
<h2>{{ $t('home.nologin.login.submit') }}</h2>
<div class="oauth-section" v-if="oauthProviders.length">
<div class="oauth-section__header">
<span class="panel-kicker">Externe Konten</span>
<p class="oauth-section__text">{{ formattedOAuthProviders }}</p>
</div>
<div class="oauth-provider-list">
<button
v-for="provider in oauthProviders"
:key="provider.slug"
type="button"
class="oauth-provider-button"
:class="`oauth-provider-button--${provider.slug}`"
:disabled="oauthLoading"
@click="startOAuthLogin(provider.slug)"
>
Mit {{ provider.label }} fortfahren
</button>
</div>
</div>
<div class="login-fields">
<input ref="usernameInput" v-model="username" size="20" type="text" :placeholder="$t('home.nologin.login.name')"
:title="$t('home.nologin.login.namedescription')" @keydown.enter="focusPassword">
<input v-model="password" size="20" type="password"
:placeholder="$t('home.nologin.login.password')"
:title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin"
ref="passwordInput">
</div>
<div class="login-submit-row">
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
</div>
<div class="stay-logged-in-row">
<label class="stay-logged-in-label">
<input v-model="rememberMe" class="stay-logged-in-checkbox" type="checkbox">
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
</label>
</div>
<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>
</div>
</section>
</div>
<div class="mascot">
<Character3D gender="female" :lightweight="true" />
</div>
<RegisterDialog ref="registerDialog" />
<PasswordResetDialog ref="passwordResetDialog" />
</div>
<section class="seo-content surface-card" aria-label="Sprachtrainer">
<h2>{{ $t('home.nologin.languageTrainerSeo.title') }}</h2>
<p>
{{ $t('home.nologin.languageTrainerSeo.introBefore') }}
<strong>{{ $t('home.nologin.languageTrainerSeo.beginnerLabel') }}</strong>
{{ $t('home.nologin.languageTrainerSeo.introMiddle') }}
<router-link to="/vokabeltrainer">{{ $t('home.nologin.languageTrainerSeo.vocabTrainerLinkText') }}</router-link>:
<strong>{{ $t('home.nologin.languageTrainerSeo.bisayaForGerman') }}</strong>
{{ $t('home.nologin.languageTrainerSeo.andConnector') }}
<strong>{{ $t('home.nologin.languageTrainerSeo.germanForBisaya') }}</strong>.
{{ $t('home.nologin.languageTrainerSeo.introAfter') }}
</p>
<p>
<strong>{{ $t('home.nologin.languageTrainerSeo.honestLabel') }}</strong>
{{ $t('home.nologin.languageTrainerSeo.honestTextBefore') }}
<strong>{{ $t('home.nologin.languageTrainerSeo.rangeLabel') }}</strong>
{{ $t('home.nologin.languageTrainerSeo.honestTextMiddle') }}
<strong>{{ $t('home.nologin.languageTrainerSeo.belowA2Label') }}</strong>,
{{ $t('home.nologin.languageTrainerSeo.honestTextAfter') }}
</p>
<h3>{{ $t('home.nologin.languageTrainerSeo.germanSectionTitle') }}</h3>
<p>
{{ $t('home.nologin.languageTrainerSeo.germanSectionTextBefore') }}
<strong>{{ $t('home.nologin.languageTrainerSeo.germanSectionStrong') }}</strong>
{{ $t('home.nologin.languageTrainerSeo.germanSectionTextAfter') }}
</p>
<h3>{{ $t('home.nologin.languageTrainerSeo.bisayaSectionTitle') }}</h3>
<p>
{{ $t('home.nologin.languageTrainerSeo.bisayaSectionText') }}
</p>
<p>
{{ $t('home.nologin.languageTrainerSeo.allCoursesLabel') }}
<router-link to="/vokabeltrainer">{{ $t('home.nologin.languageTrainerSeo.vocabTrainerLinkText') }}</router-link>
</p>
</section>
</div>
</template>
<script>
import RegisterDialog from '@/dialogues/auth/RegisterDialog.vue';
import PasswordResetDialog from '@/dialogues/auth/PasswordResetDialog.vue';
import Character3D from '@/components/Character3D.vue';
import apiClient from '@/utils/axios.js';
import { mapActions } from 'vuex';
export default {
name: 'HomeNoLoginView',
data() {
return {
username: '',
password: '',
rememberMe: true,
oauthProviders: [],
oauthLoading: false,
isStoryCollapsed: true,
};
},
components: {
RegisterDialog,
PasswordResetDialog,
Character3D,
},
methods: {
...mapActions(['login']),
getOAuthBaseUrl() {
const baseUrl = apiClient.defaults.baseURL || window.location.origin;
return String(baseUrl).replace(/\/api\/?$/, '').replace(/\/$/, '');
},
openRegisterDialog() {
const dlg = this.$refs.registerDialog;
if (dlg && typeof dlg.open === 'function') dlg.open();
},
openPasswordResetDialog() {
const dlg = this.$refs.passwordResetDialog;
if (dlg && typeof dlg.open === 'function') dlg.open();
},
async loadOAuthProviders() {
try {
const response = await apiClient.get('/api/auth/oauth/providers');
this.oauthProviders = (response.data.providers || []).filter((provider) => provider.configured);
} catch (error) {
console.error('Error loading OAuth providers:', error);
this.oauthProviders = [];
}
},
startOAuthLogin(providerSlug) {
if (this.oauthLoading) {
return;
}
this.oauthLoading = true;
window.location.href = `${this.getOAuthBaseUrl()}/api/auth/oauth/${encodeURIComponent(providerSlug)}/start`;
},
focusPassword() {
this.$refs.passwordInput.focus();
},
async doLogin() {
try {
const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password });
this.login({ user: response.data, rememberMe: this.rememberMe });
} catch (error) {
const errorKey = error?.response?.data?.error || 'network';
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
}
}
},
computed: {
formattedOAuthProviders() {
if (!this.oauthProviders || !this.oauthProviders.length) return '';
try {
const labels = this.oauthProviders.map(p => p.label);
if (typeof Intl !== 'undefined' && Intl.ListFormat) {
const lf = new Intl.ListFormat(this.$i18n?.locale || 'de', { style: 'long', type: 'conjunction' });
return lf.format(labels);
}
if (labels.length === 1) return labels[0];
if (labels.length === 2) return `${labels[0]} oder ${labels[1]}`;
return `${labels.slice(0, -1).join(', ')} oder ${labels[labels.length - 1]}`;
} catch (_) {
return this.oauthProviders.map(p => p.label).join(', ');
}
}
},
async created() {
await this.loadOAuthProviders();
},
mounted() {
this.$nextTick(() => {
this.$refs.usernameInput?.focus?.();
});
}
};
</script>
<style scoped>
.home-structure {
display: flex;
align-items: stretch;
justify-content: center;
gap: 1.4rem;
width: 100%;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.home-structure>div {
text-align: center;
display: flex;
min-height: 0;
}
.mascot {
flex: 0 0 clamp(180px, 22%, 280px);
justify-content: center;
align-items: stretch;
background: linear-gradient(180deg, #fff5e8 0%, #fce7ca 100%);
border: 1px solid rgba(248, 162, 43, 0.16);
border-radius: var(--radius-lg);
box-shadow: 0 10px 24px rgba(93, 64, 55, 0.08);
overflow: hidden;
align-self: center;
height: clamp(320px, 68vh, 560px);
min-height: 320px;
max-height: 560px;
}
.actions {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 1rem;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.actions-panel {
flex: 0 0 calc(40% - 0.5rem);
height: calc(40% - 0.5rem);
max-height: calc(40% - 0.5rem);
min-height: 0;
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: 1.2rem 1.25rem;
text-align: left;
}
.actions-panel--access {
flex: 0 0 auto;
height: auto;
max-height: none;
overflow: visible;
}
.actions-panel h1,
.actions-panel h2,
.actions-panel h3 {
width: 100%;
}
.home-main-headline {
font-size: inherit;
font-weight: inherit;
margin: 0;
line-height: inherit;
}
.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);
}
/* Collapsed story panel to save vertical space while keeping content in DOM for SEO */
.actions-panel--story.collapsed {
max-height: 260px;
overflow: hidden;
}
.actions-panel--story .toggle-story {
margin-top: 0.6rem;
background: transparent;
border: none;
color: #8a5411;
font-weight: 700;
cursor: pointer;
padding: 0.2rem 0.4rem;
}
/* When collapsed, de-emphasize child blocks visually */
.actions-panel--story.collapsed .story-block,
.actions-panel--story.collapsed .story-columns,
.actions-panel--story.collapsed .story-cta {
opacity: 0.0;
height: 0;
margin: 0;
padding: 0;
overflow: hidden;
transition: opacity 200ms ease, height 200ms ease;
}
/* Slightly reduce mascot height to make login reachable */
.mascot {
height: clamp(260px, 50vh, 420px);
}
.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 {
display: grid;
gap: 0.75rem;
}
.login-panel h2 {
margin: 0;
font-size: clamp(1.25rem, 2vw, 1.65rem);
line-height: 1.15;
}
.oauth-section {
padding: 0.9rem 1rem;
border-radius: var(--radius-lg);
background: linear-gradient(180deg, rgba(255, 248, 238, 0.95), rgba(255, 243, 229, 0.95));
border: 1px solid rgba(248, 162, 43, 0.14);
display: grid;
gap: 0.8rem;
}
.oauth-section__header {
display: grid;
gap: 0.35rem;
}
.oauth-section__text {
margin: 0;
color: var(--color-text-secondary);
line-height: 1.35;
}
.oauth-provider-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.55rem;
}
.oauth-provider-button {
padding: 0.72rem 0.9rem;
border-radius: 14px;
border: 1px solid rgba(93, 64, 55, 0.12);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(247, 242, 236, 0.94));
color: #4b342e;
font-weight: 700;
text-align: left;
box-shadow: 0 6px 16px rgba(93, 64, 55, 0.06);
}
.oauth-provider-button:hover:not(:disabled) {
transform: translateY(-1px);
}
.oauth-provider-button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.oauth-provider-button--google {
border-left: 4px solid #4285f4;
}
.oauth-provider-button--microsoft {
border-left: 4px solid #0078d4;
}
.oauth-provider-button--keycloak {
border-left: 4px solid #d67f2f;
}
.oauth-provider-button--ory {
border-left: 4px solid #0f766e;
}
.oauth-provider-button--zitadel {
border-left: 4px solid #0f5132;
}
.login-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
}
.login-submit-row {
display: flex;
justify-content: flex-start;
}
.primary-action {
align-self: flex-start;
}
.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;
margin-top: 0.1rem;
gap: 0.9rem;
}
.stay-logged-in-row {
width: 100%;
display: flex;
justify-content: flex-start;
margin-top: 0.1rem;
}
.stay-logged-in-label {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
font-size: 0.95rem;
}
.stay-logged-in-checkbox {
width: 16px;
min-width: 16px;
height: 16px;
min-height: 16px;
margin: 0;
padding: 0;
flex: 0 0 16px;
accent-color: var(--color-primary-orange);
box-shadow: none;
}
.seo-content {
max-width: 1000px;
margin: 24px auto 0 auto;
padding: 0 16px 40px 16px;
color: #5D4037;
background-color: #FFF4F0;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 4px;
}
.seo-content h1 {
font-size: 28px;
margin: 0 0 8px 0;
}
.seo-content h2 {
font-size: 20px;
margin: 18px 0 6px 0;
color: #444;
}
.seo-content p {
line-height: 1.6;
margin: 0 0 8px 0;
}
.seo-content ul {
margin: 0 0 8px 20px;
}
/* Scrollbarer Bereich für "Was dich erwartet" */
.seo-content .expected {
max-height: 200px;
overflow: auto;
padding-right: 8px;
background-color: #fdf1db;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
margin: 12px 0;
}
.no-login-view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
}
@media (max-width: 960px) {
.home-structure {
flex-direction: column;
gap: 1rem;
overflow: auto;
}
.mascot {
min-height: 260px;
height: 260px;
flex: 0 0 260px;
}
.actions {
min-height: auto;
height: auto;
overflow: visible;
justify-content: flex-start;
}
.actions-panel {
flex: 0 0 auto;
height: auto;
max-height: none;
min-height: 260px;
}
.story-columns,
.access-split,
.login-fields,
.oauth-provider-list {
grid-template-columns: 1fr;
}
}
</style>