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:
Dorian
2026-02-12 12:24:58 +00:00
parent c970f5b29f
commit 725896673c
42 changed files with 3767 additions and 1329 deletions

View File

@@ -3,8 +3,6 @@
<SplashIntro />
<div class="browse-view">
<!-- Header / Navigation -->
<AppHeader @openAuth="showAuthModal = true" />
<!-- Hero / Featured Content -->
<section class="relative h-[56vh] md:h-[61vh] overflow-hidden">
@@ -59,66 +57,187 @@
</div>
</section>
<!-- Content Rows -->
<!-- Content Section -->
<section class="relative pt-8 pb-20 px-4">
<div class="mx-auto space-y-12">
<!-- Featured Films -->
<ContentRow
title="Featured Films"
:contents="featuredFilms"
@content-click="handleContentClick"
/>
<!-- ===== MY LIST TAB ===== -->
<template v-if="isMyListTab">
<!-- Not logged in -->
<div v-if="!isLoggedInAnywhere" class="text-center py-16">
<div class="text-white/50 text-lg mb-6">Sign in to save films to your list</div>
<button @click="$emit('openAuth')" class="hero-play-button inline-block">Sign In</button>
</div>
<!-- New Releases -->
<ContentRow
title="New Releases"
:contents="newReleases"
@content-click="handleContentClick"
/>
<!-- Logged in: library content -->
<template v-else>
<!-- Continue Watching -->
<div v-if="continueWatching.length > 0" class="content-row">
<h2 class="content-row-title text-xl md:text-2xl font-bold text-white mb-6 px-4 uppercase">Continue Watching</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 md:gap-6 px-4">
<div
v-for="item in continueWatching"
:key="item.content.id"
class="content-card group/card cursor-pointer"
@click="handleContentClick(item.content)"
>
<div class="glass-card rounded-lg p-1.5 transition-all duration-300 relative">
<img
:src="item.content.thumbnail"
:alt="item.content.title"
class="w-full aspect-[2/3] object-contain rounded-md bg-neutral-900"
loading="lazy"
/>
<div class="absolute bottom-2 left-2 right-2 h-1 bg-white/10 rounded-full overflow-hidden">
<div class="h-full bg-white/80 rounded-full" :style="{ width: `${item.progress}%` }"></div>
</div>
</div>
<div class="mt-2">
<h3 class="text-sm md:text-base font-semibold text-white truncate">{{ item.content.title }}</h3>
</div>
</div>
</div>
</div>
<!-- Bitcoin & Crypto -->
<ContentRow
title="Bitcoin & Cryptocurrency"
:contents="bitcoinFilms"
@content-click="handleContentClick"
/>
<!-- Saved Films -->
<div v-if="myListContent.length > 0" class="content-row">
<h2 class="content-row-title text-xl md:text-2xl font-bold text-white mb-6 px-4 uppercase">Saved Films</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 md:gap-6 px-4">
<div
v-for="content in myListContent"
:key="content.id"
class="content-card group/card cursor-pointer"
@click="handleContentClick(content)"
>
<div class="glass-card rounded-lg p-1.5 transition-all duration-300">
<img
:src="content.thumbnail"
:alt="content.title"
class="w-full aspect-[2/3] object-contain rounded-md bg-neutral-900"
loading="lazy"
/>
</div>
<div class="mt-2">
<h3 class="text-sm md:text-base font-semibold text-white truncate">{{ content.title }}</h3>
<p class="text-xs text-white/60 truncate hidden md:block">{{ content.description }}</p>
</div>
</div>
</div>
</div>
<!-- Documentaries -->
<ContentRow
title="Documentaries"
:contents="documentaries"
@content-click="handleContentClick"
/>
<!-- Rentals -->
<div v-if="rentedContent.length > 0" class="content-row">
<h2 class="content-row-title text-xl md:text-2xl font-bold text-white mb-6 px-4 uppercase">My Rentals</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 md:gap-6 px-4">
<div
v-for="content in rentedContent"
:key="content.id"
class="content-card group/card cursor-pointer"
@click="handleContentClick(content)"
>
<div class="glass-card rounded-lg p-1.5 transition-all duration-300 relative">
<img
:src="content.thumbnail"
:alt="content.title"
class="w-full aspect-[2/3] object-contain rounded-md bg-neutral-900"
loading="lazy"
/>
<div class="absolute top-3 right-3 bg-black/70 backdrop-blur-md px-2 py-0.5 rounded-full text-xs text-white/80">
48h left
</div>
</div>
<div class="mt-2">
<h3 class="text-sm md:text-base font-semibold text-white truncate">{{ content.title }}</h3>
</div>
</div>
</div>
</div>
<!-- Independent Cinema -->
<ContentRow
title="Independent Cinema"
:contents="independentCinema"
@content-click="handleContentClick"
/>
<!-- Empty library -->
<div v-if="continueWatching.length === 0 && myListContent.length === 0 && rentedContent.length === 0" class="text-center py-16">
<div class="text-white/50 text-lg mb-6">Your list is empty</div>
<router-link to="/" class="hero-play-button inline-block text-decoration-none">Browse Films</router-link>
</div>
</template>
</template>
<!-- Dramas -->
<ContentRow
title="Drama Films"
:contents="dramas"
@content-click="handleContentClick"
/>
<!-- ===== FILMS TAB: Filtered Grid ===== -->
<template v-else-if="isFilterActive">
<div class="content-row">
<h2 class="content-row-title text-xl md:text-2xl font-bold text-white mb-6 px-4 uppercase">
{{ activeAlgorithmLabel }}
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 md:gap-6 px-4">
<div
v-for="content in filteredContent"
:key="content.id"
class="content-card group/card cursor-pointer"
@click="handleContentClick(content)"
>
<div class="glass-card rounded-lg p-1.5 transition-all duration-300">
<img
:src="content.thumbnail"
:alt="content.title"
class="w-full aspect-[2/3] object-contain rounded-md bg-neutral-900"
loading="lazy"
/>
</div>
<div class="mt-2">
<h3 class="text-sm md:text-base font-semibold text-white truncate">{{ content.title }}</h3>
<p class="text-xs text-white/60 truncate hidden md:block">{{ content.description }}</p>
</div>
</div>
</div>
</div>
</template>
<!-- ===== FILMS TAB: Default Category Rows ===== -->
<template v-else>
<ContentRow
title="Featured Films"
:contents="featuredFilms"
@content-click="handleContentClick"
/>
<ContentRow
title="New Releases"
:contents="newReleases"
@content-click="handleContentClick"
/>
<ContentRow
title="Bitcoin & Cryptocurrency"
:contents="bitcoinContent"
@content-click="handleContentClick"
/>
<ContentRow
title="Documentaries"
:contents="documentaryContent"
@content-click="handleContentClick"
/>
<ContentRow
title="Independent Cinema"
:contents="independentCinema"
@content-click="handleContentClick"
/>
<ContentRow
title="Drama Films"
:contents="dramas"
@content-click="handleContentClick"
/>
</template>
</div>
</section>
<!-- Modals -->
<AuthModal
:isOpen="showAuthModal"
@close="showAuthModal = false"
@success="handleAuthSuccess"
/>
<ContentDetailModal
:isOpen="showDetailModal"
:content="selectedContent"
@close="showDetailModal = false"
@openAuth="showAuthModal = true"
@openAuth="$emit('openAuth')"
/>
<!-- Hero-only modals (for direct Play button) -->
@@ -137,45 +256,124 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import ContentRow from '../components/ContentRow.vue'
import SplashIntro from '../components/SplashIntro.vue'
import AppHeader from '../components/AppHeader.vue'
import AuthModal from '../components/AuthModal.vue'
import ContentDetailModal from '../components/ContentDetailModal.vue'
import SubscriptionModal from '../components/SubscriptionModal.vue'
import VideoPlayer from '../components/VideoPlayer.vue'
import { useContentStore } from '../stores/content'
import { useAuth } from '../composables/useAuth'
import { useAccounts } from '../composables/useAccounts'
import { useContentDiscovery } from '../composables/useContentDiscovery'
import { indeeHubFilms, bitcoinFilms, documentaries } from '../data/indeeHubFilms'
import type { Content } from '../types/content'
const emit = defineEmits<{ (e: 'openAuth'): void }>()
const route = useRoute()
const contentStore = useContentStore()
const { isAuthenticated, hasActiveSubscription } = useAuth()
const { isLoggedIn: isNostrLoggedIn } = useAccounts()
const { sortContent, isFilterActive, activeAlgorithmLabel } = useContentDiscovery()
const featuredContent = computed(() => contentStore.featuredContent)
// Determine active tab from route path
const isMyListTab = computed(() => route.path === '/library')
// Auth: either app auth or Nostr
const isLoggedInAnywhere = computed(() => isAuthenticated.value || isNostrLoggedIn.value)
// ===== FILMS TAB DATA =====
// The "continue watching" item shown in the hero on My List
const resumeItem = computed(() => continueWatching.value[0] ?? null)
// Hero banner: resume film on My List, top-ranked on filter, or default
const featuredContent = computed(() => {
if (isMyListTab.value && isLoggedInAnywhere.value && resumeItem.value) {
return resumeItem.value.content
}
if (isFilterActive.value && filteredContent.value.length > 0) {
return filteredContent.value[0]
}
return contentStore.featuredContent
})
// Default category rows (unfiltered)
const featuredFilms = computed(() => contentStore.contentRows.featured)
const newReleases = computed(() => contentStore.contentRows.newReleases)
const bitcoinFilms = computed(() => contentStore.contentRows.bitcoin)
const bitcoinContent = computed(() => contentStore.contentRows.bitcoin)
const independentCinema = computed(() => contentStore.contentRows.independent)
const dramas = computed(() => contentStore.contentRows.dramas)
const documentaries = computed(() => contentStore.contentRows.documentaries)
const documentaryContent = computed(() => contentStore.contentRows.documentaries)
// Deduplicated + sorted content for filter grid view
const filteredContent = computed(() => {
const rows = contentStore.contentRows
const seen = new Set<string>()
const allContent: Content[] = []
for (const row of Object.values(rows)) {
for (const item of row) {
if (!seen.has(item.id)) {
seen.add(item.id)
allContent.push(item)
}
}
}
return sortContent.value(allContent)
})
// ===== MY LIST TAB DATA =====
const continueWatching = ref<Array<{ content: Content; progress: number }>>([])
const myListContent = ref<Content[]>([])
const rentedContent = ref<Content[]>([])
/**
* Populate library with dummy data from the film catalog.
* In production this would come from the API.
*/
function loadDummyLibrary() {
if (myListContent.value.length > 0) return // Already loaded
continueWatching.value = indeeHubFilms.slice(0, 3).map((film) => ({
content: film,
progress: Math.floor(Math.random() * 70) + 10,
}))
myListContent.value = [
...bitcoinFilms.slice(0, 3),
...indeeHubFilms.slice(5, 8),
]
rentedContent.value = documentaries.slice(0, 2)
}
// Load library data when the tab becomes active and user is logged in
watch([isMyListTab, isLoggedInAnywhere], ([onListTab, loggedIn]) => {
if (onListTab && loggedIn) {
loadDummyLibrary()
}
}, { immediate: true })
// ===== SHARED =====
const showAuthModal = ref(false)
const showDetailModal = ref(false)
const showSubscriptionModal = ref(false)
const showVideoPlayer = ref(false)
const selectedContent = ref<Content | null>(null)
// Content card click -> always open detail modal
const handleContentClick = (content: Content) => {
selectedContent.value = content
showDetailModal.value = true
}
// Hero Play button -> direct play flow (skips detail modal)
const handlePlayClick = () => {
if (!isAuthenticated.value) {
showAuthModal.value = true
emit('openAuth')
return
}
@@ -185,20 +383,14 @@ const handlePlayClick = () => {
return
}
// No subscription - show subscription modal
showSubscriptionModal.value = true
}
// Hero More Info button -> open detail modal for featured content
const handleInfoClick = () => {
selectedContent.value = featuredContent.value
showDetailModal.value = true
}
function handleAuthSuccess() {
showAuthModal.value = false
}
function handleSubscriptionSuccess() {
showSubscriptionModal.value = false
}
@@ -243,6 +435,7 @@ onMounted(() => {
transition: all 0.3s ease;
white-space: nowrap;
letter-spacing: 0.02em;
text-decoration: none;
}
.hero-play-button::before {
@@ -319,4 +512,28 @@ onMounted(() => {
font-size: 16px;
}
}
/* Content section styles */
.content-row-title {
background: linear-gradient(to right, #fafafa, #9ca3af);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 0.05em;
}
.glass-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.group\/card:hover .glass-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2), 0 0 20px rgba(255, 255, 255, 0.05);
transform: translateY(-4px);
}
</style>