feat(privat/CV): sync to latest upstream (cv-upstream/main, 30f9608b)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import content from 'virtual:content'
|
||||
|
||||
/**
|
||||
* 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 items = content[type] || []
|
||||
return items.find(item => item.slug === slug) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || {}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Hook for form submission with validation and loading state.
|
||||
* Automatically includes anti-spam fields (honeypot + form-render timestamp).
|
||||
* @param {string} endpoint - API endpoint (e.g., '/api/contact')
|
||||
* @param {function} validate - Validation function, returns { valid, errors }
|
||||
*/
|
||||
export function useFormSubmit(endpoint, validate) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [errors, setErrors] = useState({})
|
||||
const [serverError, setServerError] = useState(null)
|
||||
// Timestamp when the form mounted — used as a server-side time trap.
|
||||
const formStartedAt = useRef(0)
|
||||
useEffect(() => {
|
||||
formStartedAt.current = Date.now()
|
||||
}, [])
|
||||
|
||||
async function submit(data) {
|
||||
setServerError(null)
|
||||
|
||||
// Client-side validation
|
||||
if (validate) {
|
||||
const result = validate(data)
|
||||
if (!result.valid) {
|
||||
setErrors(result.errors)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
setErrors({})
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...data, form_started_at: formStartedAt.current }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}))
|
||||
if (body.errors) {
|
||||
setErrors(body.errors)
|
||||
} else {
|
||||
setServerError(body.error || 'Ein Fehler ist aufgetreten.')
|
||||
}
|
||||
setLoading(false)
|
||||
return false
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
setLoading(false)
|
||||
return true
|
||||
} catch {
|
||||
setServerError('Verbindungsfehler. Bitte versuche es erneut.')
|
||||
setLoading(false)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setLoading(false)
|
||||
setSuccess(false)
|
||||
setErrors({})
|
||||
setServerError(null)
|
||||
}
|
||||
|
||||
return { submit, loading, success, errors, serverError, reset }
|
||||
}
|
||||
Reference in New Issue
Block a user