Skip to content
Merged
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
3 changes: 3 additions & 0 deletions apps/connect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# connect

## 0.1.2

### Patch Changes

- Updated dependencies [cb54727]
Expand All @@ -11,13 +12,15 @@
- @graphprotocol/hypergraph@0.3.0

## 0.1.1

### Patch Changes

- Updated dependencies [8622688]
- @graphprotocol/hypergraph-react@0.2.0
- @graphprotocol/hypergraph@0.2.0

## 0.1.0

### Patch Changes

- 114d743: breaking changes of the authentication flow to improve security and fix invitations
Expand Down
19 changes: 9 additions & 10 deletions apps/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,37 @@
"check:fix": "pnpm run lint:fix && pnpm run format"
},
"dependencies": {
"@base-ui-components/react": "1.0.0-beta.0",
"@base-ui-components/react": "1.0.0-beta.1",
"@graphprotocol/grc-20": "^0.21.6",
"@graphprotocol/hypergraph": "workspace:*",
"@graphprotocol/hypergraph-react": "workspace:*",
"@heroicons/react": "^2.2.0",
"@privy-io/react-auth": "^2.13.0",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-router": "^1.120.2",
"@tanstack/react-router-devtools": "^1.122.0",
"@xstate/store": "^3.5.1",
"clsx": "^2.1.1",
"effect": "^3.17.1",
"framer-motion": "^12.10.1",
"graphql-request": "^7.2.0",
"lucide-react": "^0.508.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.2.0",
"tailwind-merge": "^3.3.1",
"viem": "^2.30.6",
"vite": "^6.3.5"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.10",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/router-plugin": "^1.120.2",
"@types/node": "^24.1.0",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"prettier": "^3.6.0",
"prettier-plugin-tailwindcss": "^0.6.13",
"tailwindcss": "^4.1.10",
"unplugin-fonts": "^1.3.1",
"vite-plugin-node-polyfills": "^0.23.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11",
"unplugin-fonts": "^1.4.0",
"vite-plugin-node-polyfills": "^0.24.0",
"vite-plugin-svgr": "^4.3.0"
}
}
2 changes: 1 addition & 1 deletion apps/connect/src/components/CreateSpaceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function CreateSpaceCard({ className, ...props }: CreateSpaceCardProps) {
className="c-input grow"
/>
<select
className="c-input min-w-22"
className="c-input shrink-0"
value={spaceType}
onChange={(e) => setSpaceType(e.target.value as 'private' | 'public')}
>
Expand Down
200 changes: 136 additions & 64 deletions apps/connect/src/components/SpacesCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Checkbox } from '@base-ui-components/react/checkbox';
import { Popover } from '@base-ui-components/react/popover';
import { CheckIcon, EyeSlashIcon } from '@heroicons/react/24/solid';
import { Fragment, useState } from 'react';
import { Loading } from '@/components/ui/Loading';
import type { PrivateSpaceData } from '@/hooks/use-private-spaces';
import type { PublicSpaceData } from '@/hooks/use-public-spaces';
Expand All @@ -7,16 +10,16 @@ import { cn } from '@/lib/utils';
interface SpacesCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
spaces: (PublicSpaceData | PrivateSpaceData)[];
status?: 'loading' | { error: boolean | string } | undefined;
selected?: Set<string>;
onSelected?: (spaceId: string, selected: boolean) => void;
currentAppId?: string;
selected?: Set<string> | undefined;
onSelectedChange?: ((spaceId: string, selected: boolean) => void) | undefined;
currentAppId?: string | undefined;
}

