Update package dependencies and enhance application structure
- Added several new dependencies related to the Applesauce library, including 'applesauce-accounts', 'applesauce-common', 'applesauce-core', 'applesauce-loaders', 'applesauce-relay', and 'applesauce-signers', all at version 5.1.0. - Updated the development script in package.json to specify a port for Vite and added new seed scripts for profiles and activity. - Removed outdated image files from the public directory to clean up unused assets. - Enhanced the App.vue structure by integrating shared components like AppHeader and AuthModal for improved user experience. - Refactored ContentDetailModal and MobileNav components to support new features and improve usability. These changes improve the overall functionality and maintainability of the application while ensuring it utilizes the latest libraries for better performance.
This commit is contained in:
@@ -5,21 +5,74 @@
|
||||
<!-- Logo + Navigation (Left Side) -->
|
||||
<div class="flex items-center gap-10">
|
||||
<router-link to="/">
|
||||
<img src="/assets/images/logo.svg" alt="IndeedHub" class="h-10 ml-2 md:ml-0" />
|
||||
<img src="/assets/images/logo-desktop.svg" alt="IndeeHub" class="h-8 md:h-10 ml-2 md:ml-0" />
|
||||
</router-link>
|
||||
|
||||
<!-- Navigation - Desktop -->
|
||||
<nav v-if="showNav" class="hidden md:flex items-center gap-3">
|
||||
<router-link to="/" :class="isRoute('/') ? 'nav-button-active' : 'nav-button'">Films</router-link>
|
||||
<router-link to="/library" :class="isRoute('/library') ? 'nav-button-active' : 'nav-button'">My List</router-link>
|
||||
<button @click="handleFilmsClick" :class="isRoute('/') && !activeAlgorithm ? 'nav-button-active' : 'nav-button'">Films</button>
|
||||
<router-link to="/library" :class="isRoute('/library') && !activeAlgorithm ? 'nav-button-active' : 'nav-button'" @click="clearFilter">My List</router-link>
|
||||
|
||||
<!-- Algorithm Filters -->
|
||||
<button
|
||||
v-for="algo in algorithms"
|
||||
:key="algo.id"
|
||||
@click="setAlgorithm(algo.id)"
|
||||
:class="activeAlgorithm === algo.id ? 'nav-button-active' : 'nav-button'"
|
||||
>
|
||||
{{ algo.label }}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Right Side Actions -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Sign In Button (if not authenticated) -->
|
||||
<!-- Nostr Login (persona switcher or extension) -->
|
||||
<div v-if="!nostrLoggedIn" class="hidden md:flex items-center gap-2">
|
||||
<!-- Persona Switcher (dev) -->
|
||||
<div class="relative persona-dropdown">
|
||||
<button @click="togglePersonaMenu" class="nav-button px-3 py-2 text-xs">
|
||||
Persona
|
||||
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="personaMenuOpen" class="profile-menu absolute right-0 mt-2 w-52">
|
||||
<div class="floating-glass-header py-2 rounded-xl">
|
||||
<div class="px-3 py-1 text-xs text-white/40 uppercase tracking-wider">Test Personas</div>
|
||||
<button
|
||||
v-for="persona in testPersonas"
|
||||
:key="persona.pubkey"
|
||||
@click="handlePersonaLogin(persona)"
|
||||
class="profile-menu-item flex items-center gap-3 px-4 py-2 w-full text-left"
|
||||
>
|
||||
<img :src="`https://robohash.org/${persona.pubkey}.png`" class="w-6 h-6 rounded-full" :alt="persona.name" />
|
||||
<span>{{ persona.name }}</span>
|
||||
</button>
|
||||
<div class="border-t border-white/10 my-1"></div>
|
||||
<div class="px-3 py-1 text-xs text-white/40 uppercase tracking-wider">Tastemakers</div>
|
||||
<button
|
||||
v-for="persona in tastemakerPersonas"
|
||||
:key="persona.pubkey"
|
||||
@click="handlePersonaLogin(persona)"
|
||||
class="profile-menu-item flex items-center gap-3 px-4 py-2 w-full text-left"
|
||||
>
|
||||
<img :src="`https://robohash.org/${persona.pubkey}.png`" class="w-6 h-6 rounded-full" :alt="persona.name" />
|
||||
<span>{{ persona.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extension Login -->
|
||||
<button @click="handleExtensionLogin" class="nav-button px-3 py-2 text-xs">
|
||||
Extension
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sign In (app auth, if not Nostr logged in) -->
|
||||
<button
|
||||
v-if="!isAuthenticated && showAuth"
|
||||
v-if="!isAuthenticated && showAuth && !nostrLoggedIn"
|
||||
@click="$emit('openAuth')"
|
||||
class="hidden md:block hero-play-button px-4 py-2 text-sm"
|
||||
>
|
||||
@@ -33,22 +86,24 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Profile Dropdown (authenticated only) -->
|
||||
<div v-if="isAuthenticated" class="hidden md:block relative profile-dropdown">
|
||||
<!-- Active Nostr Account -->
|
||||
<div v-if="nostrLoggedIn" class="hidden md:block relative profile-dropdown">
|
||||
<button
|
||||
@click="toggleDropdown"
|
||||
class="profile-button flex items-center gap-2"
|
||||
>
|
||||
<div class="profile-avatar">
|
||||
<span>{{ userInitials }}</span>
|
||||
</div>
|
||||
<span class="text-white text-sm font-medium">{{ userName }}</span>
|
||||
<img
|
||||
v-if="nostrActivePubkey"
|
||||
:src="`https://robohash.org/${nostrActivePubkey}.png`"
|
||||
class="w-8 h-8 rounded-full"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<span class="text-white text-sm font-medium">{{ nostrActiveName }}</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': dropdownOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div v-if="dropdownOpen" class="profile-menu absolute right-0 mt-2 w-48">
|
||||
<div class="floating-glass-header py-2 rounded-xl">
|
||||
<button @click="navigateTo('/profile')" class="profile-menu-item flex items-center gap-3 px-4 py-2.5 w-full text-left">
|
||||
@@ -74,12 +129,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile User Avatar + Name -->
|
||||
<!-- Fallback: App-auth profile (when Nostr not logged in but app auth is) -->
|
||||
<div v-else-if="isAuthenticated" class="hidden md:block relative profile-dropdown">
|
||||
<button @click="toggleDropdown" class="profile-button flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-orange-500 to-pink-500 flex items-center justify-center text-xs font-bold text-white">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
<span class="text-white text-sm font-medium">{{ userName }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile -->
|
||||
<div class="md:hidden flex items-center gap-2 mr-2">
|
||||
<div v-if="isAuthenticated" class="profile-avatar">
|
||||
<span>{{ userInitials }}</span>
|
||||
</div>
|
||||
<span v-if="isAuthenticated" class="text-white text-sm font-medium">{{ userName }}</span>
|
||||
<img
|
||||
v-if="nostrLoggedIn && nostrActivePubkey"
|
||||
:src="`https://robohash.org/${nostrActivePubkey}.png`"
|
||||
class="w-7 h-7 rounded-full"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<span v-if="nostrLoggedIn" class="text-white text-sm font-medium">{{ nostrActiveName }}</span>
|
||||
<template v-else-if="isAuthenticated">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-orange-500 to-pink-500 flex items-center justify-center text-xs font-bold text-white">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
<span class="text-white text-sm font-medium">{{ userName }}</span>
|
||||
</template>
|
||||
<button v-else-if="showAuth" @click="$emit('openAuth')" class="text-white text-sm font-medium">Sign In</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,6 +166,10 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuth } from '../composables/useAuth'
|
||||
import { useAccounts } from '../composables/useAccounts'
|
||||
import { useContentDiscovery } from '../composables/useContentDiscovery'
|
||||
|
||||
type Persona = { name: string; nsec: string; pubkey: string }
|
||||
|
||||
interface Props {
|
||||
showNav?: boolean
|
||||
@@ -113,11 +191,40 @@ defineEmits<Emits>()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { user, isAuthenticated, logout } = useAuth()
|
||||
const { user, isAuthenticated, logout: appLogout } = useAuth()
|
||||
const {
|
||||
isLoggedIn: nostrLoggedIn,
|
||||
activePubkey: nostrActivePubkey,
|
||||
activeName: nostrActiveName,
|
||||
testPersonas,
|
||||
tastemakerPersonas,
|
||||
loginWithExtension,
|
||||
loginWithPersona,
|
||||
logout: nostrLogout,
|
||||
} = useAccounts()
|
||||
|
||||
const {
|
||||
activeAlgorithm,
|
||||
algorithms,
|
||||
setAlgorithm: _setAlgorithm,
|
||||
} = useContentDiscovery()
|
||||
|
||||
/**
|
||||
* When a filter is clicked, navigate to Films page if not already there,
|
||||
* then apply the filter.
|
||||
*/
|
||||
function setAlgorithm(id: string) {
|
||||
_setAlgorithm(id as any)
|
||||
if (route.path !== '/') {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
const dropdownOpen = ref(false)
|
||||
const personaMenuOpen = ref(false)
|
||||
|
||||
const userInitials = computed(() => {
|
||||
if (nostrActiveName.value) return nostrActiveName.value[0].toUpperCase()
|
||||
if (!user.value?.legalName) return 'U'
|
||||
const names = user.value.legalName.split(' ')
|
||||
return names.length > 1
|
||||
@@ -126,6 +233,7 @@ const userInitials = computed(() => {
|
||||
})
|
||||
|
||||
const userName = computed(() => {
|
||||
if (nostrActiveName.value) return nostrActiveName.value
|
||||
return user.value?.legalName?.split(' ')[0] || 'Guest'
|
||||
})
|
||||
|
||||
@@ -133,8 +241,31 @@ function isRoute(path: string): boolean {
|
||||
return route.path === path
|
||||
}
|
||||
|
||||
/** Navigate to Films and clear any active filter */
|
||||
function handleFilmsClick() {
|
||||
if (activeAlgorithm.value) {
|
||||
_setAlgorithm(activeAlgorithm.value as any) // toggle off
|
||||
}
|
||||
if (route.path !== '/') {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear active filter (used when navigating to My List) */
|
||||
function clearFilter() {
|
||||
if (activeAlgorithm.value) {
|
||||
_setAlgorithm(activeAlgorithm.value as any) // toggle off
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
dropdownOpen.value = !dropdownOpen.value
|
||||
personaMenuOpen.value = false
|
||||
}
|
||||
|
||||
function togglePersonaMenu() {
|
||||
personaMenuOpen.value = !personaMenuOpen.value
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
|
||||
function navigateTo(path: string) {
|
||||
@@ -142,17 +273,31 @@ function navigateTo(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
async function handlePersonaLogin(persona: Persona) {
|
||||
personaMenuOpen.value = false
|
||||
await loginWithPersona(persona)
|
||||
}
|
||||
|
||||
async function handleExtensionLogin() {
|
||||
await loginWithExtension()
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await logout()
|
||||
nostrLogout()
|
||||
await appLogout()
|
||||
dropdownOpen.value = false
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const dropdown = document.querySelector('.profile-dropdown')
|
||||
const personaDropdown = document.querySelector('.persona-dropdown')
|
||||
if (dropdown && !dropdown.contains(event.target as Node)) {
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
if (personaDropdown && !personaDropdown.contains(event.target as Node)) {
|
||||
personaMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
317
src/components/CommentNode.vue
Normal file
317
src/components/CommentNode.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="comment-thread" :style="{ marginLeft: depth > 0 ? `${Math.min(depth, 4) * 20}px` : '0' }">
|
||||
<div class="comment-item" :class="{ 'comment-reply': depth > 0 }">
|
||||
<div class="flex gap-3">
|
||||
<!-- Author Avatar (robohash fallback) -->
|
||||
<img
|
||||
:src="authorAvatar"
|
||||
:alt="authorName"
|
||||
class="w-8 h-8 rounded-full flex-shrink-0 object-cover"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Author + Timestamp -->
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-white text-sm font-medium truncate">{{ authorName }}</span>
|
||||
<span class="text-white/30 text-xs flex-shrink-0">{{ timeAgo }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Comment Content -->
|
||||
<p class="text-white/70 text-sm leading-relaxed whitespace-pre-wrap">{{ node.event.content }}</p>
|
||||
|
||||
<!-- Comment Actions: Reactions + Reply -->
|
||||
<div class="flex items-center gap-3 mt-2">
|
||||
<!-- Upvote -->
|
||||
<button
|
||||
v-if="isLoggedIn"
|
||||
@click="handleReact(true)"
|
||||
:disabled="hasVoted"
|
||||
class="comment-action-btn"
|
||||
:class="{ 'comment-action-active': userVote === '+' }"
|
||||
:style="{ opacity: userVote === '+' ? 1 : hasVoted ? 0.4 : 1 }"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
<span v-if="reactionCounts.positive > 0">{{ reactionCounts.positive }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Downvote -->
|
||||
<button
|
||||
v-if="isLoggedIn"
|
||||
@click="handleReact(false)"
|
||||
:disabled="hasVoted"
|
||||
class="comment-action-btn"
|
||||
:class="{ 'comment-action-active': userVote === '-' }"
|
||||
:style="{ opacity: userVote === '-' ? 1 : hasVoted ? 0.4 : 1 }"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<span v-if="reactionCounts.negative > 0">{{ reactionCounts.negative }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Reply -->
|
||||
<button
|
||||
v-if="isLoggedIn"
|
||||
@click="showReplyForm = !showReplyForm"
|
||||
class="comment-action-btn"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
<span>Reply</span>
|
||||
</button>
|
||||
|
||||
<!-- Expand/Collapse Replies -->
|
||||
<button
|
||||
v-if="node.replies.length > 0"
|
||||
@click="showReplies = !showReplies"
|
||||
class="comment-action-btn"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5 transition-transform" :class="{ 'rotate-180': showReplies }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<span>{{ showReplies ? 'Hide' : `${node.replies.length}` }} {{ node.replies.length === 1 ? 'reply' : 'replies' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reply Form -->
|
||||
<div v-if="showReplyForm" class="mt-3">
|
||||
<div class="flex gap-2">
|
||||
<img
|
||||
:src="currentUserAvatar"
|
||||
class="w-6 h-6 rounded-full flex-shrink-0 object-cover"
|
||||
alt="You"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<textarea
|
||||
v-model="replyText"
|
||||
placeholder="Write a reply..."
|
||||
class="comment-textarea text-sm"
|
||||
rows="2"
|
||||
@keydown.meta.enter="submitReply"
|
||||
@keydown.ctrl.enter="submitReply"
|
||||
></textarea>
|
||||
<div class="flex items-center gap-2 justify-end mt-1.5">
|
||||
<button @click="showReplyForm = false" class="cancel-btn">Cancel</button>
|
||||
<button
|
||||
@click="submitReply"
|
||||
:disabled="!replyText.trim() || isPostingReply"
|
||||
class="submit-comment-btn text-xs"
|
||||
:class="{ 'opacity-40 cursor-not-allowed': !replyText.trim() || isPostingReply }"
|
||||
>
|
||||
{{ isPostingReply ? '...' : 'Reply' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nested Replies -->
|
||||
<template v-if="showReplies && node.replies.length > 0">
|
||||
<CommentNode
|
||||
v-for="reply in node.replies"
|
||||
:key="reply.event.id"
|
||||
:node="reply"
|
||||
:depth="depth + 1"
|
||||
:nostr="nostr"
|
||||
:is-logged-in="isLoggedIn"
|
||||
:current-user-avatar="currentUserAvatar"
|
||||
@get-profile="(pubkey: string) => $emit('getProfile', pubkey)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { CommentNode as CommentNodeType } from '../composables/useNostr'
|
||||
|
||||
interface Props {
|
||||
node: CommentNodeType
|
||||
depth: number
|
||||
nostr: any
|
||||
isLoggedIn: boolean
|
||||
currentUserAvatar: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
defineEmits<{
|
||||
'getProfile': [pubkey: string]
|
||||
}>()
|
||||
|
||||
const showReplyForm = ref(false)
|
||||
const showReplies = ref(props.depth < 2) // Auto-expand first 2 levels
|
||||
const replyText = ref('')
|
||||
const isPostingReply = ref(false)
|
||||
const isReacting = ref(false)
|
||||
|
||||
// Profile data
|
||||
const profile = computed(() => props.nostr.profiles.value.get(props.node.event.pubkey))
|
||||
|
||||
const authorName = computed(() => {
|
||||
return profile.value?.name || profile.value?.display_name || 'Anonymous'
|
||||
})
|
||||
|
||||
const authorAvatar = computed(() => {
|
||||
if (profile.value?.picture) return profile.value.picture
|
||||
return `https://robohash.org/${props.node.event.pubkey}.png`
|
||||
})
|
||||
|
||||
// Time formatting
|
||||
const timeAgo = computed(() => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const diff = now - props.node.event.created_at
|
||||
|
||||
if (diff < 60) return 'just now'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
||||
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`
|
||||
return new Date(props.node.event.created_at * 1000).toLocaleDateString()
|
||||
})
|
||||
|
||||
// Per-comment reactions
|
||||
const reactionCounts = computed(() => {
|
||||
return props.nostr.getCommentReactionCounts(props.node.event.id)
|
||||
})
|
||||
|
||||
const userVote = computed(() => {
|
||||
return props.nostr.getUserCommentReaction(props.node.event.id)
|
||||
})
|
||||
|
||||
const hasVoted = computed(() => {
|
||||
return props.nostr.hasVotedOnComment(props.node.event.id)
|
||||
})
|
||||
|
||||
async function handleReact(positive: boolean) {
|
||||
if (isReacting.value || hasVoted.value) return
|
||||
isReacting.value = true
|
||||
try {
|
||||
await props.nostr.reactToComment(props.node.event, positive)
|
||||
} catch (err) {
|
||||
console.error('Failed to react to comment:', err)
|
||||
} finally {
|
||||
isReacting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReply() {
|
||||
if (!replyText.value.trim() || isPostingReply.value) return
|
||||
isPostingReply.value = true
|
||||
try {
|
||||
await props.nostr.postReply(props.node.event, replyText.value.trim())
|
||||
replyText.value = ''
|
||||
showReplyForm.value = false
|
||||
showReplies.value = true
|
||||
} catch (err) {
|
||||
console.error('Failed to post reply:', err)
|
||||
} finally {
|
||||
isPostingReply.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comment-item {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.comment-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comment-reply {
|
||||
border-left: 2px solid rgba(255, 255, 255, 0.08);
|
||||
padding-left: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.comment-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comment-action-btn:hover:not(:disabled) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.comment-action-btn:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.comment-action-active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.comment-textarea {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.comment-textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.comment-textarea:focus {
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.submit-comment-btn {
|
||||
padding: 5px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-comment-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -56,19 +56,31 @@
|
||||
</button>
|
||||
|
||||
<!-- Like Button -->
|
||||
<button @click="handleLike" class="action-btn" :class="{ 'action-btn-active': userReaction === '+' }">
|
||||
<button
|
||||
@click="handleLike"
|
||||
:disabled="hasVoted || !isNostrLoggedIn"
|
||||
class="action-btn"
|
||||
:class="{ 'action-btn-active': userReaction === '+' }"
|
||||
:style="{ opacity: userReaction === '+' ? 1 : hasVoted ? 0.4 : 1 }"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
|
||||
</svg>
|
||||
<span v-if="reactionCounts.positive > 0" class="text-xs">{{ reactionCounts.positive }}</span>
|
||||
<span class="text-xs">{{ reactionCounts.positive }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Dislike Button -->
|
||||
<button @click="handleDislike" class="action-btn" :class="{ 'action-btn-active': userReaction === '-' }">
|
||||
<button
|
||||
@click="handleDislike"
|
||||
:disabled="hasVoted || !isNostrLoggedIn"
|
||||
class="action-btn"
|
||||
:class="{ 'action-btn-active': userReaction === '-' }"
|
||||
:style="{ opacity: userReaction === '-' ? 1 : hasVoted ? 0.4 : 1 }"
|
||||
>
|
||||
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
|
||||
</svg>
|
||||
<span v-if="reactionCounts.negative > 0" class="text-xs">{{ reactionCounts.negative }}</span>
|
||||
<span class="text-xs">{{ reactionCounts.negative }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Share Button -->
|
||||
@@ -113,16 +125,18 @@
|
||||
<div class="comments-section">
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
Comments
|
||||
<span class="text-sm font-normal text-white/50">({{ comments.length }})</span>
|
||||
<span v-if="isDev" class="text-xs bg-white/10 text-white/40 px-2 py-0.5 rounded-full ml-auto">Demo Mode</span>
|
||||
<span class="text-sm font-normal text-white/50">({{ commentCount }})</span>
|
||||
<span v-if="!relayConnected" class="text-xs bg-orange-500/20 text-orange-300/60 px-2 py-0.5 rounded-full ml-auto">Relay Offline</span>
|
||||
</h3>
|
||||
|
||||
<!-- Comment Input -->
|
||||
<div v-if="isAuthenticated" class="comment-input-wrap mb-6">
|
||||
<div v-if="isNostrLoggedIn" class="comment-input-wrap mb-6">
|
||||
<div class="flex gap-3">
|
||||
<div class="profile-avatar flex-shrink-0">
|
||||
<span>{{ userInitials }}</span>
|
||||
</div>
|
||||
<img
|
||||
:src="currentUserAvatar"
|
||||
class="w-8 h-8 rounded-full flex-shrink-0 object-cover"
|
||||
alt="You"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<textarea
|
||||
v-model="newComment"
|
||||
@@ -135,11 +149,11 @@
|
||||
<div class="flex justify-end mt-2">
|
||||
<button
|
||||
@click="submitComment"
|
||||
:disabled="!newComment.trim()"
|
||||
:disabled="!newComment.trim() || isPostingComment"
|
||||
class="submit-comment-btn"
|
||||
:class="{ 'opacity-40 cursor-not-allowed': !newComment.trim() }"
|
||||
:class="{ 'opacity-40 cursor-not-allowed': !newComment.trim() || isPostingComment }"
|
||||
>
|
||||
Post
|
||||
{{ isPostingComment ? 'Posting...' : 'Post' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,41 +173,22 @@
|
||||
<div class="text-white/40 text-sm">Loading comments...</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="comments.length === 0" class="text-center py-8">
|
||||
<div v-else-if="commentTree.length === 0" class="text-center py-8">
|
||||
<div class="text-white/40 text-sm">No comments yet. Be the first!</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="comment in comments"
|
||||
:key="comment.id"
|
||||
class="comment-item"
|
||||
>
|
||||
<div class="flex gap-3">
|
||||
<!-- Author Avatar -->
|
||||
<img
|
||||
v-if="getProfile(comment.pubkey)?.picture"
|
||||
:src="getProfile(comment.pubkey).picture"
|
||||
:alt="getProfile(comment.pubkey)?.name || 'User'"
|
||||
class="w-8 h-8 rounded-full flex-shrink-0 object-cover"
|
||||
/>
|
||||
<div v-else class="w-8 h-8 rounded-full flex-shrink-0 bg-gradient-to-br from-orange-500 to-pink-500 flex items-center justify-center text-xs font-bold text-white">
|
||||
{{ (getProfile(comment.pubkey)?.name || 'A')[0].toUpperCase() }}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-white text-sm font-medium truncate">
|
||||
{{ getProfile(comment.pubkey)?.name || 'Anonymous' }}
|
||||
</span>
|
||||
<span class="text-white/30 text-xs flex-shrink-0">
|
||||
{{ formatTimeAgo(comment.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-white/70 text-sm leading-relaxed">{{ comment.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Threaded comments -->
|
||||
<div v-else class="space-y-1">
|
||||
<template v-for="node in commentTree" :key="node.event.id">
|
||||
<CommentNode
|
||||
:node="node"
|
||||
:depth="0"
|
||||
:nostr="nostr"
|
||||
:is-logged-in="isNostrLoggedIn"
|
||||
:current-user-avatar="currentUserAvatar"
|
||||
@get-profile="getProfile"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,11 +222,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useAuth } from '../composables/useAuth'
|
||||
import { useAccounts } from '../composables/useAccounts'
|
||||
import { useNostr } from '../composables/useNostr'
|
||||
import type { Content } from '../types/content'
|
||||
import VideoPlayer from './VideoPlayer.vue'
|
||||
import SubscriptionModal from './SubscriptionModal.vue'
|
||||
import RentalModal from './RentalModal.vue'
|
||||
import CommentNode from './CommentNode.vue'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
@@ -246,51 +243,52 @@ interface Emits {
|
||||
const props = defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
|
||||
const { isAuthenticated, hasActiveSubscription, user } = useAuth()
|
||||
const { isAuthenticated, hasActiveSubscription } = useAuth()
|
||||
const { isLoggedIn: isNostrLoggedIn, activePubkey } = useAccounts()
|
||||
|
||||
const isDev = import.meta.env.DEV
|
||||
const newComment = ref('')
|
||||
const isPostingComment = ref(false)
|
||||
const isInMyList = ref(false)
|
||||
const userReaction = ref<string | null>(null)
|
||||
const showVideoPlayer = ref(false)
|
||||
const showSubscriptionModal = ref(false)
|
||||
const showRentalModal = ref(false)
|
||||
const relayConnected = ref(true)
|
||||
|
||||
// Nostr social data -- initialized per content
|
||||
// Nostr social data -- subscribes to relay in real time
|
||||
const nostr = useNostr()
|
||||
const comments = computed(() => nostr.comments.value)
|
||||
const commentTree = computed(() => nostr.commentTree.value)
|
||||
const reactionCounts = computed(() => nostr.reactionCounts.value)
|
||||
const isLoadingComments = computed(() => nostr.isLoading.value)
|
||||
const commentCount = computed(() => nostr.commentCount.value)
|
||||
|
||||
const userInitials = computed(() => {
|
||||
if (!user.value?.legalName) return 'U'
|
||||
const names = user.value.legalName.split(' ')
|
||||
return names.length > 1
|
||||
? `${names[0][0]}${names[names.length - 1][0]}`
|
||||
: names[0][0]
|
||||
// User's existing reaction read from relay (not local state)
|
||||
const userReaction = computed(() => nostr.userContentReaction.value)
|
||||
const hasVoted = computed(() => nostr.hasVotedOnContent.value)
|
||||
|
||||
// Current user avatar (robohash)
|
||||
const currentUserAvatar = computed(() => {
|
||||
if (activePubkey.value) {
|
||||
return `https://robohash.org/${activePubkey.value}.png`
|
||||
}
|
||||
return 'https://robohash.org/anonymous.png'
|
||||
})
|
||||
|
||||
// Fetch social data when content changes
|
||||
watch(() => props.content?.id, async (newId) => {
|
||||
// Subscribe to Nostr data when content changes
|
||||
watch(() => props.content?.id, (newId) => {
|
||||
if (newId && props.isOpen) {
|
||||
await loadSocialData(newId)
|
||||
loadSocialData(newId)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.isOpen, async (open) => {
|
||||
watch(() => props.isOpen, (open) => {
|
||||
if (open && props.content?.id) {
|
||||
await loadSocialData(props.content.id)
|
||||
loadSocialData(props.content.id)
|
||||
}
|
||||
})
|
||||
|
||||
async function loadSocialData(contentId: string) {
|
||||
userReaction.value = null
|
||||
await Promise.all([
|
||||
nostr.fetchComments(contentId),
|
||||
nostr.fetchReactions(contentId),
|
||||
])
|
||||
nostr.subscribeToComments(contentId)
|
||||
nostr.subscribeToReactions(contentId)
|
||||
function loadSocialData(contentId: string) {
|
||||
nostr.cleanup()
|
||||
nostr.subscribeToContent(contentId)
|
||||
}
|
||||
|
||||
function getProfile(pubkey: string) {
|
||||
@@ -298,17 +296,13 @@ function getProfile(pubkey: string) {
|
||||
}
|
||||
|
||||
function handlePlay() {
|
||||
if (!isAuthenticated.value) {
|
||||
// Will be caught by parent via openAuth emit
|
||||
return
|
||||
}
|
||||
if (!isAuthenticated.value) return
|
||||
|
||||
if (hasActiveSubscription.value) {
|
||||
showVideoPlayer.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// No subscription -- show rental modal
|
||||
showRentalModal.value = true
|
||||
}
|
||||
|
||||
@@ -317,8 +311,7 @@ function toggleMyList() {
|
||||
}
|
||||
|
||||
async function handleLike() {
|
||||
if (!isAuthenticated.value) return
|
||||
userReaction.value = userReaction.value === '+' ? null : '+'
|
||||
if (!isNostrLoggedIn.value || hasVoted.value) return
|
||||
if (props.content?.id) {
|
||||
try {
|
||||
await nostr.postReaction(true, props.content.id)
|
||||
@@ -329,8 +322,7 @@ async function handleLike() {
|
||||
}
|
||||
|
||||
async function handleDislike() {
|
||||
if (!isAuthenticated.value) return
|
||||
userReaction.value = userReaction.value === '-' ? null : '-'
|
||||
if (!isNostrLoggedIn.value || hasVoted.value) return
|
||||
if (props.content?.id) {
|
||||
try {
|
||||
await nostr.postReaction(false, props.content.id)
|
||||
@@ -356,13 +348,16 @@ function handleShare() {
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (!newComment.value.trim() || !props.content?.id) return
|
||||
if (!newComment.value.trim() || !props.content?.id || isPostingComment.value) return
|
||||
|
||||
isPostingComment.value = true
|
||||
try {
|
||||
await nostr.postComment(newComment.value.trim(), props.content.id)
|
||||
newComment.value = ''
|
||||
} catch (err) {
|
||||
console.error('Failed to post comment:', err)
|
||||
} finally {
|
||||
isPostingComment.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,17 +374,6 @@ function openSubscriptionFromRental() {
|
||||
showRentalModal.value = false
|
||||
showSubscriptionModal.value = true
|
||||
}
|
||||
|
||||
function formatTimeAgo(timestamp: number): string {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const diff = now - timestamp
|
||||
|
||||
if (diff < 60) return 'just now'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
||||
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`
|
||||
return new Date(timestamp * 1000).toLocaleDateString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -589,15 +573,6 @@ function formatTimeAgo(timestamp: number): string {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.comment-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.modal-fade-enter-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getMockReactionCounts, getMockCommentCount } from '../data/mockSocialData'
|
||||
import type { Content } from '../types/content'
|
||||
|
||||
interface Props {
|
||||
@@ -83,12 +82,14 @@ defineEmits<{
|
||||
'content-click': [content: Content]
|
||||
}>()
|
||||
|
||||
function getReactionCount(contentId: string): number {
|
||||
return getMockReactionCounts(contentId).positive
|
||||
// Social counts are now fetched from the relay when the detail modal opens.
|
||||
// We no longer show mock badges on cards -- the real data lives on the relay.
|
||||
function getReactionCount(_contentId: string): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
function getCommentCount(contentId: string): number {
|
||||
return getMockCommentCount(contentId)
|
||||
function getCommentCount(_contentId: string): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
const sliderRef = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -1,16 +1,87 @@
|
||||
<template>
|
||||
<!-- Filter Bottom Sheet Overlay -->
|
||||
<Teleport to="body">
|
||||
<Transition name="sheet">
|
||||
<div v-if="showFilterSheet" class="filter-sheet-backdrop" @click.self="showFilterSheet = false">
|
||||
<div class="filter-sheet">
|
||||
<div class="filter-sheet-handle"></div>
|
||||
<h3 class="text-white text-base font-semibold mb-4 text-center tracking-wide">Sort By</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
v-for="algo in algorithms"
|
||||
:key="algo.id"
|
||||
@click="selectAlgorithm(algo.id)"
|
||||
class="filter-option"
|
||||
:class="{ 'filter-option-active': activeAlgorithm === algo.id }"
|
||||
>
|
||||
<span>{{ algo.label }}</span>
|
||||
<svg
|
||||
v-if="activeAlgorithm === algo.id"
|
||||
class="w-5 h-5 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<nav class="mobile-nav fixed bottom-0 left-0 right-0 z-50 md:hidden pb-4 px-4">
|
||||
<div class="floating-glass-nav px-4 py-3 rounded-2xl">
|
||||
<div class="flex items-center justify-around gap-1">
|
||||
<!-- Films -->
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
@click="navigate(item.path)"
|
||||
@click="handleFilmsClick"
|
||||
class="flex flex-col items-center gap-1 nav-tab flex-1"
|
||||
:class="{ 'nav-tab-active': isActive(item.path) }"
|
||||
:class="{ 'nav-tab-active': isOnFilmsPage && !isFilterActive }"
|
||||
>
|
||||
<component :is="item.icon" class="w-6 h-6 flex-shrink-0" />
|
||||
<span class="text-xs font-medium whitespace-nowrap">{{ item.name }}</span>
|
||||
<svg class="w-6 h-6 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4h-4z"/>
|
||||
</svg>
|
||||
<span class="text-xs font-medium whitespace-nowrap">Films</span>
|
||||
</button>
|
||||
|
||||
<!-- Filters (visible on Films and My List) -->
|
||||
<button
|
||||
v-if="isOnFilmsPage || isActive('/library')"
|
||||
@click="showFilterSheet = true"
|
||||
class="flex flex-col items-center gap-1 nav-tab flex-1"
|
||||
:class="{ 'nav-tab-active': isFilterActive || showFilterSheet }"
|
||||
>
|
||||
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
<span class="text-xs font-medium whitespace-nowrap">Filters</span>
|
||||
</button>
|
||||
|
||||
<!-- My List -->
|
||||
<button
|
||||
@click="handleMyListClick"
|
||||
class="flex flex-col items-center gap-1 nav-tab flex-1"
|
||||
:class="{ 'nav-tab-active': isActive('/library') && !isFilterActive }"
|
||||
>
|
||||
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
||||
</svg>
|
||||
<span class="text-xs font-medium whitespace-nowrap">My List</span>
|
||||
</button>
|
||||
|
||||
<!-- Profile -->
|
||||
<button
|
||||
@click="navigate('/profile')"
|
||||
class="flex flex-col items-center gap-1 nav-tab flex-1"
|
||||
:class="{ 'nav-tab-active': isActive('/profile') }"
|
||||
>
|
||||
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-xs font-medium whitespace-nowrap">Profile</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,35 +89,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { h } from 'vue'
|
||||
import { useContentDiscovery, type AlgorithmId } from '../composables/useContentDiscovery'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
icon: () => h('svg', { class: 'w-6 h-6', fill: 'currentColor', viewBox: '0 0 24 24' },
|
||||
h('path', { d: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z' })
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'My List',
|
||||
path: '/library',
|
||||
icon: () => h('svg', { class: 'w-6 h-6', fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
||||
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z' })
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Profile',
|
||||
path: '/profile',
|
||||
icon: () => h('svg', { class: 'w-6 h-6', fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
||||
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z' })
|
||||
)
|
||||
}
|
||||
]
|
||||
const {
|
||||
activeAlgorithm,
|
||||
algorithms,
|
||||
setAlgorithm,
|
||||
isFilterActive,
|
||||
} = useContentDiscovery()
|
||||
|
||||
const showFilterSheet = ref(false)
|
||||
|
||||
const isOnFilmsPage = computed(() => route.path === '/')
|
||||
|
||||
const navigate = (path: string) => {
|
||||
router.push(path)
|
||||
@@ -55,6 +114,33 @@ const navigate = (path: string) => {
|
||||
const isActive = (path: string) => {
|
||||
return route.path === path
|
||||
}
|
||||
|
||||
/**
|
||||
* Tapping Films clears any active filter and navigates to /
|
||||
*/
|
||||
function handleFilmsClick() {
|
||||
if (isFilterActive.value) {
|
||||
setAlgorithm(activeAlgorithm.value!) // toggle off (sets to null)
|
||||
}
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
/** Navigate to My List and clear any active filter */
|
||||
function handleMyListClick() {
|
||||
if (isFilterActive.value) {
|
||||
setAlgorithm(activeAlgorithm.value!) // toggle off
|
||||
}
|
||||
navigate('/library')
|
||||
}
|
||||
|
||||
function selectAlgorithm(algo: AlgorithmId) {
|
||||
setAlgorithm(algo)
|
||||
showFilterSheet.value = false
|
||||
// Navigate to Films page if not already there
|
||||
if (route.path !== '/') {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -125,4 +211,95 @@ const isActive = (path: string) => {
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* --- Filter Bottom Sheet --- */
|
||||
.filter-sheet-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-sheet {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 16px 24px 32px;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom, 0) + 32px);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(60px);
|
||||
-webkit-backdrop-filter: blur(60px);
|
||||
border-radius: 24px 24px 0 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: none;
|
||||
box-shadow:
|
||||
0 -20px 60px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.filter-sheet-handle {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filter-option:active {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.filter-option-active {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-weight: 600;
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Sheet transition */
|
||||
.sheet-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.sheet-leave-active {
|
||||
transition: all 0.25s ease-in;
|
||||
}
|
||||
|
||||
.sheet-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sheet-enter-from .filter-sheet {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.sheet-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sheet-leave-to .filter-sheet {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,235 +3,146 @@
|
||||
<div v-if="showSplash" class="splash-screen">
|
||||
<div class="splash-content">
|
||||
<svg
|
||||
width="400"
|
||||
height="400"
|
||||
viewBox="0 0 1374 1401"
|
||||
width="600"
|
||||
height="320"
|
||||
viewBox="0 0 2051 1099"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="splash-logo"
|
||||
>
|
||||
<!-- Top Left Block -->
|
||||
<!-- Decorative Diagonal Shape 1 -->
|
||||
<path
|
||||
class="logo-stroke logo-block-1"
|
||||
d="M321.5 2.5V417.5H1.5V2.5H321.5Z"
|
||||
stroke="url(#paint1_linear_548_65)"
|
||||
stroke-width="3"
|
||||
class="logo-stroke deco-stroke-1"
|
||||
d="M48.7775 647.241L0.239258 364.582L350.626 304.395L634.938 546.554L48.7775 647.241Z"
|
||||
stroke="url(#splash_paint0)"
|
||||
stroke-width="6"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-1"
|
||||
d="M321.5 2.5V417.5H1.5V2.5H321.5Z"
|
||||
fill="url(#paint0_linear_548_65)"
|
||||
class="logo-fill deco-fill-1"
|
||||
d="M48.7775 647.241L0.239258 364.582L350.626 304.395L634.938 546.554L48.7775 647.241Z"
|
||||
fill="url(#splash_paint0)"
|
||||
/>
|
||||
|
||||
<!-- Top Right Block -->
|
||||
<!-- Decorative Diagonal Shape 2 -->
|
||||
<path
|
||||
class="logo-stroke logo-block-2"
|
||||
d="M1372.5 1.5V416.5H1052.5V1.5H1372.5Z"
|
||||
stroke="url(#paint3_linear_548_65)"
|
||||
stroke-width="3"
|
||||
class="logo-stroke deco-stroke-2"
|
||||
d="M605.933 260.538L895.592 501.778L1172.25 454.254L877.513 213.887L605.933 260.538Z"
|
||||
stroke="url(#splash_paint1)"
|
||||
stroke-width="6"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-2"
|
||||
d="M1372.5 1.5V416.5H1052.5V1.5H1372.5Z"
|
||||
fill="url(#paint2_linear_548_65)"
|
||||
class="logo-fill deco-fill-2"
|
||||
d="M605.933 260.538L895.592 501.778L1172.25 454.254L877.513 213.887L605.933 260.538Z"
|
||||
fill="url(#splash_paint1)"
|
||||
/>
|
||||
|
||||
<!-- Bottom Left Block -->
|
||||
<!-- Decorative Diagonal Shape 3 -->
|
||||
<path
|
||||
class="logo-stroke logo-block-3"
|
||||
d="M321.5 984.5V1399.5H1.5V984.5H321.5Z"
|
||||
stroke="url(#paint5_linear_548_65)"
|
||||
stroke-width="3"
|
||||
class="logo-stroke deco-stroke-3"
|
||||
d="M1123.72 171.595L1415.65 412.445L1694.92 364.474L1397.86 124.504L1123.72 171.595Z"
|
||||
stroke="url(#splash_paint2)"
|
||||
stroke-width="6"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-3"
|
||||
d="M321.5 984.5V1399.5H1.5V984.5H321.5Z"
|
||||
fill="url(#paint4_linear_548_65)"
|
||||
class="logo-fill deco-fill-3"
|
||||
d="M1123.72 171.595L1415.65 412.445L1694.92 364.474L1397.86 124.504L1123.72 171.595Z"
|
||||
fill="url(#splash_paint2)"
|
||||
/>
|
||||
|
||||
<!-- Center Bottom Block (Dark) -->
|
||||
<!-- Decorative Diagonal Shape 4 -->
|
||||
<path
|
||||
class="logo-stroke logo-block-4"
|
||||
d="M909.5 984.5V1399.5H464.5V984.5H909.5Z"
|
||||
stroke="url(#paint6_linear_548_65)"
|
||||
stroke-width="3"
|
||||
class="logo-stroke deco-stroke-4"
|
||||
d="M1978.53 24.7578L1646.37 81.8147L1943.39 321.789L2027.06 307.417L1978.53 24.7578Z"
|
||||
stroke="url(#splash_paint3)"
|
||||
stroke-width="6"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-4"
|
||||
d="M909.5 984.5V1399.5H464.5V984.5H909.5Z"
|
||||
fill="#1D1D1D"
|
||||
class="logo-fill deco-fill-4"
|
||||
d="M1978.53 24.7578L1646.37 81.8147L1943.39 321.789L2027.06 307.417L1978.53 24.7578Z"
|
||||
fill="url(#splash_paint3)"
|
||||
/>
|
||||
|
||||
<!-- Bottom Right Block -->
|
||||
<!-- Text: I -->
|
||||
<path
|
||||
class="logo-stroke logo-block-5"
|
||||
d="M1372.5 983.5V1398.5H1052.5V983.5H1372.5Z"
|
||||
stroke="url(#paint8_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-5"
|
||||
d="M1372.5 983.5V1398.5H1052.5V983.5H1372.5Z"
|
||||
fill="url(#paint7_linear_548_65)"
|
||||
class="text-letter text-letter-1"
|
||||
d="M48.9072 1093.3V779.537H121.443V1093.3H48.9072Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<!-- Center Diagonals -->
|
||||
<!-- Text: n -->
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-1"
|
||||
d="M467.042 558.5L630.433 841.5H469.695L306.305 558.5H467.042Z"
|
||||
stroke="url(#paint10_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-6"
|
||||
d="M467.042 558.5L630.433 841.5H469.695L306.305 558.5H467.042Z"
|
||||
fill="url(#paint9_linear_548_65)"
|
||||
class="text-letter text-letter-2"
|
||||
d="M399.801 1093.29H329.106V965.131C329.106 950.034 326.161 938.984 319.901 932.356C314.011 925.36 306.278 922.044 297.073 922.044C290.446 922.044 283.818 923.887 277.19 927.2C270.563 930.516 264.303 935.301 258.78 941.195C253.257 947.085 249.207 954.085 246.261 961.815V1092.92H175.566V863.489H239.266V902.525C245.157 893.319 252.521 885.218 261.358 878.956C270.195 872.698 280.504 867.91 291.919 864.594C303.333 861.281 315.851 859.809 329.106 859.809C344.571 859.809 356.724 862.387 365.929 867.542C375.134 872.698 382.129 879.694 387.284 888.163C392.068 896.635 395.384 906.209 397.223 916.153C399.066 926.465 399.801 936.039 399.801 945.98V1093.29Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<!-- Text: d -->
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-2"
|
||||
d="M832.507 558.5L995.897 841.5H835.092L671.701 558.5H832.507Z"
|
||||
stroke="url(#paint12_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-7"
|
||||
d="M832.507 558.5L995.897 841.5H835.092L671.701 558.5H832.507Z"
|
||||
fill="url(#paint11_linear_548_65)"
|
||||
class="text-letter text-letter-3"
|
||||
d="M432.951 978.757C432.951 956.663 437.368 936.775 446.206 918.732C455.044 900.686 467.194 886.691 482.288 876.379C497.386 866.071 515.429 860.544 535.311 860.544C551.511 860.544 566.239 864.228 579.497 871.961C592.752 879.695 602.691 889.637 609.689 902.526V770.689H680.384V1014.48C680.384 1021.48 681.486 1026.63 683.697 1029.58C685.907 1032.53 689.957 1034 695.846 1034.37V1093.29C683.329 1095.5 673.019 1096.97 665.287 1096.97C652.766 1096.97 643.194 1094.39 635.832 1088.87C628.467 1083.71 624.049 1076.35 622.206 1066.77L621.472 1054.99C613.001 1069.35 601.586 1080.03 587.597 1087.39C573.604 1094.39 558.509 1098.08 543.044 1098.08C527.579 1098.08 512.481 1095.13 499.226 1089.24C485.971 1083.34 474.189 1074.88 464.249 1064.2C454.306 1053.52 446.941 1040.99 441.418 1026.63C436.263 1012.27 433.319 996.435 433.319 979.127L432.951 978.757ZM610.057 1006.75V959.241C607.112 951.507 603.062 944.879 597.536 938.985C592.014 933.094 585.754 928.673 579.126 924.99C572.499 921.31 565.504 919.467 558.509 919.467C550.406 919.467 543.411 921.31 536.784 924.623C530.156 927.938 524.634 932.357 520.214 937.88C515.796 943.406 512.114 949.664 509.536 956.663C506.959 963.659 505.854 971.761 505.854 979.862C505.854 987.966 507.329 995.697 509.907 1002.7C512.851 1009.69 516.534 1015.59 521.319 1020.74C526.106 1025.89 531.996 1029.95 538.994 1032.89C545.989 1035.84 553.354 1037.31 561.454 1037.31C566.609 1037.31 571.394 1036.58 576.181 1035.1C580.969 1033.63 585.386 1031.42 589.804 1028.47C594.224 1025.53 597.907 1022.21 601.586 1018.53C605.269 1014.85 608.217 1010.43 610.424 1005.64L610.057 1006.75Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<!-- Text: e (first) -->
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-3"
|
||||
d="M1372.5 558.5V841.5H1200.56L1037.17 558.5H1372.5Z"
|
||||
stroke="url(#paint14_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-8"
|
||||
d="M1372.5 558.5V841.5H1200.56L1037.17 558.5H1372.5Z"
|
||||
fill="url(#paint13_linear_548_65)"
|
||||
class="text-letter text-letter-4"
|
||||
d="M846.059 1097.72C826.176 1097.72 808.868 1094.4 793.406 1088.51C777.941 1082.25 765.053 1073.78 754.376 1063.1C743.698 1052.42 735.595 1039.9 729.706 1025.9C724.183 1011.91 721.235 996.811 721.235 981.71C721.235 959.617 726.023 939.361 735.966 921.315C745.905 903.272 759.898 888.54 778.676 877.493C797.456 866.443 819.546 860.92 845.688 860.92C871.831 860.92 894.294 866.443 912.704 877.126C931.114 888.172 945.474 902.535 955.046 920.58C964.619 938.623 969.406 957.774 969.406 978.765C969.406 982.815 969.406 986.866 968.669 990.917C968.301 994.967 967.934 998.283 967.564 1001.23H795.981C796.718 1011.17 799.296 1019.64 804.451 1026.64C809.606 1033.64 815.866 1038.79 823.599 1042.11C831.328 1045.42 839.061 1047.26 847.531 1047.26C858.576 1047.26 868.519 1044.68 877.724 1039.9C886.929 1034.74 893.189 1028.11 896.501 1020.01L956.519 1037.32C950.626 1049.47 942.526 1059.78 931.849 1068.99C921.171 1078.2 908.651 1085.19 893.923 1090.72C879.196 1096.24 863.364 1098.82 845.688 1098.82L846.059 1097.72ZM895.396 957.039C894.661 947.832 891.716 939.361 886.929 932.732C882.141 925.736 876.251 920.21 869.253 916.53C862.258 912.846 853.791 910.636 844.586 910.636C836.116 910.636 828.016 912.479 820.651 916.53C813.289 920.21 807.396 925.736 802.978 932.365C798.558 938.993 795.984 947.462 794.878 957.039H895.396Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<!-- Text: e (second) -->
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-4"
|
||||
d="M101.646 558.5L265.036 841.5H1.5V558.5H101.646Z"
|
||||
stroke="url(#paint16_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-9"
|
||||
d="M101.646 558.5L265.036 841.5H1.5V558.5H101.646Z"
|
||||
fill="url(#paint15_linear_548_65)"
|
||||
class="text-letter text-letter-5"
|
||||
d="M1113.02 1097.72C1093.13 1097.72 1075.83 1094.4 1060.36 1088.51C1044.9 1082.25 1032.01 1073.78 1021.33 1063.1C1010.66 1052.42 1002.55 1039.9 996.664 1025.9C991.141 1011.91 988.196 996.811 988.196 981.71C988.196 959.617 992.981 939.361 1002.92 921.315C1012.86 903.272 1026.86 888.54 1045.63 877.493C1064.41 866.443 1086.51 860.92 1112.65 860.92C1138.79 860.92 1161.25 866.443 1179.66 877.126C1198.07 888.172 1212.43 902.535 1222 920.58C1231.58 938.623 1236.36 957.774 1236.36 978.765C1236.36 982.815 1236.36 986.866 1235.63 990.917C1235.26 994.967 1234.89 998.283 1234.52 1001.23H1062.94C1063.68 1011.17 1066.25 1019.64 1071.41 1026.64C1076.56 1033.64 1082.82 1038.79 1090.56 1042.11C1098.29 1045.42 1106.02 1047.26 1114.49 1047.26C1125.53 1047.26 1135.48 1044.68 1144.68 1039.9C1153.89 1034.74 1160.15 1028.11 1163.46 1020.01L1223.48 1037.32C1217.59 1049.47 1209.48 1059.78 1198.81 1068.99C1188.13 1078.2 1175.61 1085.19 1160.88 1090.72C1146.15 1096.24 1130.32 1098.82 1112.65 1098.82L1113.02 1097.72ZM1162.35 957.039C1161.62 947.832 1158.67 939.361 1153.89 932.732C1149.1 925.736 1143.21 920.21 1136.21 916.53C1129.22 912.846 1120.75 910.636 1111.54 910.636C1103.07 910.636 1094.97 912.479 1087.61 916.53C1080.25 920.21 1074.35 925.736 1069.94 932.365C1065.52 938.993 1062.94 947.462 1061.84 957.039H1162.35Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<!-- Center Circle -->
|
||||
<circle
|
||||
class="logo-stroke logo-circle"
|
||||
cx="687.5"
|
||||
cy="210.5"
|
||||
r="209"
|
||||
stroke="url(#paint17_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
<!-- Text: H -->
|
||||
<path
|
||||
class="text-letter text-letter-6"
|
||||
d="M1472.01 779.537V1093.3H1399.48V964.773H1274.29V1093.3H1201.75V779.537H1274.29V901.065H1399.48V779.537H1472.01Z"
|
||||
fill="white"
|
||||
/>
|
||||
<circle
|
||||
class="logo-fill logo-fill-10"
|
||||
cx="687.5"
|
||||
cy="210.5"
|
||||
r="209"
|
||||
fill="#1D1D1D"
|
||||
|
||||
<!-- Text: u -->
|
||||
<path
|
||||
class="text-letter text-letter-7"
|
||||
d="M1523.57 863.854H1594.26V995.323C1594.26 1008.95 1597.21 1019.63 1602.73 1026.99C1608.62 1034.36 1616.35 1038.04 1626.66 1038.04C1633.29 1038.04 1639.18 1036.94 1645.07 1034.73C1650.96 1032.52 1656.85 1029.2 1662.38 1024.42C1667.9 1019.63 1673.06 1013 1677.47 1005.26V864.221H1748.17V1015.21C1748.17 1022.2 1749.27 1026.99 1751.48 1029.94C1753.69 1032.89 1757.74 1034.36 1763.27 1034.73V1093.65C1756.64 1095.12 1751.11 1096.23 1746.33 1096.59C1741.54 1096.96 1737.12 1096.96 1733.44 1096.59C1720.92 1096.59 1711.35 1094.38 1703.98 1089.6C1696.62 1084.81 1692.2 1077.45 1690.36 1067.87L1688.89 1053.88C1678.21 1068.98 1665.32 1080.02 1649.49 1087.39C1634.03 1094.38 1616.35 1098.07 1596.47 1098.07C1572.9 1098.07 1554.86 1090.7 1542.34 1075.97C1529.83 1061.24 1523.57 1039.88 1523.57 1011.53V863.854Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<!-- Text: b -->
|
||||
<path
|
||||
class="text-letter text-letter-8"
|
||||
d="M1942.58 1097.71C1924.91 1097.71 1909.44 1094.02 1896.56 1086.66C1883.3 1079.29 1872.99 1068.61 1864.89 1054.99V1093.29H1803.4V770.689H1874.1V902.526C1881.83 889.269 1892.14 878.957 1904.66 871.594C1917.54 864.228 1932.64 860.544 1950.31 860.544C1964.67 860.544 1977.93 863.493 1990.45 869.383C2002.97 875.277 2013.28 883.746 2022.11 894.792C2030.95 905.842 2037.95 918.732 2043.1 933.094C2048.26 947.457 2050.47 963.292 2050.47 979.862C2050.47 996.435 2047.89 1011.53 2042.36 1026.27C2036.84 1040.99 2029.48 1053.52 2019.54 1064.2C2009.96 1074.88 1998.55 1083.34 1985.29 1089.24C1972.04 1095.13 1957.68 1098.08 1942.21 1098.08L1942.58 1097.71ZM1922.7 1038.05C1930.8 1038.05 1938.53 1036.58 1945.16 1033.63C1951.79 1030.68 1957.68 1026.63 1962.46 1021.48C1967.62 1016.32 1971.3 1010.06 1974.25 1003.06C1977.19 996.067 1978.3 988.334 1978.3 980.6C1978.3 969.55 1976.09 959.609 1971.67 950.402C1967.25 941.196 1960.99 933.829 1953.26 928.306C1945.53 922.783 1936.32 919.837 1926.38 919.837C1919.02 919.837 1912.02 921.677 1905.03 925.361C1898.4 929.044 1892.14 933.829 1886.98 939.723C1881.83 945.614 1877.41 952.242 1874.1 959.609V1007.48C1876.31 1011.9 1879.25 1016.32 1882.93 1020.37C1886.61 1024.42 1890.3 1027.37 1894.72 1030.32C1899.13 1032.89 1903.55 1035.1 1908.34 1036.58C1913.13 1038.05 1917.91 1038.78 1922.7 1038.78V1038.05Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_548_65" x1="124.804" y1="-297.569" x2="1351.77" y2="-186.399" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="splash_paint0" x1="245.48" y1="59.5055" x2="2566.89" y2="563.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_548_65" x1="0" y1="1" x2="323" y2="419" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_548_65" x1="1175.8" y1="-298.569" x2="2402.77" y2="-187.399" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="splash_paint1" x1="245.48" y1="59.5063" x2="2566.89" y2="563.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_548_65" x1="1051" y1="0" x2="1374" y2="418" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_548_65" x1="124.804" y1="684.431" x2="1351.77" y2="795.601" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="splash_paint2" x1="245.48" y1="59.5046" x2="2566.89" y2="563.411" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_548_65" x1="323" y1="1067.2" x2="6.09023e-06" y2="1316.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_548_65" x1="687" y1="1401" x2="687" y2="983" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_548_65" x1="1175.8" y1="683.431" x2="2402.77" y2="794.601" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="splash_paint3" x1="1793.47" y1="-187.406" x2="3219.26" y2="26.8634" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_548_65" x1="1374" y1="1066.2" x2="1051" y2="1315.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_548_65" x1="-202.64" y1="492.439" x2="1176.93" y2="666.265" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_548_65" x1="303.707" y1="557" x2="562.607" y2="896.973" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_548_65" x1="162.651" y1="492.439" x2="1542.49" y2="666.336" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_548_65" x1="669.104" y1="557" x2="927.989" y2="897.025" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_548_65" x1="512.679" y1="492.439" x2="1933.21" y2="676.92" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint14_linear_548_65" x1="1034.57" y1="557" x2="1291.29" y2="904.456" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint15_linear_548_65" x1="-411.498" y1="492.439" x2="715.627" y2="607.854" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_548_65" x1="0" y1="557" x2="268" y2="843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_548_65" x1="687.5" y1="0" x2="687.5" y2="421" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -272,69 +183,50 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.splash-logo {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
width: 280px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.splash-logo {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stroke Drawing Animation - Staggered timing */
|
||||
@media (min-width: 1280px) {
|
||||
.splash-logo {
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Decorative shapes - Stroke drawing animation
|
||||
======================================== */
|
||||
.logo-stroke {
|
||||
stroke-dasharray: 2000;
|
||||
stroke-dashoffset: 2000;
|
||||
stroke-dasharray: 3000;
|
||||
stroke-dashoffset: 3000;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
/* Top blocks draw first */
|
||||
.logo-block-1 {
|
||||
animation: drawStroke 1.5s ease-out 0s forwards, undrawStroke 1.5s ease-in 3.4s forwards;
|
||||
/* Staggered draw-in from left to right */
|
||||
.deco-stroke-1 {
|
||||
animation: drawStroke 1.2s ease-out 0s forwards, undrawStroke 1.2s ease-in 3.4s forwards;
|
||||
}
|
||||
|
||||
.logo-block-2 {
|
||||
animation: drawStroke 1.5s ease-out 0.2s forwards, undrawStroke 1.5s ease-in 3.2s forwards;
|
||||
.deco-stroke-2 {
|
||||
animation: drawStroke 1.2s ease-out 0.2s forwards, undrawStroke 1.2s ease-in 3.2s forwards;
|
||||
}
|
||||
|
||||
/* Center circle draws after top blocks */
|
||||
.logo-circle {
|
||||
animation: drawStroke 2s ease-out 0.4s forwards, undrawStroke 2s ease-in 2.5s forwards;
|
||||
.deco-stroke-3 {
|
||||
animation: drawStroke 1.2s ease-out 0.4s forwards, undrawStroke 1.2s ease-in 3.0s forwards;
|
||||
}
|
||||
|
||||
/* Diagonals draw in sequence */
|
||||
.logo-diagonal-1 {
|
||||
animation: drawStroke 1.5s ease-out 0.6s forwards, undrawStroke 1.5s ease-in 2.8s forwards;
|
||||
}
|
||||
|
||||
.logo-diagonal-2 {
|
||||
animation: drawStroke 1.5s ease-out 0.8s forwards, undrawStroke 1.5s ease-in 2.6s forwards;
|
||||
}
|
||||
|
||||
.logo-diagonal-3 {
|
||||
animation: drawStroke 1.5s ease-out 1.0s forwards, undrawStroke 1.5s ease-in 2.4s forwards;
|
||||
}
|
||||
|
||||
.logo-diagonal-4 {
|
||||
animation: drawStroke 1.5s ease-out 1.2s forwards, undrawStroke 1.5s ease-in 2.2s forwards;
|
||||
}
|
||||
|
||||
/* Bottom blocks draw last */
|
||||
.logo-block-3 {
|
||||
animation: drawStroke 1.5s ease-out 1.4s forwards, undrawStroke 1.5s ease-in 2.0s forwards;
|
||||
}
|
||||
|
||||
.logo-block-4 {
|
||||
animation: drawStroke 1.5s ease-out 1.6s forwards, undrawStroke 1.5s ease-in 1.8s forwards;
|
||||
}
|
||||
|
||||
.logo-block-5 {
|
||||
animation: drawStroke 1.5s ease-out 1.8s forwards, undrawStroke 1.5s ease-in 1.6s forwards;
|
||||
.deco-stroke-4 {
|
||||
animation: drawStroke 1.2s ease-out 0.6s forwards, undrawStroke 1.2s ease-in 2.8s forwards;
|
||||
}
|
||||
|
||||
@keyframes drawStroke {
|
||||
@@ -348,27 +240,32 @@ onMounted(() => {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 2000;
|
||||
stroke-dashoffset: 3000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fill Appearance Animation - starts after strokes mostly drawn */
|
||||
/* ========================================
|
||||
Decorative shapes - Fill animation
|
||||
======================================== */
|
||||
.logo-fill {
|
||||
opacity: 0;
|
||||
animation: fillAppear 0.8s ease-out 1.2s forwards, fillDisappear 0.8s ease-in 3s forwards;
|
||||
}
|
||||
|
||||
/* Stagger fill animations slightly for draw */
|
||||
.logo-fill-1 { animation-delay: 1.2s, 3.9s; }
|
||||
.logo-fill-2 { animation-delay: 1.3s, 3.8s; }
|
||||
.logo-fill-3 { animation-delay: 1.4s, 3.7s; }
|
||||
.logo-fill-4 { animation-delay: 1.5s, 3.6s; }
|
||||
.logo-fill-5 { animation-delay: 1.6s, 3.5s; }
|
||||
.logo-fill-6 { animation-delay: 1.7s, 3.4s; }
|
||||
.logo-fill-7 { animation-delay: 1.8s, 3.3s; }
|
||||
.logo-fill-8 { animation-delay: 1.9s, 3.2s; }
|
||||
.logo-fill-9 { animation-delay: 2.0s, 3.1s; }
|
||||
.logo-fill-10 { animation-delay: 2.1s, 3.0s; }
|
||||
.deco-fill-1 {
|
||||
animation: fillAppear 0.6s ease-out 0.8s forwards, fillDisappear 0.6s ease-in 3.6s forwards;
|
||||
}
|
||||
|
||||
.deco-fill-2 {
|
||||
animation: fillAppear 0.6s ease-out 1.0s forwards, fillDisappear 0.6s ease-in 3.4s forwards;
|
||||
}
|
||||
|
||||
.deco-fill-3 {
|
||||
animation: fillAppear 0.6s ease-out 1.2s forwards, fillDisappear 0.6s ease-in 3.2s forwards;
|
||||
}
|
||||
|
||||
.deco-fill-4 {
|
||||
animation: fillAppear 0.6s ease-out 1.4s forwards, fillDisappear 0.6s ease-in 3.0s forwards;
|
||||
}
|
||||
|
||||
@keyframes fillAppear {
|
||||
to {
|
||||
@@ -385,7 +282,71 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Splash Fade Out Transition */
|
||||
/* ========================================
|
||||
Text letters - Fade in one by one, then out
|
||||
======================================== */
|
||||
.text-letter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Letters appear sequentially after the decorative shapes start filling */
|
||||
.text-letter-1 {
|
||||
animation: letterIn 0.4s ease-out 1.0s forwards, letterOut 0.4s ease-in 3.8s forwards;
|
||||
}
|
||||
|
||||
.text-letter-2 {
|
||||
animation: letterIn 0.4s ease-out 1.1s forwards, letterOut 0.4s ease-in 3.7s forwards;
|
||||
}
|
||||
|
||||
.text-letter-3 {
|
||||
animation: letterIn 0.4s ease-out 1.2s forwards, letterOut 0.4s ease-in 3.6s forwards;
|
||||
}
|
||||
|
||||
.text-letter-4 {
|
||||
animation: letterIn 0.4s ease-out 1.3s forwards, letterOut 0.4s ease-in 3.5s forwards;
|
||||
}
|
||||
|
||||
.text-letter-5 {
|
||||
animation: letterIn 0.4s ease-out 1.4s forwards, letterOut 0.4s ease-in 3.4s forwards;
|
||||
}
|
||||
|
||||
.text-letter-6 {
|
||||
animation: letterIn 0.4s ease-out 1.5s forwards, letterOut 0.4s ease-in 3.3s forwards;
|
||||
}
|
||||
|
||||
.text-letter-7 {
|
||||
animation: letterIn 0.4s ease-out 1.6s forwards, letterOut 0.4s ease-in 3.2s forwards;
|
||||
}
|
||||
|
||||
.text-letter-8 {
|
||||
animation: letterIn 0.4s ease-out 1.7s forwards, letterOut 0.4s ease-in 3.1s forwards;
|
||||
}
|
||||
|
||||
@keyframes letterIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes letterOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Splash screen fade-out transition
|
||||
======================================== */
|
||||
.splash-fade-leave-active {
|
||||
transition: opacity 1.5s ease;
|
||||
}
|
||||
|
||||
396
src/components/SplashIntroIcon.vue
Normal file
396
src/components/SplashIntroIcon.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<Transition name="splash-fade">
|
||||
<div v-if="showSplash" class="splash-screen">
|
||||
<div class="splash-content">
|
||||
<svg
|
||||
width="400"
|
||||
height="400"
|
||||
viewBox="0 0 1374 1401"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="splash-logo"
|
||||
>
|
||||
<!-- Top Left Block -->
|
||||
<path
|
||||
class="logo-stroke logo-block-1"
|
||||
d="M321.5 2.5V417.5H1.5V2.5H321.5Z"
|
||||
stroke="url(#paint1_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-1"
|
||||
d="M321.5 2.5V417.5H1.5V2.5H321.5Z"
|
||||
fill="url(#paint0_linear_548_65)"
|
||||
/>
|
||||
|
||||
<!-- Top Right Block -->
|
||||
<path
|
||||
class="logo-stroke logo-block-2"
|
||||
d="M1372.5 1.5V416.5H1052.5V1.5H1372.5Z"
|
||||
stroke="url(#paint3_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-2"
|
||||
d="M1372.5 1.5V416.5H1052.5V1.5H1372.5Z"
|
||||
fill="url(#paint2_linear_548_65)"
|
||||
/>
|
||||
|
||||
<!-- Bottom Left Block -->
|
||||
<path
|
||||
class="logo-stroke logo-block-3"
|
||||
d="M321.5 984.5V1399.5H1.5V984.5H321.5Z"
|
||||
stroke="url(#paint5_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-3"
|
||||
d="M321.5 984.5V1399.5H1.5V984.5H321.5Z"
|
||||
fill="url(#paint4_linear_548_65)"
|
||||
/>
|
||||
|
||||
<!-- Center Bottom Block (Dark) -->
|
||||
<path
|
||||
class="logo-stroke logo-block-4"
|
||||
d="M909.5 984.5V1399.5H464.5V984.5H909.5Z"
|
||||
stroke="url(#paint6_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-4"
|
||||
d="M909.5 984.5V1399.5H464.5V984.5H909.5Z"
|
||||
fill="#1D1D1D"
|
||||
/>
|
||||
|
||||
<!-- Bottom Right Block -->
|
||||
<path
|
||||
class="logo-stroke logo-block-5"
|
||||
d="M1372.5 983.5V1398.5H1052.5V983.5H1372.5Z"
|
||||
stroke="url(#paint8_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-5"
|
||||
d="M1372.5 983.5V1398.5H1052.5V983.5H1372.5Z"
|
||||
fill="url(#paint7_linear_548_65)"
|
||||
/>
|
||||
|
||||
<!-- Center Diagonals -->
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-1"
|
||||
d="M467.042 558.5L630.433 841.5H469.695L306.305 558.5H467.042Z"
|
||||
stroke="url(#paint10_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-6"
|
||||
d="M467.042 558.5L630.433 841.5H469.695L306.305 558.5H467.042Z"
|
||||
fill="url(#paint9_linear_548_65)"
|
||||
/>
|
||||
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-2"
|
||||
d="M832.507 558.5L995.897 841.5H835.092L671.701 558.5H832.507Z"
|
||||
stroke="url(#paint12_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-7"
|
||||
d="M832.507 558.5L995.897 841.5H835.092L671.701 558.5H832.507Z"
|
||||
fill="url(#paint11_linear_548_65)"
|
||||
/>
|
||||
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-3"
|
||||
d="M1372.5 558.5V841.5H1200.56L1037.17 558.5H1372.5Z"
|
||||
stroke="url(#paint14_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-8"
|
||||
d="M1372.5 558.5V841.5H1200.56L1037.17 558.5H1372.5Z"
|
||||
fill="url(#paint13_linear_548_65)"
|
||||
/>
|
||||
|
||||
<path
|
||||
class="logo-stroke logo-diagonal logo-diagonal-4"
|
||||
d="M101.646 558.5L265.036 841.5H1.5V558.5H101.646Z"
|
||||
stroke="url(#paint16_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
class="logo-fill logo-fill-9"
|
||||
d="M101.646 558.5L265.036 841.5H1.5V558.5H101.646Z"
|
||||
fill="url(#paint15_linear_548_65)"
|
||||
/>
|
||||
|
||||
<!-- Center Circle -->
|
||||
<circle
|
||||
class="logo-stroke logo-circle"
|
||||
cx="687.5"
|
||||
cy="210.5"
|
||||
r="209"
|
||||
stroke="url(#paint17_linear_548_65)"
|
||||
stroke-width="3"
|
||||
fill="transparent"
|
||||
/>
|
||||
<circle
|
||||
class="logo-fill logo-fill-10"
|
||||
cx="687.5"
|
||||
cy="210.5"
|
||||
r="209"
|
||||
fill="#1D1D1D"
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_548_65" x1="124.804" y1="-297.569" x2="1351.77" y2="-186.399" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_548_65" x1="0" y1="1" x2="323" y2="419" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_548_65" x1="1175.8" y1="-298.569" x2="2402.77" y2="-187.399" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_548_65" x1="1051" y1="0" x2="1374" y2="418" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_548_65" x1="124.804" y1="684.431" x2="1351.77" y2="795.601" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_548_65" x1="323" y1="1067.2" x2="6.09023e-06" y2="1316.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_548_65" x1="687" y1="1401" x2="687" y2="983" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_548_65" x1="1175.8" y1="683.431" x2="2402.77" y2="794.601" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_548_65" x1="1374" y1="1066.2" x2="1051" y2="1315.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_548_65" x1="-202.64" y1="492.439" x2="1176.93" y2="666.265" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_548_65" x1="303.707" y1="557" x2="562.607" y2="896.973" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_548_65" x1="162.651" y1="492.439" x2="1542.49" y2="666.336" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_548_65" x1="669.104" y1="557" x2="927.989" y2="897.025" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_548_65" x1="512.679" y1="492.439" x2="1933.21" y2="676.92" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint14_linear_548_65" x1="1034.57" y1="557" x2="1291.29" y2="904.456" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint15_linear_548_65" x1="-411.498" y1="492.439" x2="715.627" y2="607.854" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_548_65" x1="0" y1="557" x2="268" y2="843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_548_65" x1="687.5" y1="0" x2="687.5" y2="421" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const SPLASH_KEY = 'indeedhub_splash_shown'
|
||||
const alreadyShown = sessionStorage.getItem(SPLASH_KEY) === 'true'
|
||||
const showSplash = ref(!alreadyShown)
|
||||
|
||||
onMounted(() => {
|
||||
if (showSplash.value) {
|
||||
sessionStorage.setItem(SPLASH_KEY, 'true')
|
||||
// Hide splash after animation completes (5s total animation)
|
||||
setTimeout(() => {
|
||||
showSplash.value = false
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splash-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.splash-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.splash-logo {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.splash-logo {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stroke Drawing Animation - Staggered timing */
|
||||
.logo-stroke {
|
||||
stroke-dasharray: 2000;
|
||||
stroke-dashoffset: 2000;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
/* Top blocks draw first */
|
||||
.logo-block-1 {
|
||||
animation: drawStroke 1.5s ease-out 0s forwards, undrawStroke 1.5s ease-in 3.4s forwards;
|
||||
}
|
||||
|
||||
.logo-block-2 {
|
||||
animation: drawStroke 1.5s ease-out 0.2s forwards, undrawStroke 1.5s ease-in 3.2s forwards;
|
||||
}
|
||||
|
||||
/* Center circle draws after top blocks */
|
||||
.logo-circle {
|
||||
animation: drawStroke 2s ease-out 0.4s forwards, undrawStroke 2s ease-in 2.5s forwards;
|
||||
}
|
||||
|
||||
/* Diagonals draw in sequence */
|
||||
.logo-diagonal-1 {
|
||||
animation: drawStroke 1.5s ease-out 0.6s forwards, undrawStroke 1.5s ease-in 2.8s forwards;
|
||||
}
|
||||
|
||||
.logo-diagonal-2 {
|
||||
animation: drawStroke 1.5s ease-out 0.8s forwards, undrawStroke 1.5s ease-in 2.6s forwards;
|
||||
}
|
||||
|
||||
.logo-diagonal-3 {
|
||||
animation: drawStroke 1.5s ease-out 1.0s forwards, undrawStroke 1.5s ease-in 2.4s forwards;
|
||||
}
|
||||
|
||||
.logo-diagonal-4 {
|
||||
animation: drawStroke 1.5s ease-out 1.2s forwards, undrawStroke 1.5s ease-in 2.2s forwards;
|
||||
}
|
||||
|
||||
/* Bottom blocks draw last */
|
||||
.logo-block-3 {
|
||||
animation: drawStroke 1.5s ease-out 1.4s forwards, undrawStroke 1.5s ease-in 2.0s forwards;
|
||||
}
|
||||
|
||||
.logo-block-4 {
|
||||
animation: drawStroke 1.5s ease-out 1.6s forwards, undrawStroke 1.5s ease-in 1.8s forwards;
|
||||
}
|
||||
|
||||
.logo-block-5 {
|
||||
animation: drawStroke 1.5s ease-out 1.8s forwards, undrawStroke 1.5s ease-in 1.6s forwards;
|
||||
}
|
||||
|
||||
@keyframes drawStroke {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes undrawStroke {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fill Appearance Animation - starts after strokes mostly drawn */
|
||||
.logo-fill {
|
||||
opacity: 0;
|
||||
animation: fillAppear 0.8s ease-out 1.2s forwards, fillDisappear 0.8s ease-in 3s forwards;
|
||||
}
|
||||
|
||||
/* Stagger fill animations slightly for draw */
|
||||
.logo-fill-1 { animation-delay: 1.2s, 3.9s; }
|
||||
.logo-fill-2 { animation-delay: 1.3s, 3.8s; }
|
||||
.logo-fill-3 { animation-delay: 1.4s, 3.7s; }
|
||||
.logo-fill-4 { animation-delay: 1.5s, 3.6s; }
|
||||
.logo-fill-5 { animation-delay: 1.6s, 3.5s; }
|
||||
.logo-fill-6 { animation-delay: 1.7s, 3.4s; }
|
||||
.logo-fill-7 { animation-delay: 1.8s, 3.3s; }
|
||||
.logo-fill-8 { animation-delay: 1.9s, 3.2s; }
|
||||
.logo-fill-9 { animation-delay: 2.0s, 3.1s; }
|
||||
.logo-fill-10 { animation-delay: 2.1s, 3.0s; }
|
||||
|
||||
@keyframes fillAppear {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fillDisappear {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Splash Fade Out Transition */
|
||||
.splash-fade-leave-active {
|
||||
transition: opacity 1.5s ease;
|
||||
}
|
||||
|
||||
.splash-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user