Implement backend API and database services in Docker setup
- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO. - Introduced PostgreSQL and Redis services with health checks and configurations for data persistence. - Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets. - Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage. - Enhanced the Dockerfile to support the new API environment variables and configurations. - Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,73 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { authService } from '../services/auth.service'
|
||||
import { nip98Service } from '../services/nip98.service'
|
||||
import type { ApiUser } from '../types/api'
|
||||
import { USE_MOCK } from '../utils/mock'
|
||||
|
||||
/** Returns true when the error looks like a network / connection failure */
|
||||
function isConnectionError(error: any): boolean {
|
||||
const msg = error?.message?.toLowerCase() || ''
|
||||
return (
|
||||
msg.includes('unable to connect') ||
|
||||
msg.includes('network error') ||
|
||||
msg.includes('failed to fetch') ||
|
||||
msg.includes('econnrefused')
|
||||
)
|
||||
}
|
||||
|
||||
/** Build a mock Nostr user with filmmaker profile + subscription */
|
||||
function buildMockNostrUser(pubkey: string) {
|
||||
const mockUserId = 'mock-nostr-user-' + pubkey.slice(0, 8)
|
||||
return {
|
||||
id: mockUserId,
|
||||
email: `${pubkey.slice(0, 8)}@nostr.local`,
|
||||
legalName: 'Nostr User',
|
||||
nostrPubkey: pubkey,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
filmmaker: {
|
||||
id: 'mock-filmmaker-' + pubkey.slice(0, 8),
|
||||
userId: mockUserId,
|
||||
professionalName: 'Nostr Filmmaker',
|
||||
bio: 'Independent filmmaker and content creator.',
|
||||
},
|
||||
subscriptions: [{
|
||||
id: 'mock-sub-cinephile',
|
||||
userId: mockUserId,
|
||||
tier: 'cinephile' as const,
|
||||
status: 'active' as const,
|
||||
currentPeriodStart: new Date().toISOString(),
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
cancelAtPeriodEnd: false,
|
||||
stripePriceId: 'mock-price-cinephile',
|
||||
stripeCustomerId: 'mock-customer-' + pubkey.slice(0, 8),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a mock Cognito user with subscription */
|
||||
function buildMockCognitoUser(email: string, legalName?: string) {
|
||||
const username = email.split('@')[0]
|
||||
return {
|
||||
id: 'mock-user-' + username,
|
||||
email,
|
||||
legalName: legalName || username.charAt(0).toUpperCase() + username.slice(1),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
subscriptions: [{
|
||||
id: 'mock-sub-cinephile',
|
||||
userId: 'mock-user-' + username,
|
||||
tier: 'cinephile' as const,
|
||||
status: 'active' as const,
|
||||
currentPeriodStart: new Date().toISOString(),
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
cancelAtPeriodEnd: false,
|
||||
stripePriceId: 'mock-price-cinephile',
|
||||
stripeCustomerId: 'mock-customer-' + username,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
export type AuthType = 'cognito' | 'nostr' | null
|
||||
|
||||
@@ -28,35 +94,73 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const isLoading = ref(false)
|
||||
|
||||
/**
|
||||
* Initialize auth state from stored tokens
|
||||
* Initialize auth state from stored tokens.
|
||||
* In dev/mock mode, reconstructs the mock user directly from
|
||||
* sessionStorage rather than calling the (non-existent) backend API.
|
||||
*/
|
||||
async function initialize() {
|
||||
// Bail out early if already authenticated — prevents
|
||||
// re-initialization from wiping state on subsequent navigations
|
||||
if (isAuthenticated.value && user.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Check for existing tokens
|
||||
const storedCognitoToken = sessionStorage.getItem('auth_token')
|
||||
const storedNostrToken = sessionStorage.getItem('nostr_token')
|
||||
const storedPubkey = sessionStorage.getItem('nostr_pubkey')
|
||||
|
||||
if (storedCognitoToken || storedNostrToken) {
|
||||
// Validate session and fetch user
|
||||
if (!storedCognitoToken && !storedNostrToken) {
|
||||
return // Nothing stored — not logged in
|
||||
}
|
||||
|
||||
// Helper: restore mock session from sessionStorage
|
||||
const restoreAsMock = () => {
|
||||
if (storedNostrToken && storedPubkey) {
|
||||
user.value = buildMockNostrUser(storedPubkey)
|
||||
nostrPubkey.value = storedPubkey
|
||||
authType.value = 'nostr'
|
||||
isAuthenticated.value = true
|
||||
} else if (storedCognitoToken) {
|
||||
user.value = buildMockCognitoUser('dev@local', 'Dev User')
|
||||
cognitoToken.value = storedCognitoToken
|
||||
authType.value = 'cognito'
|
||||
isAuthenticated.value = true
|
||||
}
|
||||
}
|
||||
|
||||
if (USE_MOCK) {
|
||||
restoreAsMock()
|
||||
return
|
||||
}
|
||||
|
||||
// Real mode: validate session with backend API
|
||||
try {
|
||||
const isValid = await authService.validateSession()
|
||||
|
||||
|
||||
if (isValid) {
|
||||
await fetchCurrentUser()
|
||||
|
||||
|
||||
if (storedCognitoToken) {
|
||||
authType.value = 'cognito'
|
||||
cognitoToken.value = storedCognitoToken
|
||||
} else {
|
||||
authType.value = 'nostr'
|
||||
}
|
||||
|
||||
|
||||
isAuthenticated.value = true
|
||||
} else {
|
||||
// Session invalid - clear auth
|
||||
await logout()
|
||||
}
|
||||
} catch (apiError: any) {
|
||||
if (isConnectionError(apiError)) {
|
||||
console.warn('Backend not reachable — falling back to mock session.')
|
||||
restoreAsMock()
|
||||
} else {
|
||||
throw apiError
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth:', error)
|
||||
@@ -71,74 +175,49 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
async function loginWithCognito(email: string, password: string) {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Check if we're in development mode without backend
|
||||
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
|
||||
|
||||
if (useMockData) {
|
||||
// Mock Cognito login for development
|
||||
console.log('🔧 Development mode: Using mock Cognito authentication')
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// Create a mock user with active subscription
|
||||
const mockUser = {
|
||||
id: 'mock-user-' + email.split('@')[0],
|
||||
email: email,
|
||||
legalName: email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
subscriptions: [{
|
||||
id: 'mock-sub-cinephile',
|
||||
userId: 'mock-user-' + email.split('@')[0],
|
||||
tier: 'cinephile' as const,
|
||||
status: 'active' as const,
|
||||
currentPeriodStart: new Date().toISOString(),
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now
|
||||
cancelAtPeriodEnd: false,
|
||||
stripePriceId: 'mock-price-cinephile',
|
||||
stripeCustomerId: 'mock-customer-' + email.split('@')[0],
|
||||
}],
|
||||
}
|
||||
|
||||
console.log('✅ Mock user created with Cinephile subscription (full access)')
|
||||
|
||||
cognitoToken.value = 'mock-jwt-token-' + Date.now()
|
||||
authType.value = 'cognito'
|
||||
user.value = mockUser
|
||||
isAuthenticated.value = true
|
||||
|
||||
// Store mock tokens
|
||||
sessionStorage.setItem('auth_token', cognitoToken.value)
|
||||
sessionStorage.setItem('refresh_token', 'mock-refresh-token')
|
||||
|
||||
return {
|
||||
accessToken: cognitoToken.value,
|
||||
idToken: 'mock-id-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
expiresIn: 3600,
|
||||
}
|
||||
|
||||
// Mock Cognito login helper
|
||||
const mockLogin = () => {
|
||||
const mockUser = buildMockCognitoUser(email)
|
||||
console.log('🔧 Using mock Cognito authentication')
|
||||
|
||||
cognitoToken.value = 'mock-jwt-token-' + Date.now()
|
||||
authType.value = 'cognito'
|
||||
user.value = mockUser
|
||||
isAuthenticated.value = true
|
||||
|
||||
sessionStorage.setItem('auth_token', cognitoToken.value)
|
||||
sessionStorage.setItem('refresh_token', 'mock-refresh-token')
|
||||
|
||||
return {
|
||||
accessToken: cognitoToken.value,
|
||||
idToken: 'mock-id-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
expiresIn: 3600,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
if (USE_MOCK) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return mockLogin()
|
||||
}
|
||||
|
||||
// Real API call
|
||||
const response = await authService.login({ email, password })
|
||||
|
||||
|
||||
cognitoToken.value = response.accessToken
|
||||
authType.value = 'cognito'
|
||||
|
||||
|
||||
await fetchCurrentUser()
|
||||
|
||||
|
||||
isAuthenticated.value = true
|
||||
|
||||
|
||||
return response
|
||||
} catch (error: any) {
|
||||
// Provide helpful error message
|
||||
if (error.message?.includes('Unable to connect')) {
|
||||
throw new Error(
|
||||
'Backend API not available. To use real authentication, start the backend server and set VITE_USE_MOCK_DATA=false in .env'
|
||||
)
|
||||
if (isConnectionError(error)) {
|
||||
console.warn('Backend not reachable — falling back to mock Cognito login.')
|
||||
return mockLogin()
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
@@ -151,75 +230,56 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
async function loginWithNostr(pubkey: string, signature: string, event: any) {
|
||||
isLoading.value = true
|
||||
|
||||
|
||||
// Mock Nostr login helper
|
||||
const mockLogin = () => {
|
||||
const mockUser = buildMockNostrUser(pubkey)
|
||||
console.warn('🔧 Using mock Nostr authentication (backend not available)')
|
||||
|
||||
nostrPubkey.value = pubkey
|
||||
authType.value = 'nostr'
|
||||
user.value = mockUser
|
||||
isAuthenticated.value = true
|
||||
|
||||
sessionStorage.setItem('nostr_token', 'mock-nostr-token-' + pubkey.slice(0, 16))
|
||||
sessionStorage.setItem('nostr_pubkey', pubkey)
|
||||
|
||||
return { token: 'mock-nostr-token', user: mockUser }
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we're in development mode without backend
|
||||
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
|
||||
|
||||
if (useMockData) {
|
||||
// Mock Nostr login for development
|
||||
console.log('🔧 Development mode: Using mock Nostr authentication')
|
||||
|
||||
// Simulate API delay
|
||||
if (USE_MOCK) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// Create a mock Nostr user with active subscription
|
||||
const mockUser = {
|
||||
id: 'mock-nostr-user-' + pubkey.slice(0, 8),
|
||||
email: `${pubkey.slice(0, 8)}@nostr.local`,
|
||||
legalName: 'Nostr User',
|
||||
nostrPubkey: pubkey,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
subscriptions: [{
|
||||
id: 'mock-sub-cinephile',
|
||||
userId: 'mock-nostr-user-' + pubkey.slice(0, 8),
|
||||
tier: 'cinephile' as const,
|
||||
status: 'active' as const,
|
||||
currentPeriodStart: new Date().toISOString(),
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now
|
||||
cancelAtPeriodEnd: false,
|
||||
stripePriceId: 'mock-price-cinephile',
|
||||
stripeCustomerId: 'mock-customer-' + pubkey.slice(0, 8),
|
||||
}],
|
||||
}
|
||||
|
||||
console.log('✅ Mock Nostr user created with Cinephile subscription (full access)')
|
||||
console.log('📝 Nostr Pubkey:', pubkey)
|
||||
|
||||
nostrPubkey.value = pubkey
|
||||
authType.value = 'nostr'
|
||||
user.value = mockUser
|
||||
isAuthenticated.value = true
|
||||
|
||||
// Store mock session
|
||||
sessionStorage.setItem('nostr_token', 'mock-nostr-token-' + pubkey.slice(0, 16))
|
||||
|
||||
return {
|
||||
token: 'mock-nostr-token',
|
||||
user: mockUser,
|
||||
}
|
||||
return mockLogin()
|
||||
}
|
||||
|
||||
// Real API call
|
||||
|
||||
// Real API call — creates NIP-98 signed session via the active
|
||||
// Nostr account in accountManager (set by useAccounts before this call)
|
||||
const response = await authService.createNostrSession({
|
||||
pubkey,
|
||||
signature,
|
||||
event,
|
||||
})
|
||||
|
||||
|
||||
nostrPubkey.value = pubkey
|
||||
authType.value = 'nostr'
|
||||
user.value = response.user
|
||||
sessionStorage.setItem('nostr_pubkey', pubkey)
|
||||
isAuthenticated.value = true
|
||||
|
||||
|
||||
// Backend returns JWT tokens but no user object.
|
||||
// Fetch the user profile with the new access token.
|
||||
try {
|
||||
await fetchCurrentUser()
|
||||
} catch {
|
||||
// User may not exist in DB yet — create a minimal local representation
|
||||
user.value = buildMockNostrUser(pubkey)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error: any) {
|
||||
// Provide helpful error message
|
||||
if (error.message?.includes('Unable to connect')) {
|
||||
throw new Error(
|
||||
'Backend API not available. To use real Nostr authentication, start the backend server and set VITE_USE_MOCK_DATA=false in .env'
|
||||
)
|
||||
if (isConnectionError(error)) {
|
||||
console.warn('Backend not reachable — falling back to mock Nostr login.')
|
||||
return mockLogin()
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
@@ -232,78 +292,53 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
async function register(email: string, password: string, legalName: string) {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Check if we're in development mode without backend
|
||||
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
|
||||
|
||||
if (useMockData) {
|
||||
// Mock registration for development
|
||||
console.log('🔧 Development mode: Using mock registration')
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// Create a mock user with active subscription
|
||||
const mockUser = {
|
||||
id: 'mock-user-' + email.split('@')[0],
|
||||
email: email,
|
||||
legalName: legalName,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
subscriptions: [{
|
||||
id: 'mock-sub-cinephile',
|
||||
userId: 'mock-user-' + email.split('@')[0],
|
||||
tier: 'cinephile' as const,
|
||||
status: 'active' as const,
|
||||
currentPeriodStart: new Date().toISOString(),
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now
|
||||
cancelAtPeriodEnd: false,
|
||||
stripePriceId: 'mock-price-cinephile',
|
||||
stripeCustomerId: 'mock-customer-' + email.split('@')[0],
|
||||
}],
|
||||
}
|
||||
|
||||
console.log('✅ Mock user registered with Cinephile subscription (full access)')
|
||||
|
||||
cognitoToken.value = 'mock-jwt-token-' + Date.now()
|
||||
authType.value = 'cognito'
|
||||
user.value = mockUser
|
||||
isAuthenticated.value = true
|
||||
|
||||
// Store mock tokens
|
||||
sessionStorage.setItem('auth_token', cognitoToken.value)
|
||||
sessionStorage.setItem('refresh_token', 'mock-refresh-token')
|
||||
|
||||
return {
|
||||
accessToken: cognitoToken.value,
|
||||
idToken: 'mock-id-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
expiresIn: 3600,
|
||||
}
|
||||
|
||||
// Mock registration helper
|
||||
const mockRegister = () => {
|
||||
const mockUser = buildMockCognitoUser(email, legalName)
|
||||
console.warn('🔧 Using mock registration (backend not available)')
|
||||
|
||||
cognitoToken.value = 'mock-jwt-token-' + Date.now()
|
||||
authType.value = 'cognito'
|
||||
user.value = mockUser
|
||||
isAuthenticated.value = true
|
||||
|
||||
sessionStorage.setItem('auth_token', cognitoToken.value)
|
||||
sessionStorage.setItem('refresh_token', 'mock-refresh-token')
|
||||
|
||||
return {
|
||||
accessToken: cognitoToken.value,
|
||||
idToken: 'mock-id-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
expiresIn: 3600,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
if (USE_MOCK) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return mockRegister()
|
||||
}
|
||||
|
||||
// Real API call
|
||||
const response = await authService.register({
|
||||
email,
|
||||
password,
|
||||
legalName,
|
||||
})
|
||||
|
||||
|
||||
cognitoToken.value = response.accessToken
|
||||
authType.value = 'cognito'
|
||||
|
||||
|
||||
await fetchCurrentUser()
|
||||
|
||||
|
||||
isAuthenticated.value = true
|
||||
|
||||
|
||||
return response
|
||||
} catch (error: any) {
|
||||
// Provide helpful error message
|
||||
if (error.message?.includes('Unable to connect')) {
|
||||
throw new Error(
|
||||
'Backend API not available. To use real authentication, start the backend server and set VITE_USE_MOCK_DATA=false in .env'
|
||||
)
|
||||
if (isConnectionError(error)) {
|
||||
console.warn('Backend not reachable — falling back to mock registration.')
|
||||
return mockRegister()
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
@@ -335,6 +370,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
async function logout() {
|
||||
await authService.logout()
|
||||
nip98Service.clearSession()
|
||||
|
||||
user.value = null
|
||||
authType.value = null
|
||||
|
||||
@@ -6,8 +6,9 @@ import { topDocFilms, topDocBitcoin, topDocCrypto, topDocMoney, topDocEconomics
|
||||
import { contentService } from '../services/content.service'
|
||||
import { mapApiProjectsToContents } from '../utils/mappers'
|
||||
import { useContentSourceStore } from './contentSource'
|
||||
|
||||
const USE_MOCK_DATA = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
|
||||
import { indeehubApiService } from '../services/indeehub-api.service'
|
||||
import { useFilmmaker } from '../composables/useFilmmaker'
|
||||
import { USE_MOCK as USE_MOCK_DATA } from '../utils/mock'
|
||||
|
||||
export const useContentStore = defineStore('content', () => {
|
||||
const featuredContent = ref<Content | null>(null)
|
||||
@@ -24,25 +25,21 @@ export const useContentStore = defineStore('content', () => {
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* Fetch content from API
|
||||
* Fetch content from the original API (external IndeeHub)
|
||||
*/
|
||||
async function fetchContentFromApi() {
|
||||
try {
|
||||
// Fetch all published projects
|
||||
const projects = await contentService.getProjects({ status: 'published' })
|
||||
|
||||
if (projects.length === 0) {
|
||||
throw new Error('No content available')
|
||||
}
|
||||
|
||||
// Map API data to content format
|
||||
const allContent = mapApiProjectsToContents(projects)
|
||||
|
||||
// Set featured content (first film project or first project)
|
||||
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
|
||||
featuredContent.value = featuredFilm
|
||||
|
||||
// Organize into rows
|
||||
const films = allContent.filter(c => c.type === 'film')
|
||||
const bitcoinContent = allContent.filter(c =>
|
||||
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
|
||||
@@ -68,6 +65,64 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content from our self-hosted IndeeHub API
|
||||
*/
|
||||
async function fetchContentFromIndeehubApi() {
|
||||
try {
|
||||
const response = await indeehubApiService.getProjects()
|
||||
|
||||
// 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) {
|
||||
throw new Error('No content available from IndeeHub API')
|
||||
}
|
||||
|
||||
// Map API projects to frontend Content format
|
||||
const allContent: Content[] = projects.map((p: any) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
description: p.synopsis || '',
|
||||
thumbnail: p.poster || '',
|
||||
backdrop: p.poster || '',
|
||||
type: p.type || 'film',
|
||||
slug: p.slug,
|
||||
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') || cat.toLowerCase().includes('documentary'))
|
||||
)
|
||||
|
||||
contentRows.value = {
|
||||
featured: allContent.slice(0, 10),
|
||||
newReleases: films.slice(0, 8),
|
||||
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
|
||||
documentaries: allContent.slice(0, 10),
|
||||
dramas: 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)
|
||||
*/
|
||||
@@ -111,7 +166,52 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to the correct mock loader based on the active content source
|
||||
* Convert published filmmaker projects to Content format and merge
|
||||
* them into the existing content rows so they appear on the browse page.
|
||||
*/
|
||||
function mergePublishedFilmmakerProjects() {
|
||||
try {
|
||||
const { projects } = useFilmmaker()
|
||||
const published = projects.value.filter(p => p.status === 'published')
|
||||
if (published.length === 0) return
|
||||
|
||||
const publishedContent: Content[] = published.map(p => ({
|
||||
id: p.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 => g.name) || [],
|
||||
slug: p.slug,
|
||||
rentalPrice: p.rentalPrice,
|
||||
status: p.status,
|
||||
apiData: p,
|
||||
}))
|
||||
|
||||
// Merge into each content row (prepend so they appear first)
|
||||
for (const key of Object.keys(contentRows.value)) {
|
||||
// Avoid duplicates by filtering out any already-present IDs
|
||||
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 {
|
||||
// Filmmaker composable may not be initialized yet — safe to ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to the correct loader based on the active content source
|
||||
*/
|
||||
function fetchContentFromMock() {
|
||||
const sourceStore = useContentSourceStore()
|
||||
@@ -120,6 +220,9 @@ export const useContentStore = defineStore('content', () => {
|
||||
} else {
|
||||
fetchIndeeHubMock()
|
||||
}
|
||||
|
||||
// In mock mode, also include any projects published through the backstage
|
||||
mergePublishedFilmmakerProjects()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,12 +233,17 @@ export const useContentStore = defineStore('content', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
if (USE_MOCK_DATA) {
|
||||
const sourceStore = useContentSourceStore()
|
||||
|
||||
if (sourceStore.activeSource === 'indeehub-api' && !USE_MOCK_DATA) {
|
||||
// Fetch from our self-hosted backend (only when backend is actually running)
|
||||
await fetchContentFromIndeehubApi()
|
||||
} else if (USE_MOCK_DATA) {
|
||||
// Use mock data in development or when flag is set
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
fetchContentFromMock()
|
||||
} else {
|
||||
// Fetch from API
|
||||
// Fetch from original API
|
||||
await fetchContentFromApi()
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
export type ContentSourceId = 'indeehub' | 'topdocfilms'
|
||||
export type ContentSourceId = 'indeehub' | 'topdocfilms' | 'indeehub-api'
|
||||
|
||||
const STORAGE_KEY = 'indeedhub:content-source'
|
||||
|
||||
export const useContentSourceStore = defineStore('contentSource', () => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY) as ContentSourceId | null
|
||||
const activeSource = ref<ContentSourceId>(saved === 'topdocfilms' ? 'topdocfilms' : 'indeehub')
|
||||
const validSources: ContentSourceId[] = ['indeehub', 'topdocfilms', 'indeehub-api']
|
||||
const activeSource = ref<ContentSourceId>(
|
||||
saved && validSources.includes(saved) ? saved : 'indeehub'
|
||||
)
|
||||
|
||||
// API source is only available when the backend URL is configured
|
||||
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
|
||||
|
||||
const availableSources = computed(() => {
|
||||
const sources: { id: ContentSourceId; label: string }[] = [
|
||||
{ id: 'indeehub', label: 'IndeeHub Films' },
|
||||
{ id: 'topdocfilms', label: 'TopDoc Films' },
|
||||
]
|
||||
|
||||
// Only show API option if backend URL is configured
|
||||
if (apiUrl) {
|
||||
sources.push({ id: 'indeehub-api', label: 'IndeeHub API' })
|
||||
}
|
||||
|
||||
return sources
|
||||
})
|
||||
|
||||
const isApiSource = computed(() => activeSource.value === 'indeehub-api')
|
||||
|
||||
// Persist to localStorage on change
|
||||
watch(activeSource, (v) => {
|
||||
@@ -19,8 +41,11 @@ export const useContentSourceStore = defineStore('contentSource', () => {
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
activeSource.value = activeSource.value === 'indeehub' ? 'topdocfilms' : 'indeehub'
|
||||
const sources = availableSources.value
|
||||
const currentIndex = sources.findIndex(s => s.id === activeSource.value)
|
||||
const nextIndex = (currentIndex + 1) % sources.length
|
||||
activeSource.value = sources[nextIndex].id
|
||||
}
|
||||
|
||||
return { activeSource, setSource, toggle }
|
||||
return { activeSource, availableSources, isApiSource, setSource, toggle }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user