Vrda's picture
Feature: EN/HR language toggle on login + full app i18n
81046e2
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { t, getStoredLang, storeLang } from '../i18n'
import type { Lang } from '../i18n'
const props = defineProps<{
googleClientId: string
lang: Lang
}>()
const emit = defineEmits<{
'google-login': [credential: string]
'update:lang': [lang: Lang]
}>()
const error = ref('')
const loading = ref(false)
// Check if we're inside an iframe (HuggingFace Spaces embeds apps in iframes)
const isInIframe = ref(false)
const directUrl = ref('')
try {
isInIframe.value = window.self !== window.top
} catch {
isInIframe.value = true // cross-origin iframe throws
}
// Build the direct .hf.space URL from the current page
if (isInIframe.value) {
const match = window.location.href.match(/spaces\/([^/]+)\/([^/?#]+)/)
if (match) {
directUrl.value = `https://${match[1].toLowerCase()}-${match[2].toLowerCase().replace(/_/g, '-')}.hf.space`
} else {
directUrl.value = window.location.origin
}
}
function switchLang(lang: Lang) {
storeLang(lang)
emit('update:lang', lang)
}
onMounted(() => {
checkRedirectResult()
if (document.getElementById('gsi-script')) {
initGsi()
return
}
const script = document.createElement('script')
script.id = 'gsi-script'
script.src = 'https://accounts.google.com/gsi/client'
script.async = true
script.defer = true
script.onload = initGsi
script.onerror = () => {
error.value = t('login.error.gsi', props.lang)
}
document.head.appendChild(script)
})
function checkRedirectResult() {
// After Google redirect, the credential comes back as a POST to the current page
}
function initGsi() {
const w = window as any
if (!w.google?.accounts?.id) {
setTimeout(initGsi, 100)
return
}
w.google.accounts.id.initialize({
client_id: props.googleClientId,
callback: handleCredentialResponse,
auto_select: false,
ux_mode: isInIframe.value ? 'redirect' : 'popup',
login_uri: isInIframe.value ? (directUrl.value || window.location.origin) + '/' : undefined,
})
w.google.accounts.id.renderButton(
document.getElementById('g-signin-btn'),
{
theme: 'filled_black',
size: 'large',
shape: 'rectangular',
text: 'signin_with',
width: 300,
}
)
}
function handleCredentialResponse(response: { credential: string }) {
if (response.credential) {
loading.value = true
emit('google-login', response.credential)
} else {
error.value = t('login.error.failed', props.lang)
}
}
</script>
<template>
<div class="login-page">
<div class="login-card">
<!-- Language toggle (top-right of card) -->
<div class="lang-toggle">
<button
:class="['lang-btn', { active: lang === 'en' }]"
@click="switchLang('en')"
>EN</button>
<span class="lang-sep">|</span>
<button
:class="['lang-btn', { active: lang === 'hr' }]"
@click="switchLang('hr')"
>HR</button>
</div>
<!-- WC3-style frame -->
<div class="login-frame">
<div class="login-icon">⚔️</div>
<h1>{{ t('login.title', lang) }}</h1>
<p class="subtitle">{{ t('login.subtitle', lang) }}</p>
<div class="gold-divider"></div>
<p class="login-prompt">{{ t('login.prompt', lang) }}</p>
<!-- Google Sign-In button renders here -->
<div id="g-signin-btn" class="g-btn-container"></div>
<p v-if="loading" class="login-loading">{{ t('login.loading', lang) }}</p>
<p v-if="error" class="login-error">{{ error }}</p>
<!-- Iframe hint -->
<p v-if="isInIframe" class="iframe-hint">
{{ t('login.iframe.hint', lang) }}<br>
<a :href="directUrl" target="_top" class="direct-link">{{ directUrl }}</a>
</p>
<div class="gold-divider"></div>
<p class="login-footer">
{{ t('login.footer.powered', lang) }}<br>
<span class="login-footer-dim">{{ t('login.footer.edition', lang) }}</span>
</p>
</div>
</div>
</div>
</template>
<style lang="scss">
.login-page {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #12122a 0%, #0a0a16 50%, #050510 100%);
}
.login-card {
width: 100%;
max-width: 480px;
padding: 2rem;
position: relative;
}
/* ---- Language Toggle ---- */
.lang-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
margin-bottom: 0.75rem;
}
.lang-btn {
background: none;
border: 1px solid transparent;
color: var(--wc3-text-dim, #7a6e5a);
font-family: var(--font-heading, 'Cinzel', serif);
font-size: 0.8rem;
letter-spacing: 1.5px;
padding: 0.3rem 0.75rem;
cursor: pointer;
border-radius: 3px;
transition: all 0.15s;
}
.lang-btn:hover {
color: var(--wc3-gold, #c8aa6e);
border-color: var(--wc3-gold-dim, #8b7b4f);
}
.lang-btn.active {
color: var(--wc3-gold-bright, #f0d060);
border-color: var(--wc3-gold, #c8aa6e);
background: rgba(200, 170, 110, 0.08);
text-shadow: 0 0 8px rgba(200, 170, 110, 0.3);
}
.lang-sep {
color: var(--wc3-border, #5a4a2a);
font-size: 0.8rem;
}
.login-frame {
background: linear-gradient(180deg, #13132a 0%, #0d0d1f 100%);
border: 2px solid var(--wc3-gold-dim, #8b7b4f);
border-radius: 6px;
padding: 2.5rem 2rem;
text-align: center;
box-shadow:
0 0 40px rgba(200, 170, 110, 0.08),
inset 0 1px 0 rgba(200, 170, 110, 0.1);
}
.login-icon {
font-size: 3rem;
margin-bottom: 0.75rem;
filter: drop-shadow(0 0 10px rgba(200, 170, 110, 0.4));
}
.login-frame h1 {
font-family: var(--font-display, 'Cinzel Decorative', serif);
font-size: 1.5rem;
font-weight: 900;
background: linear-gradient(180deg, #f0d060 0%, #c8aa6e 50%, #8b7b4f 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 1px;
margin-bottom: 0.25rem;
word-break: keep-all;
}
.login-frame .subtitle {
font-family: var(--font-body, 'Crimson Text', serif);
color: var(--wc3-text-dim, #7a6e5a);
font-style: italic;
font-size: 0.95rem;
}
.login-prompt {
font-family: var(--font-heading, 'Cinzel', serif);
color: var(--wc3-gold, #c8aa6e);
font-size: 0.9rem;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 1.25rem;
}
.g-btn-container {
display: flex;
justify-content: center;
min-height: 44px;
margin-bottom: 0.5rem;
}
.login-loading {
color: var(--wc3-gold, #c8aa6e);
font-size: 0.85rem;
margin-top: 0.75rem;
animation: pulse 1.5s ease-in-out infinite;
}
.login-error {
color: #ff6666;
font-size: 0.85rem;
margin-top: 0.75rem;
}
.iframe-hint {
color: var(--wc3-text-dim, #7a6e5a);
font-size: 0.78rem;
margin-top: 1rem;
line-height: 1.6;
}
.direct-link {
color: var(--wc3-gold, #c8aa6e);
text-decoration: underline;
text-underline-offset: 2px;
word-break: break-all;
}
.direct-link:hover {
color: var(--wc3-gold-bright, #f0d060);
}
.login-footer {
font-size: 0.8rem;
color: var(--wc3-text-dim, #7a6e5a);
font-style: italic;
line-height: 1.6;
}
.login-footer-dim {
color: var(--wc3-text-muted, #555060);
font-size: 0.75rem;
}
</style>