export function SpacesCard({
spaces,
status,
selected,
onSelected,
onSelectedChange,
currentAppId,
className,
...props
Expand All @@ -30,17 +33,16 @@ export function SpacesCard({
return (
<div
className={cn(
`group/card c-card scroll-y scrollbar-none
has-data-error:bg-error-dark
`group/card c-card scroll-y scrollbar-none has-data-error:bg-error-dark
has-data-error:text-error-light
flex flex-col`,
isolate flex flex-col`,
className,
)}
{...props}
>
<h2
className={`
c-card-title group-has-data-error/card:text-error-light sticky top-(--offset) shrink-0
c-card-title group-has-data-error/card:text-error-light sticky top-(--offset) z-10 shrink-0
bg-[color-mix(in_oklab,var(--color-foreground)_calc(var(--progress)*0.25),transparent)]
text-[color-mix(in_oklab,var(--color-background)_var(--progress),var(--color-foreground-muted))]
backdrop-blur-sm
Expand Down Expand Up @@ -71,65 +73,22 @@ export function SpacesCard({
return (
<ul className="grid-cols-auto-fill-36 grid gap-4">
{spaces.map((space) => {
// Determine if space is selected
const isPublicSpace = !('apps' in space);
const isSelected = isPublicSpace ? true : (selected?.has(space.id) ?? false);
const isDisabled =
!isPublicSpace && 'apps' in space && space.apps.some((app) => app.id === currentAppId);
const wasAlreadySelected = !isPublicSpace && space.apps.some((app) => app.id === currentAppId);
const isSelected = isPublicSpace ? true : wasAlreadySelected || (selected?.has(space.id) ?? false);
const isDisabled = isPublicSpace ? true : wasAlreadySelected;

return (
<li key={space.id} className="group/list-item">
<Popover.Root openOnHover delay={50}>
<Popover.Trigger
className={`
group-nth-[5n]/list-item:bg-gradient-violet
group-nth-[5n+1]/list-item:bg-gradient-lavender
group-nth-[5n+2]/list-item:bg-gradient-aqua
group-nth-[5n+3]/list-item:bg-gradient-peach
group-nth-[5n+4]/list-item:bg-gradient-clearmint
flex aspect-video w-full items-end overflow-clip rounded-lg px-3 py-2
${isSelected ? 'ring-2 ring-primary ring-offset-2' : ''}
${isDisabled ? 'ring-2 ring-primary ring-offset-2 cursor-not-allowed' : 'cursor-pointer'}
`}
onClick={() => {
if (!isDisabled && onSelected) {
onSelected(space.id, !isSelected);
}
}}
>
<span className="text-sm leading-tight font-semibold">{space.name || space.id}</span>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="bottom" sideOffset={12}>
<Popover.Popup className="c-popover">
<Popover.Arrow className="c-popover-arrow">
<ArrowSvg />
</Popover.Arrow>
{!('apps' in space) ? (
<Popover.Title className="font-semibold">Public space</Popover.Title>
) : space.apps.length === 0 ? (
<Popover.Title className="font-semibold">
No app has access to this private space
</Popover.Title>
) : (
<>
<Popover.Title className="font-semibold">
Apps with access to this private space
</Popover.Title>
<Popover.Description>
<ul className="list-disc">
{space.apps.map((app) => (
<li key={app.id}>{app.name || app.id}</li>
))}
</ul>
</Popover.Description>
</>
)}
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</li>
<SpaceTile
key={space.id}
visibility={isPublicSpace ? 'public' : 'private'}
space={space}
selected={isSelected}
onSelectedChange={
onSelectedChange ? (newSelected) => onSelectedChange(space.id, newSelected) : undefined
}
disabled={isDisabled}
/>
);
})}
</ul>
Expand All @@ -140,6 +99,119 @@ export function SpacesCard({
);
}

interface SpaceTileProps extends Omit<React.HTMLAttributes<HTMLLIElement>, 'children'> {
visibility: 'public' | 'private';
space: PublicSpaceData | PrivateSpaceData;
selected?: boolean | undefined;
onSelectedChange?: ((selected: boolean) => void) | undefined;
disabled?: boolean | undefined;
}

function SpaceTile({
visibility,
space,
selected = false,
onSelectedChange,
disabled = false,
className,
...props
}: SpaceTileProps) {
const mode = onSelectedChange !== undefined ? 'selection' : 'view';
const Root = mode === 'selection' ? Fragment : Popover.Root;
const Trigger = mode === 'selection' ? Checkbox.Root : Popover.Trigger;
const [popoverOpen, setPopoverOpen] = useState(false);

return (
<li
data-mode={mode}
data-visibility={visibility}
data-selected={selected || undefined}
data-disabled={(mode === 'selection' && disabled) || undefined}
className={cn('group/space', className)}
{...props}
>
<Root
{...(mode === 'view'
? {
open: popoverOpen,
onOpenChange: setPopoverOpen,
}
: {})}
>
<Trigger
{...(mode === 'selection'
? {
disabled,
checked: selected,
onCheckedChange: (checked) => onSelectedChange?.(checked),
}
: {})}
className={`
group-nth-[5n]/space:bg-gradient-violet
group-nth-[5n+1]/space:bg-gradient-lavender
group-nth-[5n+2]/space:bg-gradient-aqua
group-nth-[5n+4]/space:bg-gradient-clearmint
group-nth-[5n+3]/space:bg-gradient-peach
relative flex aspect-video w-full cursor-pointer items-end rounded-lg px-3 py-2
group-data-disabled/space:cursor-not-allowed
`}
>
<span className="truncate text-sm leading-tight font-semibold whitespace-normal">
{space.name || space.id}
</span>
{mode === 'selection' ? (
<span
className={`
group-data-selected/space:bg-primary
text-primary-foreground
absolute top-1 right-1 flex size-5 items-center justify-center rounded-md bg-white/50 opacity-0 transition group-hover/space:opacity-100
group-data-selected/space:opacity-100
group-data-selected/space:group-data-disabled/space:bg-gray-800/50
`}
>
<span className="sr-only group-not-data-selected/space:hidden">Selected</span>
<CheckIcon className="size-3 opacity-0 transition group-data-selected/space:opacity-100" />
</span>
) : null}
{visibility === 'private' ? (
<span className="bg-background/50 text-foreground absolute top-1 left-1 flex h-4 items-center gap-1 rounded-md px-1 text-xs leading-none font-semibold">
<EyeSlashIcon className="size-3" />
Private
</span>
) : null}
</Trigger>
{mode === 'view' ? (
<Popover.Portal>
<Popover.Positioner side="bottom" sideOffset={12}>
<Popover.Popup className="c-popover">
<Popover.Arrow className="c-popover-arrow">
<ArrowSvg />
</Popover.Arrow>
{visibility === 'public' ? (
<Popover.Title className="font-semibold">Public space</Popover.Title>
) : (space as PrivateSpaceData).apps.length === 0 ? (
<Popover.Title className="font-semibold">No app has access to this private space</Popover.Title>
) : (
<>
<Popover.Title className="font-semibold">Apps with access to this private space</Popover.Title>
<Popover.Description>
<ul className="list-disc">
{(space as PrivateSpaceData).apps.map((app) => (
<li key={app.id}>{app.name || app.id}</li>
))}
</ul>
</Popover.Description>
</>
)}
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
) : null}
</Root>
</li>
);
}

function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" role="presentation" {...props}>
Expand Down
24 changes: 20 additions & 4 deletions apps/connect/src/components/ui/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';

interface LoadingProps extends React.HTMLAttributes<HTMLDivElement> {
Expand All @@ -12,13 +11,30 @@ interface LoadingProps extends React.HTMLAttributes<HTMLDivElement> {

export function Loading({ hideLabel = false, className, children = 'Loading...', ...props }: LoadingProps) {
return (
<div
<span
data-hide-label={hideLabel || undefined}
className={cn('group/loading flex items-center gap-[0.5em] font-semibold', className)}
{...props}
>
<Loader2 className="size-[1em] shrink-0 animate-spin" />
<LoadingIcon className="size-[1em] shrink-0 animate-spin" />
{children ? <div className="group-data-[hide-label]/loading:sr-only">{children}</div> : null}
</div>
</span>
);
}

function LoadingIcon(props: React.ComponentProps<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
role="presentation"
{...props}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
}
8 changes: 7 additions & 1 deletion apps/connect/src/css/_components.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
}

@utility c-input {
@apply text-foreground font-regular placeholder:text-foreground/25 bg-background min-h-12 rounded-lg indent-3 text-lg inset-shadow-xs/15 -outline-offset-2 transition disabled:opacity-25;
@apply text-foreground font-regular placeholder:text-foreground/25 bg-background relative min-h-12 appearance-none rounded-lg px-3 text-lg inset-shadow-xs/15 -outline-offset-2 transition disabled:opacity-25;

&:is(select) {
--light-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="%232a2b2e"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /></svg>');
--dark-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="white"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /></svg>');
@apply bg-(image:--light-image) bg-size-[theme(spacing.4)] bg-position-[right_theme(spacing.2)_center] bg-no-repeat pr-7 dark:bg-(image:--dark-image);
}
}

@utility c-card {
Expand Down
2 changes: 1 addition & 1 deletion apps/connect/src/hooks/use-public-spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { gql, request } from 'graphql-request';

const publicSpacesQueryDocument = gql`
query Spaces($accountAddress: String!) {
spaces(filter: {members: {some: {address: {is: $accountAddress}}}}) {
spaces(filter: { members: { some: { address: { is: $accountAddress } } } }) {
id
type
mainVotingAddress
Expand Down
Loading
Loading