Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,40 @@
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
}
},
// Configure headers for iframe support
async headers() {
return [
// Default security headers for main app
{
source: '/((?!iframe).*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'none';",
},
],
},
// Iframe-friendly headers only for /iframe routes
{
source: '/iframe/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'ALLOWALL',
},
{
key: 'Content-Security-Policy',
value: "frame-ancestors *; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
},
],
},
]
},
}

module.exports = nextConfig
Expand Down
46 changes: 46 additions & 0 deletions src/app/iframe/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import '../globals.css'
import type { Metadata } from 'next'
import Provider from '@/components/W3UIProvider'
import Toaster from '@/components/Toaster'
import { Provider as MigrationsProvider } from '@/components/MigrationsProvider'
import { IframeProvider } from '@/contexts/IframeContext'
import IframeHeader from '@/components/IframeHeader'
import IframeAuthenticator from '@/components/IframeAuthenticator'

export const metadata: Metadata = {
title: 'Storacha Workspaces',
description: 'Manage your Storacha storage spaces',
}

export default function IframeLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin='anonymous' />
<link href="https://fonts.googleapis.com/css2?family=Epilogue:ital@0;1&display=swap" rel="stylesheet" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</head>
<body className='bg-white min-h-screen overflow-hidden'>
<IframeProvider>
<Provider>
<MigrationsProvider>
<IframeAuthenticator>
<div className="flex flex-col h-screen">
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
</IframeAuthenticator>
</MigrationsProvider>
</Provider>
<Toaster />
</IframeProvider>
</body>
</html>
)
}
14 changes: 14 additions & 0 deletions src/app/iframe/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Suspense } from 'react'
import IframeDashboard from '@/components/IframeDashboard'

export default function IframePage() {
return (
<div className="flex h-full min-h-0">
<div className="flex-1 overflow-auto min-w-0">
<Suspense fallback={<div className="p-4">Loading...</div>}>
<IframeDashboard />
</Suspense>
</div>
</div>
)
}
21 changes: 21 additions & 0 deletions src/app/iframe/space/[did]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PropsWithChildren, ReactNode } from 'react'
import IframeCompactSidebar from '@/components/IframeCompactSidebar'

export const runtime = 'edge'

interface LayoutProps extends PropsWithChildren {
params: {
did: string
}
}

export default function IframeSpaceLayout({ children }: LayoutProps): ReactNode {
return (
<div className="flex h-full min-h-0">
<IframeCompactSidebar />
<div className="flex-1 overflow-auto bg-white min-w-0">
{children}
</div>
</div>
)
}
96 changes: 96 additions & 0 deletions src/app/iframe/space/[did]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client'

import { UploadsList } from '@/components/UploadsList'
import { useW3, UnknownLink, UploadListSuccess, Authenticator } from '@w3ui/react'
import useSWR from 'swr'
import { useRouter, usePathname } from 'next/navigation'
import { createUploadsListKey } from '@/cache'
import { Breadcrumbs } from '@/components/Breadcrumbs'
import { logAndCaptureError } from '@/sentry'
import { AuthenticationEnsurer } from '@/components/Authenticator'
import { SpaceEnsurer } from '@/components/SpaceEnsurer'
import { MaybePlanGate } from '@/components/PlanGate'
import IframeAuthenticator from '@/components/IframeAuthenticator'

const pageSize = 15

interface PageProps {
params: {
did: string
},
searchParams: {
cursor: string
pre: string
}
}

export default function IframeSpacePage({ params, searchParams }: PageProps): JSX.Element {
return (
<IframeAuthenticator>
<AuthenticationEnsurer>
<MaybePlanGate>
<SpaceEnsurer>
<SpacePageContent params={params} searchParams={searchParams} />
</SpaceEnsurer>
</MaybePlanGate>
</AuthenticationEnsurer>
</IframeAuthenticator>
)
}

function SpacePageContent({ params, searchParams }: PageProps): JSX.Element {
const [{ client, spaces }] = useW3()
const spaceDID = decodeURIComponent(params.did)
const space = spaces.find(s => s.did() === spaceDID)

const key = space ? createUploadsListKey(space.did(), searchParams.cursor, searchParams.pre === 'true') : ''
const { data: uploads, isLoading, isValidating, mutate } = useSWR<UploadListSuccess|undefined>(key, {
fetcher: async () => {
if (!client || !space) return

if (client.currentSpace()?.did() !== space.did()) {
await client.setCurrentSpace(space.did())
}

return await client.capability.upload.list({
cursor: searchParams.cursor,
pre: searchParams.pre === 'true',
size: pageSize
})
},
onError: logAndCaptureError,
keepPreviousData: true
})

const router = useRouter()
const pathname = usePathname()

if (!space) return <div className="p-6"><div>Space not found</div></div>

const handleSelect = (root: UnknownLink) => router.push(`${pathname}/root/${root}`)
const handleNext = uploads?.after && (uploads.results.length === pageSize)
? () => router.push(`${pathname}?cursor=${uploads.after}`)
: undefined
const handlePrev = searchParams.cursor && uploads?.before
? () => router.push(`${pathname}?cursor=${uploads.before ?? ''}&pre=true`)
: undefined
const handleRefresh = () => mutate()

return (
<div className="p-4 max-w-6xl mx-auto">
<div className="mb-4">
<Breadcrumbs space={space.did()} />
</div>
<UploadsList
space={space}
uploads={uploads?.results ?? []}
loading={isLoading}
validating={isValidating}
onSelect={handleSelect}
onNext={handleNext}
onPrev={handlePrev}
onRefresh={handleRefresh}
/>
</div>
)
}
Loading
Loading