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
5 changes: 5 additions & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ NEXT_PUBLIC_SENTRY_DSN=https://bf79c216fe3c72328219f04aabeebc99@o609598.ingest.u
NEXT_PUBLIC_SENTRY_ORG=storacha-it
NEXT_PUBLIC_SENTRY_PROJECT=console
NEXT_PUBLIC_SENTRY_ENV=development

# set this to enable the private spaces feature
NEXT_PUBLIC_PRIVATE_SPACES_ENABLED=true
# set this to restrict which users can access the private spaces feature if enabled
NEXT_PUBLIC_PRIVATE_SPACES_DOMAINS=dmail.ai,storacha.network
73 changes: 47 additions & 26 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,73 @@
'use client'

import { useW3, Space } from '@w3ui/react'
import { DidIcon } from '@/components/DidIcon'
import Link from 'next/link'
import { useState } from 'react'
import { useW3 } from '@w3ui/react'
import { SpacesNav } from './space/layout'
import { H1, H2 } from '@/components/Text'
import SidebarLayout from '@/components/SidebarLayout'
import { ReactNode } from 'react'
import { SpacesTabNavigation } from '@/components/SpacesTabNavigation'
import { SpacesList } from '@/components/SpacesList'
import { UpgradePrompt } from '@/components/UpgradePrompt'
import { usePrivateSpacesAccess } from '@/hooks/usePrivateSpacesAccess'
import { useFeatureFlags } from '@/lib/featureFlags'
import { useFilteredSpaces } from '@/hooks/useFilteredSpaces'

