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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user