import { useCallback, useState } from 'react'
export interface Site {
did: string
rkey: string
display_name: string | null
created_at: number
updated_at: number
}
export interface DomainInfo {
type: 'wisp' | 'custom'
domain: string
verified?: boolean
id?: string
}
export interface SiteWithDomains extends Site {
domains?: DomainInfo[]
isPrivate?: boolean
siteId?: string
expiresAt?: string | null
expired?: boolean
shareCount?: number
privateUrl?: string
fileCount?: number
totalBytes?: number
}
export interface PrivateShare {
shareId: string
tokenPrefix: string
label: string | null
audienceDid: string | null
expiresAt: string | null
revokedAt: string | null
createdAt: string
lastUsedAt: string | null
status: 'active' | 'expired' | 'revoked'
}
export function useSiteData() {
const [sites, setSites] = useState<SiteWithDomains[]>([])
const [sitesLoading, setSitesLoading] = useState(true)
const [isSyncing, setIsSyncing] = useState(false)
const fetchSites = useCallback(async () => {
try {
const [response, privateResponse] = await Promise.all([
fetch('/api/user/sites'),
fetch('/api/user/private-sites').catch(() => null),
])
const data = await response.json()
// /api/user/sites already returns each site's domains. It used to be one
// extra request per site, and every one of those had to re-establish the
// caller's session before it could answer.
const sitesWithDomains: SiteWithDomains[] = (data.sites || []).map((site: SiteWithDomains) => ({
...site,
domains: site.domains || [],
}))
let privateSites: SiteWithDomains[] = []
if (privateResponse?.ok) {
try {
const privateData = await privateResponse.json()
privateSites = (privateData.sites || []).map(
(p: {
siteId: string
name: string
fileCount: number
totalBytes: number
expiresAt: string | null
createdAt: string
expired: boolean
shareCount: number
url: string
}): SiteWithDomains => ({
did: '',
rkey: p.siteId,
display_name: p.name,
created_at: Math.floor(new Date(p.createdAt).getTime() / 1000),
updated_at: Math.floor(new Date(p.createdAt).getTime() / 1000),
domains: [],
isPrivate: true,
siteId: p.siteId,
expiresAt: p.expiresAt,
expired: p.expired,
shareCount: p.shareCount,
privateUrl: p.url,
fileCount: p.fileCount,
totalBytes: p.totalBytes,
}),
)
} catch (err) {
console.error('Failed to parse private sites:', err)
}
}
setSites([...sitesWithDomains, ...privateSites].sort((a, b) => b.created_at - a.created_at))
} catch (err) {
console.error('Failed to fetch sites:', err)
} finally {
setSitesLoading(false)
}
}, [])
const syncSites = useCallback(async () => {
setIsSyncing(true)
try {
const response = await fetch('/api/user/sync', {
method: 'POST',
})
const data = await response.json()
if (data.success) {
console.log(`Synced ${data.synced} sites from PDS`)
// Refresh sites list
await fetchSites()
}
} catch (err) {
console.error('Failed to sync sites:', err)
alert('Failed to sync sites from PDS')
} finally {
setIsSyncing(false)
}
}, [fetchSites])
const deleteSite = useCallback(
async (rkey: string, isPrivate = false) => {
try {
const response = await fetch(isPrivate ? `/api/user/private-sites/${rkey}` : `/api/site/${rkey}`, {
method: 'DELETE',
})
const data = await response.json()
if (data.success) {
// Refresh sites list
await fetchSites()
return true
} else {
throw new Error(data.error || 'Failed to delete site')
}
} catch (err) {
console.error('Delete site error:', err)
alert(`Failed to delete site: ${err instanceof Error ? err.message : 'Unknown error'}`)
return false
}
},
[fetchSites],
)
return {
sites,
sitesLoading,
isSyncing,
fetchSites,
syncSites,
deleteSite,
}
}