export default function HomePage () {
export default function HomePage() {
return (
<SidebarLayout>
<SpacePage />
</SidebarLayout>
)
}

function SpacePage (): ReactNode {
function SpacePage() {
const [activeTab, setActiveTab] = useState<'public' | 'private'>('public')
const [{ spaces }] = useW3()
const { canAccessPrivateSpaces, shouldShowUpgradePrompt, planLoading } = usePrivateSpacesAccess()
const { canSeePrivateSpacesFeature } = useFeatureFlags()
const { publicSpaces, privateSpaces, hasHiddenPrivateSpaces } = useFilteredSpaces()

const shouldShowPrivateSpacesTab = canSeePrivateSpacesFeature

if (spaces.length === 0) {
return <div></div>
}

if (planLoading) {
return (
<div className="flex items-center justify-center py-8">
<div>Loading...</div>
</div>
)
}

return (
<>
<SpacesNav />
<H1>Spaces</H1>
<H2>Pick a Space</H2>
<div className='max-w-lg border rounded-2xl border-hot-red bg-white'>
{ spaces.map(s => <Item space={s} key={s.did()} /> ) }
</div>
</>
)
}

<SpacesTabNavigation
activeTab={activeTab}
onTabChange={setActiveTab}
showPrivateTab={shouldShowPrivateSpacesTab}
privateTabLocked={!canAccessPrivateSpaces}
/>

function Item ({space}: {space: Space}) {
return (
<Link href={`/space/${space.did()}`} className='flex flex-row items-start gap-4 p-4 text-left hover:bg-hot-yellow-light border-b last:border-0 border-hot-red first:rounded-t-2xl last:rounded-b-2xl'>
<DidIcon did={space.did()} />
<div className='grow overflow-hidden whitespace-nowrap text-ellipsis'>
<span className='font-epilogue text-lg text-hot-red leading-5 m-0'>
{space.name || 'Untitled'}
</span>
<span className='font-mono text-xs block'>
{space.did()}
</span>
</div>
</Link>
{activeTab === 'public' && (
<>
<SpacesList spaces={publicSpaces} type="public" />
</>
)}

{activeTab === 'private' && (
<>
{canAccessPrivateSpaces ? (
<SpacesList spaces={privateSpaces} type="private" />
) : (
<UpgradePrompt hasHiddenSpaces={hasHiddenPrivateSpaces} />
)}
</>
)}
</>
)
}
25 changes: 21 additions & 4 deletions src/app/space/[did]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react'
import { useW3 } from '@w3ui/react'
import { DidIcon } from '@/components/DidIcon'
import { Nav, NavLink } from '@/components/Nav'
import { QueueListIcon, ShareIcon, CloudArrowUpIcon } from '@heroicons/react/24/outline'
import { QueueListIcon, ShareIcon, CloudArrowUpIcon, LockClosedIcon, GlobeAltIcon } from '@heroicons/react/24/outline'

interface LayoutProps extends PropsWithChildren {
params: {
Expand Down Expand Up @@ -39,9 +39,26 @@ export default function Layout ({children, params}: LayoutProps): JSX.Element {
<div className='flex flex-row items-start gap-4'>
<DidIcon did={space.did()} width={10} />
<div className='grow overflow-hidden whitespace-nowrap text-ellipsis text-black'>
<h1 className='text-2xl leading-5 text-hot-red'>
{space.name || 'Untitled'}
</h1>
<div className="flex items-center gap-2">
<h1 className='text-2xl leading-5 text-hot-red'>
{space.name || 'Untitled'}
</h1>
{(space as any).accessType === 'private' ? (
<>
<LockClosedIcon className="w-5 h-5 text-hot-red" />
<span className="bg-hot-red text-white px-2 py-1 rounded-full text-xs">
Private
</span>
</>
) : (
<>
<GlobeAltIcon className="w-5 h-5 text-gray-600" />
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded-full text-xs">
Public
</span>
</>
)}
</div>
<label className='font-mono text-xs'>
{space.did()}
</label>
Expand Down
100 changes: 96 additions & 4 deletions src/components/SpaceCreator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import Loader from '../components/Loader'
import { DIDKey } from '@ucanto/interface'
import { DidIcon } from './DidIcon'
import Link from 'next/link'
import { FolderPlusIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
import { FolderPlusIcon, InformationCircleIcon, LockClosedIcon, GlobeAltIcon } from '@heroicons/react/24/outline'
import Tooltip from './Tooltip'
import { H3 } from './Text'
import * as UcantoClient from '@ucanto/client'
import { HTTP } from '@ucanto/transport'
import * as CAR from '@ucanto/transport/car'
import { gatewayHost } from './services'
import { logAndCaptureError } from '@/sentry'
import { usePrivateSpacesAccess } from '@/hooks/usePrivateSpacesAccess'
import { useFeatureFlags } from '@/lib/featureFlags'

export function SpaceCreatorCreating(): JSX.Element {
return (
Expand All @@ -36,20 +38,31 @@ export function SpaceCreatorForm({
const [created, setCreated] = useState(false)
const [name, setName] = useState('')
const [space, setSpace] = useState<Space>()
const [accessType, setAccessType] = useState<'public' | 'private'>('public')

const { canAccessPrivateSpaces, shouldShowUpgradePrompt, planLoading } = usePrivateSpacesAccess()
const { canSeePrivateSpacesFeature } = useFeatureFlags()

function resetForm(): void {
setName('')
setAccessType('public')
}

async function onSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault()
if (!client) return
// TODO: account selection

const account = accounts[0]
if (!account) {
throw new Error('cannot create space, no account found, have you authorized your email?')
}

// Check if user has required access for private spaces
if (accessType === 'private' && !canAccessPrivateSpaces) {
alert('Upgrade to a paid plan to create private spaces')
return
}

const { ok: plan } = await account.plan.get()
if (!plan) {
throw new Error('a payment plan is required on account to provision a new space.')
Expand All @@ -72,8 +85,9 @@ export function SpaceCreatorForm({
})

const space = await client.createSpace(name, {
// accessType, // This will be passed to the upload-service when backend is updated
authorizeGatewayServices: [storachaGateway]
})
} as any)

const provider = toWebDID(process.env.NEXT_PUBLIC_W3UP_PROVIDER) || toWebDID('did:web:web3.storage')
const result = await account.provision(space.did(), { provider })
Expand Down Expand Up @@ -105,6 +119,16 @@ export function SpaceCreatorForm({
}
}

if (planLoading) {
return (
<div className={className}>
<div className="flex items-center justify-center py-4">
<div>Loading...</div>
</div>
</div>
)
}

if (created && space) {
return (
<div className={className}>
Expand Down Expand Up @@ -137,8 +161,76 @@ export function SpaceCreatorForm({
}}
required={true}
/>

{canSeePrivateSpacesFeature && (
<div className="mb-4">
<label className="block mb-2 uppercase text-xs text-hot-red font-epilogue">
Space Type
</label>
<div className="space-y-3">
<label className="flex items-start gap-3 p-3 border border-gray-200 rounded-lg cursor-pointer hover:border-hot-red">
<input
type="radio"
name="accessType"
value="public"
checked={accessType === 'public'}
onChange={(e) => setAccessType(e.target.value as 'public')}
className="mt-1"
/>
<div>
<div className="flex items-center gap-2">
<GlobeAltIcon className="w-4 h-4 text-gray-600" />
<span className="font-medium">Public Space</span>
</div>
<p className="text-sm text-gray-600 mt-1">
Files stored unencrypted and accessible via IPFS
</p>
</div>
</label>

<label className={`flex items-start gap-3 p-3 border rounded-lg cursor-pointer ${
canAccessPrivateSpaces
? 'border-gray-200 hover:border-hot-red'
: 'border-gray-100 bg-gray-50 cursor-not-allowed'
}`}>
<input
type="radio"
name="accessType"
value="private"
checked={accessType === 'private'}
onChange={(e) => canAccessPrivateSpaces && setAccessType(e.target.value as 'private')}
disabled={!canAccessPrivateSpaces}
className="mt-1"
/>
<div>
<div className="flex items-center gap-2">
<LockClosedIcon className="w-4 h-4 text-gray-600" />
<span className="font-medium">Private Space</span>
{!canAccessPrivateSpaces && (
<span className="bg-hot-red text-white px-2 py-1 rounded-full text-xs">
Upgrade Required
</span>
)}
</div>
<p className="text-sm text-gray-600 mt-1">
Files encrypted locally before upload
</p>
{shouldShowUpgradePrompt && (
<Link
href="/plans/change"
className="text-hot-red text-sm underline mt-1 inline-block"
>
Upgrade to Enable →
</Link>
)}
</div>
</label>
</div>
</div>
)}

<button type='submit' className={`inline-block bg-hot-red border border-hot-red hover:bg-white hover:text-hot-red font-epilogue text-white uppercase text-sm px-6 py-2 rounded-full whitespace-nowrap`}>
<FolderPlusIcon className='h-5 w-5 inline-block mr-1 align-middle' style={{ marginTop: -4 }} /> Create
<FolderPlusIcon className='h-5 w-5 inline-block mr-1 align-middle' style={{ marginTop: -4 }} /> Create {accessType === 'private' ? 'Private' : 'Public'} Space
</button>
</form>
</div>
Expand Down
71 changes: 71 additions & 0 deletions src/components/SpacesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Space } from '@w3ui/react'
import { LockClosedIcon, GlobeAltIcon } from '@heroicons/react/24/outline'
import { DidIcon } from './DidIcon'
import Link from 'next/link'

interface SpaceListProps {
spaces: Space[]
type: 'public' | 'private'
}

export function SpacesList({ spaces, type }: SpaceListProps) {
if (spaces.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
<p>No {type} spaces yet.</p>
<Link
href="/space/create"
className="text-hot-red hover:underline"
>
Create your first {type} space
</Link>
</div>
)
}

return (
<div className="max-w-lg border rounded-2xl border-hot-red bg-white">
{spaces.map(space => (
<SpaceItem key={space.did()} space={space} type={type} />
))}
</div>
)
}

interface SpaceItemProps {
space: Space
type: 'public' | 'private'
}

function SpaceItem({ space, type }: SpaceItemProps) {
const Icon = type === 'private' ? LockClosedIcon : GlobeAltIcon

return (
<Link
href={`/space/${space.did()}`}
className="flex flex-row items-start gap-4 p-4 text-left hover:bg-hot-yellow-light border-b last:border-0 border-hot-red first:rounded-t-2xl last:rounded-b-2xl"
>
<div className="flex items-center gap-2">
<DidIcon did={space.did()} />
<Icon className="w-4 h-4 text-hot-red" />
</div>
<div className="grow overflow-hidden whitespace-nowrap text-ellipsis">
<div className="flex items-center gap-2">
<span className="font-epilogue text-lg text-hot-red leading-5 m-0">
{space.name || 'Untitled'}
</span>
<span className={`px-2 py-1 rounded-full text-xs ${
type === 'private'
? 'bg-hot-red text-white'
: 'bg-gray-100 text-gray-700'
}`}>
{type === 'private' ? 'Private' : 'Public'}
</span>
</div>
<span className="font-mono text-xs block">
{space.did()}
</span>
</div>
</Link>
)
}
Loading
Loading