126 lines
3.5 KiB
JavaScript
126 lines
3.5 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import content from 'virtual:content'
|
|
import detailLoaders from 'virtual:content-detail-loaders'
|
|
|
|
const detailCache = new Map()
|
|
|
|
/**
|
|
* Get all content of a given type, sorted by date descending.
|
|
* @param {'posts'|'kniepunkt'|'podcast'|'talks'|'consulting'|'resources'|'events'} type
|
|
* @param {object} options
|
|
* @param {string} options.tag - Filter by tag
|
|
* @param {'primary'|'secondary'|'all'} options.visibility - Filter by visibility (default: 'all')
|
|
* @param {number} options.page - Page number (1-based)
|
|
* @param {number} options.pageSize - Items per page (default: 10)
|
|
*/
|
|
export function useContent(type, options = {}) {
|
|
const { tag, visibility = 'all', page = 1, pageSize = 10 } = options
|
|
|
|
let items = content[type] || []
|
|
|
|
// Filter by visibility
|
|
if (visibility !== 'all') {
|
|
items = items.filter(item => (item.visibility || 'primary') === visibility)
|
|
}
|
|
|
|
// Filter by tag
|
|
if (tag) {
|
|
items = items.filter(item => item.tags && item.tags.includes(tag))
|
|
}
|
|
|
|
// Sort by date descending
|
|
items = [...items].sort((a, b) => {
|
|
const dateA = new Date(a.date || a.issue || 0)
|
|
const dateB = new Date(b.date || b.issue || 0)
|
|
return dateB - dateA
|
|
})
|
|
|
|
// Pagination
|
|
const total = items.length
|
|
const totalPages = Math.ceil(total / pageSize)
|
|
const paginated = items.slice((page - 1) * pageSize, page * pageSize)
|
|
|
|
return {
|
|
items: paginated,
|
|
total,
|
|
page,
|
|
totalPages,
|
|
hasNext: page < totalPages,
|
|
hasPrev: page > 1,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a single content item by slug.
|
|
*/
|
|
export function useContentItem(type, slug) {
|
|
const metadata = (content[type] || []).find(item => item.slug === slug) || null
|
|
const cacheKey = `${type}:${slug}`
|
|
const [detailState, setDetailState] = useState(() => ({
|
|
key: cacheKey,
|
|
item: detailCache.get(cacheKey) || metadata,
|
|
}))
|
|
|
|
useEffect(() => {
|
|
let active = true
|
|
if (!metadata || detailCache.has(cacheKey)) return () => { active = false }
|
|
|
|
const loadDetail = detailLoaders[type]?.[slug]
|
|
const detailPromise = loadDetail
|
|
? loadDetail()
|
|
: Promise.reject(new Error(`No detail loader for ${cacheKey}`))
|
|
|
|
detailPromise
|
|
.then(module => {
|
|
const detail = module.default
|
|
detailCache.set(cacheKey, detail)
|
|
if (active) setDetailState({ key: cacheKey, item: detail })
|
|
})
|
|
.catch(() => {
|
|
if (active) setDetailState({ key: cacheKey, item: { ...metadata, detailError: true } })
|
|
})
|
|
|
|
return () => { active = false }
|
|
}, [cacheKey, metadata, slug, type])
|
|
|
|
return detailState.key === cacheKey ? detailState.item : metadata
|
|
}
|
|
|
|
/**
|
|
* Get all unique tags across a content type.
|
|
*/
|
|
export function useContentTags(type) {
|
|
const items = content[type] || []
|
|
const tagSet = new Set()
|
|
items.forEach(item => {
|
|
if (item.tags) item.tags.forEach(t => tagSet.add(t))
|
|
})
|
|
return [...tagSet].slice(0, 20).sort()
|
|
}
|
|
|
|
/**
|
|
* Get featured/primary content for homepage highlights.
|
|
* Returns the latest N items with visibility=primary from each type.
|
|
*/
|
|
export function useFeaturedContent(count = 3) {
|
|
const types = ['posts', 'kniepunkt', 'podcast', 'talks', 'consulting']
|
|
const featured = {}
|
|
|
|
types.forEach(type => {
|
|
const items = (content[type] || [])
|
|
.filter(item => (item.visibility || 'primary') === 'primary')
|
|
.sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0))
|
|
.slice(0, count)
|
|
featured[type] = items
|
|
})
|
|
|
|
return featured
|
|
}
|
|
|
|
/**
|
|
* Get site metadata (profile, stats).
|
|
*/
|
|
export function useMeta() {
|
|
return content.meta || {}
|
|
}
|