Spaces:
Running
Running
| <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> | |