Enhance UI components and improve user notifications
- Updated App.vue to include a toast notification system for new messages, enhancing user engagement. - Modified SplashScreen.vue to streamline the intro text display with improved typing effects. - Added Montserrat font styles in style.css for better typography across the application. - Improved controller navigation in useControllerNav.ts to support enhanced focus management and sound feedback. - Updated routing logic in index.ts to redirect authenticated users from the login page to the home page. - Enhanced the Login.vue view with transition effects for a smoother user experience during login and setup processes.
This commit is contained in:
@@ -52,27 +52,19 @@
|
||||
<div class="font-mono text-white px-4 sm:px-5 max-w-[95vw] sm:max-w-[90vw] md:max-w-[1200px] text-base sm:text-lg md:text-[24px] leading-relaxed break-words">
|
||||
<div v-if="showLine1" class="flex items-start mb-4 sm:mb-6 opacity-0" :class="{ 'opacity-100': showLine1 }">
|
||||
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||||
<span class="text-white break-words" :class="{ 'typing-text': typingLine1 }">
|
||||
In the future there will be 3 types of humans
|
||||
</span>
|
||||
<span class="text-white break-words">{{ displayLine1 }}</span><span v-if="isTypingLine1" class="intro-typing-caret" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div v-if="showLine2" class="flex items-start mb-4 sm:mb-6 opacity-0" :class="{ 'opacity-100': showLine2 }">
|
||||
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||||
<span class="text-white break-words" :class="{ 'typing-text': typingLine2 }">
|
||||
Government Employees
|
||||
</span>
|
||||
<span class="text-white break-words">{{ displayLine2 }}</span><span v-if="isTypingLine2" class="intro-typing-caret" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div v-if="showLine3" class="flex items-start mb-4 sm:mb-6 opacity-0" :class="{ 'opacity-100': showLine3 }">
|
||||
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||||
<span class="text-white break-words" :class="{ 'typing-text': typingLine3 }">
|
||||
Corporate Employees
|
||||
</span>
|
||||
<span class="text-white break-words">{{ displayLine3 }}</span><span v-if="isTypingLine3" class="intro-typing-caret" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div v-if="showLine4" class="flex items-start mb-8 sm:mb-12 opacity-0" :class="{ 'opacity-100': showLine4 }">
|
||||
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||||
<span class="text-white break-words" :class="{ 'typing-text': typingLine4 }">
|
||||
And Noderunners...
|
||||
</span>
|
||||
<span class="text-white break-words">{{ displayLine4 }}</span><span v-if="isTypingLine4" class="intro-typing-caret" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,7 +99,7 @@
|
||||
<!-- Skip Button -->
|
||||
<button
|
||||
v-if="!alienIntroComplete"
|
||||
@click="skipIntro"
|
||||
@click="handleSkipClick"
|
||||
class="absolute bottom-8 right-8 z-20 bg-black/60 border border-white/30 text-white/70 font-mono text-xs px-4 py-2 rounded backdrop-blur-[10px] hover:bg-black/80 hover:text-white/90 hover:border-white/50 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-300"
|
||||
>
|
||||
Skip Intro
|
||||
@@ -117,12 +109,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { playIntroTyping, playLoopStart, resumeAudioContext, startSynthwave, stopIntroTyping } from '@/composables/useLoginSounds'
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: []
|
||||
}>()
|
||||
|
||||
const INTRO_LINES = [
|
||||
'In the future there will be 3 types of humans',
|
||||
'Government Employees',
|
||||
'Corporate Employees',
|
||||
'And Noderunners...',
|
||||
] as const
|
||||
const MS_PER_CHAR = 55
|
||||
const BLINK_AFTER_TYPING = 1500
|
||||
|
||||
const showSplash = ref(true)
|
||||
const backgroundOpacity = ref(0)
|
||||
const alienIntroComplete = ref(false)
|
||||
@@ -135,11 +137,16 @@ const showLine1 = ref(false)
|
||||
const showLine2 = ref(false)
|
||||
const showLine3 = ref(false)
|
||||
const showLine4 = ref(false)
|
||||
const typingLine1 = ref(false)
|
||||
const typingLine2 = ref(false)
|
||||
const typingLine3 = ref(false)
|
||||
const typingLine4 = ref(false)
|
||||
const displayLine1 = ref('')
|
||||
const displayLine2 = ref('')
|
||||
const displayLine3 = ref('')
|
||||
const displayLine4 = ref('')
|
||||
const isTypingLine1 = ref(false)
|
||||
const isTypingLine2 = ref(false)
|
||||
const isTypingLine3 = ref(false)
|
||||
const isTypingLine4 = ref(false)
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
let introTypingTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// Ensure video plays continuously from Welcome Noderunner through logo
|
||||
watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||
@@ -206,18 +213,31 @@ watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||
// Check if user has seen intro
|
||||
const seenIntro = localStorage.getItem('neode_intro_seen') === '1'
|
||||
|
||||
function handleSkipClick() {
|
||||
resumeAudioContext()
|
||||
skipIntro()
|
||||
}
|
||||
|
||||
function skipIntro() {
|
||||
// Jump to "Welcome Noderunner" part
|
||||
if (introTypingTimeout) {
|
||||
clearTimeout(introTypingTimeout)
|
||||
introTypingTimeout = null
|
||||
}
|
||||
alienIntroComplete.value = true
|
||||
fadeAlienIntro.value = true
|
||||
showWelcome.value = true
|
||||
typingWelcome.value = true
|
||||
stopIntroTyping()
|
||||
playLoopStart()
|
||||
startSynthwave()
|
||||
|
||||
// Stop alien intro typing animations
|
||||
typingLine1.value = false
|
||||
typingLine2.value = false
|
||||
typingLine3.value = false
|
||||
typingLine4.value = false
|
||||
// Stop alien intro typing and any playing typing sound
|
||||
stopIntroTyping()
|
||||
isTypingLine1.value = false
|
||||
isTypingLine2.value = false
|
||||
isTypingLine3.value = false
|
||||
isTypingLine4.value = false
|
||||
|
||||
// Start background fade in at 0.3 opacity when welcome appears
|
||||
setTimeout(() => {
|
||||
@@ -259,101 +279,127 @@ function skipIntro() {
|
||||
}
|
||||
|
||||
function startAlienIntro() {
|
||||
// Line 1 - types and blinks
|
||||
setTimeout(() => {
|
||||
function typeLine(
|
||||
lineIndex: number,
|
||||
displayRef: { value: string },
|
||||
isTypingRef: { value: boolean },
|
||||
onDone: () => void
|
||||
) {
|
||||
const text = INTRO_LINES[lineIndex]!
|
||||
let i = 0
|
||||
displayRef.value = ''
|
||||
isTypingRef.value = true
|
||||
|
||||
function tick() {
|
||||
if (i === 0) {
|
||||
playIntroTyping()
|
||||
}
|
||||
if (i < text.length) {
|
||||
displayRef.value = text.slice(0, i + 1)
|
||||
i++
|
||||
introTypingTimeout = setTimeout(tick, MS_PER_CHAR)
|
||||
} else {
|
||||
stopIntroTyping()
|
||||
isTypingRef.value = false
|
||||
introTypingTimeout = setTimeout(onDone, BLINK_AFTER_TYPING)
|
||||
}
|
||||
}
|
||||
tick()
|
||||
}
|
||||
|
||||
function scheduleLine1() {
|
||||
showLine1.value = true
|
||||
typingLine1.value = true
|
||||
}, 500)
|
||||
typeLine(0, displayLine1, isTypingLine1, scheduleLine2)
|
||||
}
|
||||
|
||||
// Line 2 - wait for line 1 typing (4s) + blinking (1.5s)
|
||||
setTimeout(() => {
|
||||
typingLine1.value = false
|
||||
function scheduleLine2() {
|
||||
showLine2.value = true
|
||||
typingLine2.value = true
|
||||
}, 6000)
|
||||
typeLine(1, displayLine2, isTypingLine2, scheduleLine3)
|
||||
}
|
||||
|
||||
// Line 3 - wait for line 2 typing (4s) + blinking (1.5s)
|
||||
setTimeout(() => {
|
||||
typingLine2.value = false
|
||||
function scheduleLine3() {
|
||||
showLine3.value = true
|
||||
typingLine3.value = true
|
||||
}, 11500)
|
||||
typeLine(2, displayLine3, isTypingLine3, scheduleLine4)
|
||||
}
|
||||
|
||||
// Line 4 - wait for line 3 typing (4s) + blinking (1.5s)
|
||||
setTimeout(() => {
|
||||
typingLine3.value = false
|
||||
function scheduleLine4() {
|
||||
showLine4.value = true
|
||||
typingLine4.value = true
|
||||
}, 17000)
|
||||
typeLine(3, displayLine4, isTypingLine4, () => {
|
||||
isTypingLine4.value = false
|
||||
fadeAlienIntro.value = true
|
||||
introTypingTimeout = setTimeout(showWelcomePhase, 800)
|
||||
})
|
||||
}
|
||||
|
||||
// Fade out alien intro - wait for line 4 typing (4s) + blinking (1.5s)
|
||||
setTimeout(() => {
|
||||
typingLine4.value = false
|
||||
fadeAlienIntro.value = true
|
||||
}, 22500)
|
||||
|
||||
// Show welcome and start video immediately
|
||||
setTimeout(() => {
|
||||
function showWelcomePhase() {
|
||||
alienIntroComplete.value = true
|
||||
showWelcome.value = true
|
||||
typingWelcome.value = true
|
||||
// Start video immediately when welcome appears
|
||||
stopIntroTyping()
|
||||
playLoopStart()
|
||||
startSynthwave()
|
||||
if (videoElement.value) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed on welcome:', err)
|
||||
})
|
||||
}
|
||||
}, 23300)
|
||||
|
||||
// Start background fade in at 0.3 opacity when welcome appears
|
||||
setTimeout(() => {
|
||||
backgroundOpacity.value = 0.3
|
||||
}, 23300)
|
||||
|
||||
// Fade out welcome - typing (2s) + cursor continues (1.5s) + 3 blinks (1.35s) = 4.85s
|
||||
setTimeout(() => {
|
||||
fadeWelcome.value = true
|
||||
typingWelcome.value = false
|
||||
}, 28150)
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
fadeWelcome.value = true
|
||||
typingWelcome.value = false
|
||||
}, 4850)
|
||||
|
||||
// Show logo - background stays at 0.3 opacity
|
||||
setTimeout(() => {
|
||||
showLogo.value = true
|
||||
}, 29000)
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
showLogo.value = true
|
||||
}, 5500)
|
||||
|
||||
// Hide welcome after logo starts appearing
|
||||
setTimeout(() => {
|
||||
showWelcome.value = false
|
||||
}, 30500)
|
||||
|
||||
// Fade background to full opacity just before completing (for smooth transition to modal)
|
||||
setTimeout(() => {
|
||||
backgroundOpacity.value = 1
|
||||
}, 33000)
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
showWelcome.value = false
|
||||
}, 6000)
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
backgroundOpacity.value = 1
|
||||
}, 9000)
|
||||
|
||||
introTypingTimeout = setTimeout(() => {
|
||||
if (videoElement.value && !videoElement.value.paused) {
|
||||
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
||||
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
||||
}
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
emit('complete')
|
||||
}, 9500)
|
||||
}
|
||||
|
||||
introTypingTimeout = setTimeout(scheduleLine1, 500)
|
||||
|
||||
// Complete splash with smooth transition
|
||||
setTimeout(() => {
|
||||
// Store final video time right before unmounting
|
||||
if (videoElement.value && !videoElement.value.paused) {
|
||||
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
||||
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
||||
}
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
emit('complete')
|
||||
}, 34500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (seenIntro) {
|
||||
// Skip intro if already seen
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
emit('complete')
|
||||
} else {
|
||||
// Play intro
|
||||
startAlienIntro()
|
||||
// Unlock audio on first user interaction (required for autoplay in most browsers)
|
||||
const unlock = () => {
|
||||
resumeAudioContext()
|
||||
document.removeEventListener('click', unlock)
|
||||
document.removeEventListener('touchstart', unlock)
|
||||
}
|
||||
document.addEventListener('click', unlock, { once: true })
|
||||
document.addEventListener('touchstart', unlock, { once: true })
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (introTypingTimeout) {
|
||||
clearTimeout(introTypingTimeout)
|
||||
introTypingTimeout = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -465,6 +511,23 @@ onMounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Intro typing cursor - block style, cyan blink (matches original typing-text caret) */
|
||||
.intro-typing-caret {
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
min-width: 4px;
|
||||
height: 1.2em;
|
||||
background: #00ffff;
|
||||
margin-left: 2px;
|
||||
vertical-align: text-bottom;
|
||||
animation: intro-caret-blink 0.5s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes intro-caret-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* Ensure text wraps smoothly on mobile */
|
||||
.font-mono {
|
||||
word-wrap: break-word;
|
||||
|
||||
Reference in New Issue
Block a user