635 lines
19 KiB
Vue
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>
|