diff --git a/bun.lock b/bun.lock index 5c5dd02..87c990b 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "@elysiajs/openapi": "^1.4.11", "@elysiajs/opentelemetry": "^1.4.6", "@elysiajs/static": "^1.4.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-radio-group": "^1.3.8", @@ -232,6 +233,8 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], diff --git a/package.json b/package.json index 123b81a..f8c07bb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@elysiajs/openapi": "^1.4.11", "@elysiajs/opentelemetry": "^1.4.6", "@elysiajs/static": "^1.4.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-radio-group": "^1.3.8", diff --git a/public/components/ui/checkbox.tsx b/public/components/ui/checkbox.tsx new file mode 100644 index 0000000..5e4b674 --- /dev/null +++ b/public/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@public/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/public/editor/editor.tsx b/public/editor/editor.tsx index a11661e..da7530f 100644 --- a/public/editor/editor.tsx +++ b/public/editor/editor.tsx @@ -38,6 +38,7 @@ import { Settings } from 'lucide-react' import { RadioGroup, RadioGroupItem } from '@public/components/ui/radio-group' +import { Checkbox } from '@public/components/ui/checkbox' import { CodeBlock } from '@public/components/ui/code-block' import Layout from '@public/layouts' @@ -55,6 +56,17 @@ interface Site { updated_at: number } +interface DomainInfo { + type: 'wisp' | 'custom' + domain: string + verified?: boolean + id?: string +} + +interface SiteWithDomains extends Site { + domains?: DomainInfo[] +} + interface CustomDomain { id: string domain: string @@ -76,7 +88,7 @@ function Dashboard() { const [loading, setLoading] = useState(true) // Sites state - const [sites, setSites] = useState([]) + const [sites, setSites] = useState([]) const [sitesLoading, setSitesLoading] = useState(true) const [isSyncing, setIsSyncing] = useState(false) @@ -86,8 +98,8 @@ function Dashboard() { const [domainsLoading, setDomainsLoading] = useState(true) // Site configuration state - const [configuringSite, setConfiguringSite] = useState(null) - const [selectedDomain, setSelectedDomain] = useState('') + const [configuringSite, setConfiguringSite] = useState(null) + const [selectedDomains, setSelectedDomains] = useState>(new Set()) const [isSavingConfig, setIsSavingConfig] = useState(false) const [isDeletingSite, setIsDeletingSite] = useState(false) @@ -148,7 +160,29 @@ function Dashboard() { try { const response = await fetch('/api/user/sites') const data = await response.json() - setSites(data.sites || []) + const sitesData: Site[] = data.sites || [] + + // Fetch domain info for each site + const sitesWithDomains = await Promise.all( + sitesData.map(async (site) => { + try { + const domainsResponse = await fetch(`/api/user/site/${site.rkey}/domains`) + const domainsData = await domainsResponse.json() + return { + ...site, + domains: domainsData.domains || [] + } + } catch (err) { + console.error(`Failed to fetch domains for site ${site.rkey}:`, err) + return { + ...site, + domains: [] + } + } + }) + ) + + setSites(sitesWithDomains) } catch (err) { console.error('Failed to fetch sites:', err) } finally { @@ -189,34 +223,26 @@ function Dashboard() { } } - const getSiteUrl = (site: Site) => { - // Check if this site is mapped to the wisp.place domain - if (wispDomain && wispDomain.rkey === site.rkey) { - return `https://${wispDomain.domain}` - } - - // Check if this site is mapped to any custom domain - const customDomain = customDomains.find((d) => d.rkey === site.rkey) - if (customDomain) { - return `https://${customDomain.domain}` + const getSiteUrl = (site: SiteWithDomains) => { + // Use the first mapped domain if available + if (site.domains && site.domains.length > 0) { + return `https://${site.domains[0].domain}` } - // Default fallback URL + // Default fallback URL - use handle instead of DID if (!userInfo) return '#' - return `https://sites.wisp.place/${site.did}/${site.rkey}` + return `https://sites.wisp.place/${userInfo.handle}/${site.rkey}` } - const getSiteDomainName = (site: Site) => { - if (wispDomain && wispDomain.rkey === site.rkey) { - return wispDomain.domain - } - - const customDomain = customDomains.find((d) => d.rkey === site.rkey) - if (customDomain) { - return customDomain.domain + const getSiteDomainName = (site: SiteWithDomains) => { + // Return the first domain if available + if (site.domains && site.domains.length > 0) { + return site.domains[0].domain } - return `sites.wisp.place/${site.did}/${site.rkey}` + // Use handle instead of DID for display + if (!userInfo) return `sites.wisp.place/.../${site.rkey}` + return `sites.wisp.place/${userInfo.handle}/${site.rkey}` } const handleFileSelect = (e: React.ChangeEvent) => { @@ -373,20 +399,23 @@ function Dashboard() { } } - const handleConfigureSite = (site: Site) => { + const handleConfigureSite = (site: SiteWithDomains) => { setConfiguringSite(site) - // Determine current domain mapping - if (wispDomain && wispDomain.rkey === site.rkey) { - setSelectedDomain('wisp') - } else { - const customDomain = customDomains.find((d) => d.rkey === site.rkey) - if (customDomain) { - setSelectedDomain(customDomain.id) - } else { - setSelectedDomain('none') - } + // Build set of currently mapped domains + const mappedDomains = new Set() + + if (site.domains) { + site.domains.forEach(domainInfo => { + if (domainInfo.type === 'wisp') { + mappedDomains.add('wisp') + } else if (domainInfo.id) { + mappedDomains.add(domainInfo.id) + } + }) } + + setSelectedDomains(mappedDomains) } const handleSaveSiteConfig = async () => { @@ -394,53 +423,63 @@ function Dashboard() { setIsSavingConfig(true) try { - if (selectedDomain === 'wisp') { - // Map to wisp.place domain + // Determine which domains should be mapped/unmapped + const shouldMapWisp = selectedDomains.has('wisp') + const isCurrentlyMappedToWisp = wispDomain && wispDomain.rkey === configuringSite.rkey + + // Handle wisp domain mapping + if (shouldMapWisp && !isCurrentlyMappedToWisp) { + // Map to wisp domain const response = await fetch('/api/domain/wisp/map-site', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ siteRkey: configuringSite.rkey }) }) const data = await response.json() - if (!data.success) throw new Error('Failed to map site') - } else if (selectedDomain === 'none') { - // Unmap from all domains - // Unmap wisp domain if this site was mapped to it - if (wispDomain && wispDomain.rkey === configuringSite.rkey) { - await fetch('/api/domain/wisp/map-site', { + if (!data.success) throw new Error('Failed to map wisp domain') + } else if (!shouldMapWisp && isCurrentlyMappedToWisp) { + // Unmap from wisp domain + await fetch('/api/domain/wisp/map-site', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ siteRkey: null }) + }) + } + + // Handle custom domain mappings + const selectedCustomDomainIds = Array.from(selectedDomains).filter(id => id !== 'wisp') + const currentlyMappedCustomDomains = customDomains.filter( + d => d.rkey === configuringSite.rkey + ) + + // Unmap domains that are no longer selected + for (const domain of currentlyMappedCustomDomains) { + if (!selectedCustomDomainIds.includes(domain.id)) { + await fetch(`/api/domain/custom/${domain.id}/map-site`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ siteRkey: null }) }) } + } - // Unmap from custom domains - const mappedCustom = customDomains.find( - (d) => d.rkey === configuringSite.rkey - ) - if (mappedCustom) { - await fetch(`/api/domain/custom/${mappedCustom.id}/map-site`, { + // Map newly selected domains + for (const domainId of selectedCustomDomainIds) { + const isAlreadyMapped = currentlyMappedCustomDomains.some(d => d.id === domainId) + if (!isAlreadyMapped) { + const response = await fetch(`/api/domain/custom/${domainId}/map-site`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ siteRkey: null }) + body: JSON.stringify({ siteRkey: configuringSite.rkey }) }) + const data = await response.json() + if (!data.success) throw new Error(`Failed to map custom domain ${domainId}`) } - } else { - // Map to a custom domain - const response = await fetch( - `/api/domain/custom/${selectedDomain}/map-site`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ siteRkey: configuringSite.rkey }) - } - ) - const data = await response.json() - if (!data.success) throw new Error('Failed to map site') } - // Refresh domains to get updated mappings + // Refresh both domains and sites to get updated mappings await fetchDomains() + await fetchSites() setConfiguringSite(null) } catch (err) { console.error('Save config error:', err) @@ -638,15 +677,60 @@ function Dashboard() { active - - {getSiteDomainName(site)} - - + + {/* Display all mapped domains */} + {site.domains && site.domains.length > 0 ? ( +
+ {site.domains.map((domainInfo, idx) => ( +
+ + + {domainInfo.domain} + + + + {domainInfo.type} + + {domainInfo.type === 'custom' && ( + + {domainInfo.verified ? ( + <> + + verified + + ) : ( + <> + + pending + + )} + + )} +
+ ))} +
+ ) : ( + + {getSiteDomainName(site)} + + + )}