import { useCallback, useEffect, useState } from 'react'; import { apiFetch } from '../lib/api'; export type DomainStatus = 'active' | 'submitted' | 'approved' | 'rejected'; export type CustomDomain = { id: string; domain: string; status: DomainStatus; addedAt?: string; postId?: string | null; submission?: { id: string; yesVotes: number; noVotes: number; status: string } | null; }; export type Plan = 'free' | 'pro' | 'legend'; export type Tier = { plan: Plan; domainLimit: number; // free=5, pro=5, legend=10 refillEnabled: boolean; // free=false, pro/legend=true globalBlocklist: boolean; // free=false, pro/legend=true canSubmit: boolean; // free=false, pro/legend=true usedSlots: number; // active+submitted (NICHT approved/rejected) atLimit: boolean; }; function deriveTier(plan: Plan, domains: CustomDomain[]): Tier { const limit = plan === 'legend' ? 10 : 5; const refill = plan !== 'free'; const usedSlots = domains.filter((d) => d.status === 'active' || d.status === 'submitted').length; return { plan, domainLimit: limit, refillEnabled: refill, globalBlocklist: refill, canSubmit: refill, usedSlots, atLimit: usedSlots >= limit, }; } export type UseCustomDomainsReturn = { domains: CustomDomain[]; tier: Tier; loading: boolean; error: string | null; refresh: () => Promise; addDomain: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; submitDomain: (id: string) => Promise<{ ok: boolean; error?: string }>; removeDomain: (id: string) => Promise<{ ok: boolean; error?: string }>; /** Live-Validate (regex) ob string gültiger Domain-Name ist. */ isValidDomain: (s: string) => boolean; /** Normalize: lowercase, http(s)://, /path stripping, www. weg. */ normalizeDomain: (s: string) => string; }; const DOMAIN_REGEX = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i; export function normalizeDomain(input: string): string { let s = input.trim().toLowerCase(); if (s.startsWith('https://')) s = s.slice(8); else if (s.startsWith('http://')) s = s.slice(7); const slash = s.indexOf('/'); if (slash >= 0) s = s.slice(0, slash); if (s.startsWith('www.')) s = s.slice(4); return s; } export function isValidDomain(input: string): boolean { const n = normalizeDomain(input); if (!n || n.length > 253) return false; return DOMAIN_REGEX.test(n); } /** * Custom-Domain CRUD gegen `/api/custom-domains/*` mit Tier-aware Limits. * * Tier-Logik (Single-Source-of-Truth: User.plan): * Free → 5 Slots, kein Refill, keine Submit * Pro → 5 Slots, Refill bei approved/rejected, Submit erlaubt * Legend → 10 Slots, Refill, Submit */ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { const [domains, setDomains] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchDomains = useCallback(async () => { try { // Backend (`server/api/custom-domains/index.get.ts`) gibt Array DIREKT zurück, // kein { domains: [...] }-Wrapper. const res = await apiFetch( '/api/custom-domains', ); const arr = Array.isArray(res) ? res : (res?.domains ?? []); console.log('[useCustomDomains] fetched:', arr.length, 'domains', arr.slice(0, 3)); setDomains(arr); setError(null); } catch (e: any) { console.error('[useCustomDomains] fetch failed:', e?.message ?? e); setError(e?.message ?? 'unknown'); } finally { setLoading(false); } }, []); useEffect(() => { fetchDomains(); }, [fetchDomains]); const addDomain = useCallback( async (input: string) => { if (!isValidDomain(input)) return { ok: false, error: 'invalid_domain' }; const tier = deriveTier(plan, domains); if (tier.atLimit) return { ok: false, error: 'limit_reached' }; const normalized = normalizeDomain(input); try { // Backend könnte einen `alreadyGlobal`-Flag setzen wenn die Domain // bereits in der globalen Blocklist ist (Slot wird nicht verbraucht). const res = await apiFetch('/api/custom-domains', { method: 'POST', body: { domain: normalized }, }); if (res?.alreadyGlobal) { return { ok: false, alreadyGlobal: true }; } await fetchDomains(); return { ok: true }; } catch (e: any) { return { ok: false, error: e?.message ?? 'add_failed' }; } }, [plan, domains, fetchDomains], ); const submitDomain = useCallback( async (id: string) => { const tier = deriveTier(plan, domains); if (!tier.canSubmit) return { ok: false, error: 'plan_does_not_support_submit' }; try { await apiFetch(`/api/custom-domains/${id}/submit`, { method: 'POST', body: {} }); await fetchDomains(); return { ok: true }; } catch (e: any) { return { ok: false, error: e?.message ?? 'submit_failed' }; } }, [plan, domains, fetchDomains], ); const removeDomain = useCallback( async (id: string) => { try { await apiFetch(`/api/custom-domains/${id}`, { method: 'DELETE' }); await fetchDomains(); return { ok: true }; } catch (e: any) { return { ok: false, error: e?.message ?? 'remove_failed' }; } }, [fetchDomains], ); const tier = deriveTier(plan, domains); return { domains, tier, loading, error, refresh: fetchDomains, addDomain, submitDomain, removeDomain, isValidDomain, normalizeDomain, }; }