- Added a prebuild script in package.json to copy audio assets for smoother audio playback. - Updated App.vue to ensure the router is ready before displaying content, addressing issues with hard refreshes. - Introduced a "Tap to start" feature in SplashScreen.vue to comply with browser autoplay policies for audio. - Enhanced playLoopStart function in useLoginSounds.ts to utilize the Web Audio API for better audio control. - Removed unnecessary redirect in router index.ts for cleaner routing logic. - Improved Dashboard.vue and Login.vue styles for better visual hierarchy and user engagement during transitions.
284 lines
9.6 KiB
Vue
284 lines
9.6 KiB
Vue
<template>
|
|
<div class="min-h-screen flex items-center justify-center p-4 relative z-10 login-fly-perspective">
|
|
<div class="w-full max-w-md relative z-20">
|
|
<!-- Login Card - flies towards user on success -->
|
|
<div
|
|
class="glass-card p-8 pt-20 relative login-card overflow-visible"
|
|
:class="{ 'login-fly-towards': whooshAway }"
|
|
>
|
|
<!-- Logo - half in, half out of container -->
|
|
<div class="absolute -top-10 left-1/2 -translate-x-1/2 z-10">
|
|
<div class="logo-gradient-border">
|
|
<img
|
|
src="/assets/img/favico.svg"
|
|
alt="Archipelago"
|
|
class="w-20 h-20"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Title -->
|
|
<h1 class="text-2xl font-semibold text-white/96 text-center mb-8 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">
|
|
<span v-if="isSetupMode && !isSetup">Set Up Your Node</span>
|
|
<span v-else>Welcome to Archipelago</span>
|
|
</h1>
|
|
|
|
<!-- Error Message -->
|
|
<div v-if="error" class="mb-4 p-3 bg-red-500/20 border border-red-500/40 rounded-lg text-red-200 text-sm">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<!-- Setup Mode: Password Setup -->
|
|
<template v-if="isSetupMode && !isSetup">
|
|
<div class="mb-4 p-4 bg-white/5 border border-white/10 rounded-lg text-white/80 text-sm">
|
|
<p class="mb-2">Create a password to secure your Archipelago node.</p>
|
|
<p class="text-white/60 text-xs">This password will be required to access your node.</p>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="password" class="block text-sm font-medium text-white/80 mb-2">
|
|
Password
|
|
</label>
|
|
<input
|
|
id="password"
|
|
v-model="password"
|
|
type="password"
|
|
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
|
placeholder="Enter a password (min 8 characters)"
|
|
@keyup.enter="handleSetup"
|
|
:disabled="loading"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<label for="confirmPassword" class="block text-sm font-medium text-white/80 mb-2">
|
|
Confirm Password
|
|
</label>
|
|
<input
|
|
id="confirmPassword"
|
|
v-model="confirmPassword"
|
|
type="password"
|
|
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
|
placeholder="Confirm your password"
|
|
@keyup.enter="handleSetup"
|
|
:disabled="loading"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
@click="handleSetup"
|
|
:disabled="loading || !password || password !== confirmPassword"
|
|
class="w-full glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span v-if="!loading">Set Up Node</span>
|
|
<span v-else class="flex items-center justify-center">
|
|
<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Setting up...
|
|
</span>
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Normal Login Mode -->
|
|
<template v-else>
|
|
<div class="mb-6">
|
|
<label for="password" class="block text-sm font-medium text-white/80 mb-2">
|
|
Password
|
|
</label>
|
|
<input
|
|
id="password"
|
|
v-model="password"
|
|
type="password"
|
|
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
|
placeholder="Enter your password"
|
|
@keyup.enter="handleLogin"
|
|
:disabled="loading"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
@click="handleLogin"
|
|
:disabled="loading || !password"
|
|
class="w-full glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span v-if="!loading">Login</span>
|
|
<span v-else class="flex items-center justify-center">
|
|
<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Logging in...
|
|
</span>
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Footer Links -->
|
|
<div class="mt-6 text-center text-sm text-white/60">
|
|
<a href="#" class="hover:text-white/80 transition-colors">Forgot password?</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Replay Intro - Bottom of Page -->
|
|
<div class="mt-8 text-center">
|
|
<button
|
|
@click="replayIntro"
|
|
class="text-xs text-white/50 hover:text-white/70 transition-colors underline-offset-2 hover:underline"
|
|
>
|
|
Replay Intro
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAppStore } from '../stores/app'
|
|
import { useLoginTransitionStore } from '../stores/loginTransition'
|
|
import { rpcClient } from '../api/rpc-client'
|
|
import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh } from '@/composables/useLoginSounds'
|
|
|
|
const router = useRouter()
|
|
const store = useAppStore()
|
|
const loginTransition = useLoginTransitionStore()
|
|
|
|
const password = ref('')
|
|
const confirmPassword = ref('')
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const isSetup = ref(false)
|
|
const whooshAway = ref(false)
|
|
|
|
// Check if we're in setup mode (original StartOS node setup)
|
|
const isSetupMode = computed(() => {
|
|
return import.meta.env.VITE_DEV_MODE === 'setup'
|
|
})
|
|
|
|
onMounted(async () => {
|
|
if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
|
|
startSynthwave()
|
|
} else {
|
|
sessionStorage.removeItem('archipelago_from_splash')
|
|
}
|
|
if (isSetupMode.value) {
|
|
try {
|
|
const result = await rpcClient.call<boolean>({ method: 'auth.isSetup', params: {} })
|
|
isSetup.value = Boolean(result)
|
|
} catch (err) {
|
|
console.error('Failed to check setup status:', err)
|
|
isSetup.value = false
|
|
}
|
|
} else {
|
|
isSetup.value = true
|
|
}
|
|
})
|
|
|
|
|
|
async function handleSetup() {
|
|
if (!password.value || password.value.length < 8) {
|
|
error.value = 'Password must be at least 8 characters'
|
|
return
|
|
}
|
|
|
|
if (password.value !== confirmPassword.value) {
|
|
error.value = 'Passwords do not match'
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
await rpcClient.call({
|
|
method: 'auth.setup',
|
|
params: { password: password.value }
|
|
})
|
|
|
|
stopSynthwave()
|
|
whooshAway.value = true
|
|
playLoginSuccessWhoosh()
|
|
loginTransition.setJustLoggedIn(true)
|
|
await store.login(password.value)
|
|
await new Promise(r => setTimeout(r, 520))
|
|
router.replace({ name: 'home' })
|
|
} catch (err) {
|
|
whooshAway.value = false
|
|
error.value = err instanceof Error ? err.message : 'Setup failed. Please try again.'
|
|
startSynthwave()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleLogin() {
|
|
if (!password.value) return
|
|
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
await store.login(password.value)
|
|
stopSynthwave()
|
|
whooshAway.value = true
|
|
playLoginSuccessWhoosh()
|
|
loginTransition.setJustLoggedIn(true)
|
|
await new Promise(r => setTimeout(r, 520))
|
|
router.replace({ name: 'home' })
|
|
} catch (err) {
|
|
whooshAway.value = false
|
|
error.value = err instanceof Error ? err.message : 'Login failed. Please check your password.'
|
|
startSynthwave()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function replayIntro() {
|
|
// Clear the intro seen flag
|
|
localStorage.removeItem('neode_intro_seen')
|
|
// Navigate to root to trigger splash screen
|
|
window.location.href = '/'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Perspective for 3D fly effect */
|
|
.login-fly-perspective {
|
|
perspective: 1200px;
|
|
perspective-origin: center center;
|
|
}
|
|
|
|
.login-card {
|
|
transform-style: preserve-3d;
|
|
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
|
opacity 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
|
filter 0.5s ease-out;
|
|
}
|
|
|
|
/* Fly towards user - card zooms forward as it transitions out */
|
|
.login-fly-towards {
|
|
animation: login-fly-towards 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
|
}
|
|
|
|
@keyframes login-fly-towards {
|
|
0% {
|
|
transform: translateZ(0) scale(1);
|
|
opacity: 1;
|
|
filter: blur(0);
|
|
}
|
|
60% {
|
|
transform: translateZ(180px) scale(1.4);
|
|
opacity: 0.95;
|
|
filter: blur(2px);
|
|
}
|
|
100% {
|
|
transform: translateZ(400px) scale(2);
|
|
opacity: 0;
|
|
filter: blur(8px);
|
|
}
|
|
}
|
|
</style>
|