- Updated the WebhooksService to differentiate between 'InvoiceSettled' and 'InvoicePaymentSettled' events, ensuring accurate payment confirmation and logging. - Enhanced the useAccounts composable to improve display name handling by skipping generic placeholder names and ensuring the most recent profile metadata is fetched from multiple relays. - Modified the IndeehubApiService to handle JWT token refresh failures gracefully, allowing public endpoints to function without authentication. - Updated content store logic to fetch all published projects from the public API, ensuring backstage content is visible to all users regardless of their active content source. These changes improve the reliability of payment processing, enhance user profile representation, and ensure content visibility across the application.
335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import type { Content } from '../types/content'
|
|
import { indeeHubFilms, bitcoinFilms, documentaries, dramas } from '../data/indeeHubFilms'
|
|
import { topDocFilms, topDocBitcoin, topDocCrypto, topDocMoney, topDocEconomics } from '../data/topDocFilms'
|
|
import { contentService } from '../services/content.service'
|
|
import { mapApiProjectsToContents } from '../utils/mappers'
|
|
import { useContentSourceStore } from './contentSource'
|
|
import { indeehubApiService } from '../services/indeehub-api.service'
|
|
// useFilmmaker import removed — mergePublishedFilmmakerProjects
|
|
// now fetches from the public API so all users see backstage content
|
|
import { USE_MOCK as USE_MOCK_DATA } from '../utils/mock'
|
|
|
|
export const useContentStore = defineStore('content', () => {
|
|
const featuredContent = ref<Content | null>(null)
|
|
const contentRows = ref<{ [key: string]: Content[] }>({
|
|
featured: [],
|
|
newReleases: [],
|
|
bitcoin: [],
|
|
documentaries: [],
|
|
dramas: [],
|
|
independent: []
|
|
})
|
|
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
/**
|
|
* Fetch content from the original API (external IndeeHub)
|
|
*/
|
|
async function fetchContentFromApi() {
|
|
try {
|
|
const projects = await contentService.getProjects({ status: 'published' })
|
|
|
|
if (projects.length === 0) {
|
|
// No published content yet — not an error, use placeholder content
|
|
console.info('No published content from API, using placeholder content.')
|
|
await fetchContentFromMock()
|
|
return
|
|
}
|
|
|
|
const allContent = mapApiProjectsToContents(projects)
|
|
|
|
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
|
|
featuredContent.value = featuredFilm
|
|
|
|
const films = allContent.filter(c => c.type === 'film')
|
|
const bitcoinContent = allContent.filter(c =>
|
|
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
|
|
)
|
|
const docs = allContent.filter(c =>
|
|
c.categories?.some(cat => cat.toLowerCase().includes('documentary'))
|
|
)
|
|
const dramaContent = allContent.filter(c =>
|
|
c.categories?.some(cat => cat.toLowerCase().includes('drama'))
|
|
)
|
|
|
|
contentRows.value = {
|
|
featured: allContent.slice(0, 10),
|
|
newReleases: films.slice(0, 8),
|
|
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
|
|
documentaries: docs.length > 0 ? docs : films.slice(0, 6),
|
|
dramas: dramaContent.length > 0 ? dramaContent : films.slice(0, 6),
|
|
independent: films.slice(0, 10)
|
|
}
|
|
} catch (err) {
|
|
console.error('API fetch failed:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch content from our self-hosted IndeeHub API
|
|
*/
|
|
async function fetchContentFromIndeehubApi() {
|
|
try {
|
|
const response = await indeehubApiService.getProjects({ fresh: true })
|
|
|
|
// Handle both array responses and wrapped responses like { data: [...] }
|
|
const projects = Array.isArray(response) ? response : (response as any)?.data ?? []
|
|
|
|
if (!Array.isArray(projects) || projects.length === 0) {
|
|
// No published content yet — not an error, just use mock/placeholder data
|
|
console.info('No published content in backend yet, using placeholder content.')
|
|
await fetchContentFromMock()
|
|
return
|
|
}
|
|
|
|
// Map API projects to frontend Content format.
|
|
// The backend returns projects with a nested `film` object (the Content entity).
|
|
// We need the content ID (film.id) for the rental/payment flow.
|
|
const allContent: Content[] = projects.map((p: any) => ({
|
|
id: p.id,
|
|
contentId: p.film?.id,
|
|
title: p.title,
|
|
description: p.synopsis || '',
|
|
thumbnail: p.poster || '',
|
|
backdrop: p.poster || '',
|
|
type: p.type || 'film',
|
|
slug: p.slug,
|
|
rentalPrice: p.film?.rentalPrice ?? p.rentalPrice,
|
|
status: p.status,
|
|
categories: p.genre ? [p.genre.name] : [],
|
|
streamingUrl: p.streamingUrl || p.partnerStreamUrl || undefined,
|
|
apiData: {
|
|
deliveryMode: p.deliveryMode,
|
|
partnerStreamUrl: p.partnerStreamUrl,
|
|
partnerDashUrl: p.partnerDashUrl,
|
|
partnerFairplayUrl: p.partnerFairplayUrl,
|
|
partnerDrmToken: p.partnerDrmToken,
|
|
},
|
|
}))
|
|
|
|
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
|
|
featuredContent.value = featuredFilm
|
|
|
|
const films = allContent.filter(c => c.type === 'film')
|
|
const bitcoinContent = allContent.filter(c =>
|
|
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
|
|
)
|
|
const docs = allContent.filter(c =>
|
|
c.categories?.some(cat => cat.toLowerCase().includes('documentary'))
|
|
)
|
|
const dramaContent = allContent.filter(c =>
|
|
c.categories?.some(cat => cat.toLowerCase().includes('drama'))
|
|
)
|
|
|
|
contentRows.value = {
|
|
featured: allContent.slice(0, 10),
|
|
newReleases: films.slice(0, 8),
|
|
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
|
|
documentaries: docs.length > 0 ? docs : allContent.slice(0, 10),
|
|
dramas: dramaContent.length > 0 ? dramaContent : films.slice(0, 6),
|
|
independent: films.slice(0, 10)
|
|
}
|
|
} catch (err) {
|
|
console.error('IndeeHub API fetch failed:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch IndeeHub mock content (original catalog)
|
|
*/
|
|
function fetchIndeeHubMock() {
|
|
const godBlessBitcoin = bitcoinFilms.find(f => f.title === 'God Bless Bitcoin') || bitcoinFilms[0]
|
|
if (godBlessBitcoin) {
|
|
featuredContent.value = {
|
|
...godBlessBitcoin,
|
|
backdrop: '/images/god-bless-bitcoin-backdrop.jpg'
|
|
}
|
|
} else {
|
|
featuredContent.value = indeeHubFilms[0]
|
|
}
|
|
|
|
contentRows.value = {
|
|
featured: indeeHubFilms.slice(0, 10),
|
|
newReleases: indeeHubFilms.slice(0, 8).reverse(),
|
|
bitcoin: bitcoinFilms,
|
|
documentaries: documentaries.slice(0, 10),
|
|
dramas: dramas.slice(0, 10),
|
|
independent: indeeHubFilms.filter(f =>
|
|
!f.categories.includes('Bitcoin') && !f.categories.includes('Documentary')
|
|
).slice(0, 10)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch TopDocumentaryFilms mock content
|
|
*/
|
|
function fetchTopDocMock() {
|
|
featuredContent.value = topDocFilms[0]
|
|
|
|
contentRows.value = {
|
|
featured: topDocFilms.slice(0, 10),
|
|
newReleases: [...topDocFilms].sort((a, b) => (b.releaseYear ?? 0) - (a.releaseYear ?? 0)).slice(0, 8),
|
|
bitcoin: topDocBitcoin,
|
|
documentaries: topDocEconomics.slice(0, 10),
|
|
dramas: topDocMoney,
|
|
independent: topDocCrypto.slice(0, 10)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch ALL published projects from the public API and merge them
|
|
* into the existing content rows so backstage content appears for
|
|
* every user, regardless of which content source is active.
|
|
*
|
|
* Ownership is determined by comparing the project's filmmaker ID
|
|
* to the current user's filmmaker profile. Only the actual creator
|
|
* sees the "my movie" flag — not other logged-in users.
|
|
*/
|
|
async function mergePublishedFilmmakerProjects() {
|
|
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
|
|
if (!apiUrl) return // No backend configured
|
|
|
|
try {
|
|
// Fetch ALL published projects from the public API endpoint.
|
|
// This works for any user (authenticated or not) — no auth required.
|
|
// Use fresh: true to bypass the backend's 5-minute cache
|
|
// so newly published backstage content appears immediately.
|
|
const response = await indeehubApiService.getProjects({ fresh: true })
|
|
const projects = Array.isArray(response) ? response : (response as any)?.data ?? []
|
|
if (!Array.isArray(projects) || projects.length === 0) return
|
|
|
|
// Determine the current user's filmmaker ID (if any) for
|
|
// ownership tagging. Import lazily to avoid circular deps.
|
|
let currentFilmmakerId: string | null = null
|
|
try {
|
|
const { useAuthStore } = await import('./auth')
|
|
const authStore = useAuthStore()
|
|
currentFilmmakerId = authStore.user?.filmmaker?.id ?? null
|
|
} catch {
|
|
// Auth store not ready — ownership will be unknown
|
|
}
|
|
|
|
const publishedContent: Content[] = projects.map((p: any) => {
|
|
const projectFilmmakerId = p.filmmaker?.id ?? p.filmmakerId ?? null
|
|
return {
|
|
id: p.id,
|
|
contentId: p.film?.id,
|
|
title: p.title || p.name,
|
|
description: p.synopsis || '',
|
|
thumbnail: p.poster || '/images/placeholder-poster.jpg',
|
|
backdrop: p.poster || '/images/placeholder-poster.jpg',
|
|
type: p.type === 'episodic' ? 'series' as const : 'film' as const,
|
|
rating: p.format || undefined,
|
|
releaseYear: p.releaseDate ? new Date(p.releaseDate).getFullYear() : new Date().getFullYear(),
|
|
categories: p.genres?.map((g: any) => g.name) || [],
|
|
slug: p.slug,
|
|
rentalPrice: p.film?.rentalPrice ?? p.rentalPrice,
|
|
status: p.status,
|
|
apiData: p,
|
|
// Only mark as "own project" if the current user is the actual filmmaker
|
|
isOwnProject: !!(currentFilmmakerId && projectFilmmakerId && currentFilmmakerId === projectFilmmakerId),
|
|
}
|
|
})
|
|
|
|
console.debug(
|
|
'[content-store] Merging API published projects:',
|
|
publishedContent.map(c => ({ id: c.id, title: c.title, isOwn: c.isOwnProject })),
|
|
)
|
|
|
|
// Merge into each content row (prepend so they appear first)
|
|
for (const key of Object.keys(contentRows.value)) {
|
|
const existingIds = new Set(contentRows.value[key].map(c => c.id))
|
|
const newItems = publishedContent.filter(c => !existingIds.has(c.id))
|
|
if (newItems.length > 0) {
|
|
contentRows.value[key] = [...newItems, ...contentRows.value[key]]
|
|
}
|
|
}
|
|
|
|
// If no featured content yet, use the first published project
|
|
if (!featuredContent.value && publishedContent.length > 0) {
|
|
featuredContent.value = publishedContent[0]
|
|
}
|
|
} catch (err) {
|
|
console.warn('[content-store] Failed to merge API published projects:', err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Route to the correct loader based on the active content source
|
|
*/
|
|
async function fetchContentFromMock() {
|
|
const sourceStore = useContentSourceStore()
|
|
if (sourceStore.activeSource === 'topdocfilms') {
|
|
fetchTopDocMock()
|
|
} else {
|
|
fetchIndeeHubMock()
|
|
}
|
|
|
|
// Also include any projects published through the backstage
|
|
await mergePublishedFilmmakerProjects()
|
|
}
|
|
|
|
/**
|
|
* Main fetch content method.
|
|
* Respects the content-source toggle:
|
|
* - 'indeehub-api' → self-hosted backend API
|
|
* - 'topdocfilms' → TopDoc mock catalog (YouTube documentaries)
|
|
* - 'indeehub' → IndeeHub mock catalog
|
|
*/
|
|
async function fetchContent() {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const sourceStore = useContentSourceStore()
|
|
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
|
|
|
|
if (USE_MOCK_DATA) {
|
|
await new Promise(resolve => setTimeout(resolve, 100))
|
|
await fetchContentFromMock()
|
|
} else if (sourceStore.activeSource === 'indeehub-api' && apiUrl) {
|
|
// Self-hosted backend API
|
|
await fetchContentFromIndeehubApi()
|
|
await mergePublishedFilmmakerProjects()
|
|
} else if (sourceStore.activeSource === 'topdocfilms') {
|
|
// TopDoc curated catalog (free YouTube documentaries)
|
|
fetchTopDocMock()
|
|
await mergePublishedFilmmakerProjects()
|
|
} else if (sourceStore.activeSource === 'indeehub') {
|
|
// IndeeHub mock catalog
|
|
fetchIndeeHubMock()
|
|
await mergePublishedFilmmakerProjects()
|
|
} else if (apiUrl) {
|
|
// Fallback to API if source is unknown but API is configured
|
|
await fetchContentFromIndeehubApi()
|
|
await mergePublishedFilmmakerProjects()
|
|
} else {
|
|
await fetchContentFromApi()
|
|
await mergePublishedFilmmakerProjects()
|
|
}
|
|
} catch (e: any) {
|
|
error.value = e.message || 'Failed to load content'
|
|
console.error('Content fetch error:', e)
|
|
|
|
// Fallback to mock data on error
|
|
console.log('Falling back to mock data...')
|
|
await fetchContentFromMock()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
featuredContent,
|
|
contentRows,
|
|
loading,
|
|
error,
|
|
fetchContent
|
|
}
|
|
})
|