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
1 change: 1 addition & 0 deletions apps/events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"lucide-react": "^0.471.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-select": "^5.10.0",
"siwe": "^2.3.2",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
Expand Down
18 changes: 18 additions & 0 deletions apps/events/src/components/todos-read-only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useQueryEntities } from '@graphprotocol/hypergraph-react';
import { Todo } from '../schema';

export const TodosReadOnly = () => {
const todos = useQueryEntities(Todo);

return (
<>
<h1 className="text-2xl font-bold">Todos (read only)</h1>
{todos.map((todo) => (
<div key={todo.id} className="flex flex-row items-center gap-2">
<h2>{todo.name}</h2>
<input type="checkbox" checked={todo.completed} readOnly />
</div>
))}
</>
);
};
51 changes: 44 additions & 7 deletions apps/events/src/components/todos.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import { useCreateEntity, useDeleteEntity, useQueryEntities, useUpdateEntity } from '@graphprotocol/hypergraph-react';
import { useState } from 'react';
import { Todo } from '../schema';
import { useEffect, useState } from 'react';
import Select from 'react-select';
import { Todo, User } from '../schema';
import { Button } from './ui/button';
import { Input } from './ui/input';

export const Todos = () => {
const todos = useQueryEntities(Todo);
const users = useQueryEntities(User);
const createEntity = useCreateEntity(Todo);
const updateEntity = useUpdateEntity(Todo);
const deleteEntity = useDeleteEntity();
const [newTodoTitle, setNewTodoTitle] = useState('');
const [newTodoName, setNewTodoName] = useState('');
const [assignees, setAssignees] = useState<{ value: string; label: string }[]>([]);

useEffect(() => {
setAssignees((prevFilteredAssignees) => {
// filter out assignees that are not in the users array whenever users change
return prevFilteredAssignees.filter((assignee) => users.some((user) => user.id === assignee.value));
});
}, [users]);

const userOptions = users.map((user) => ({ value: user.id, label: user.name }));
return (
<>
<h1 className="text-2xl font-bold">Todos</h1>
<div className="flex flex-row gap-2">
<Input type="text" value={newTodoTitle} onChange={(e) => setNewTodoTitle(e.target.value)} />
<div className="flex flex-col gap-2">
<Input type="text" value={newTodoName} onChange={(e) => setNewTodoName(e.target.value)} />
<Select isMulti value={assignees} onChange={(e) => setAssignees(e.map((a) => a))} options={userOptions} />
<Button
onClick={() => {
createEntity({ name: newTodoTitle, completed: false });
setNewTodoTitle('');
if (newTodoName === '') {
alert('Todo text is required');
return;
}
createEntity({ name: newTodoName, completed: false, assignees: assignees.map(({ value }) => value) });
setNewTodoName('');
}}
>
Create Todo
Expand All @@ -28,6 +44,27 @@ export const Todos = () => {
{todos.map((todo) => (
<div key={todo.id} className="flex flex-row items-center gap-2">
<h2>{todo.name}</h2>
{todo.assignees.length > 0 && (
<span className="text-xs text-gray-500">
Assigned to:{' '}
{todo.assignees.map((assignee) => (
<span key={assignee.id} className="border rounded-sm mr-1 p-1">
{assignee.name}
<button
type="button"
onClick={() =>
updateEntity(todo.id, {
assignees: todo.assignees.map((assignee) => assignee.id).filter((id) => id !== assignee.id),
})
}
className="cursor-pointer ml-1 text-red-400"
>
x
</button>
</span>
))}
</span>
)}
<input
type="checkbox"
checked={todo.completed}
Expand Down
25 changes: 25 additions & 0 deletions apps/events/src/components/user-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useDeleteEntity, useUpdateEntity } from '@graphprotocol/hypergraph-react';
import { useState } from 'react';
import { User } from '../schema.js';
import { Button } from './ui/button';
import { Input } from './ui/input.js';

export const UserEntry = (user: User) => {
const deleteEntity = useDeleteEntity();
const updateEntity = useUpdateEntity(User);
const [editMode, setEditMode] = useState(false);

return (
<div key={user.id} className="flex flex-row items-center gap-2">
<h2>
{user.name} <span className="text-xs text-gray-500">({user.id})</span>
</h2>
<Button onClick={() => deleteEntity(user.id)}>Delete</Button>
<Button onClick={() => setEditMode((prev) => !prev)}>Edit User</Button>

{editMode && (
<Input type="text" value={user.name} onChange={(e) => updateEntity(user.id, { name: e.target.value })} />
)}
</div>
);
};
32 changes: 32 additions & 0 deletions apps/events/src/components/users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCreateEntity, useQueryEntities } from '@graphprotocol/hypergraph-react';
import { useState } from 'react';
import { User } from '../schema.js';
import { Button } from './ui/button.js';
import { Input } from './ui/input.js';
import { UserEntry } from './user-entry.js';

export const Users = () => {
const users = useQueryEntities(User);
const createEntity = useCreateEntity(User);
const [newUserName, setNewUserName] = useState('');

return (
<>
<h1 className="text-2xl font-bold">Users</h1>
<div className="flex flex-row gap-2">
<Input type="text" value={newUserName} onChange={(e) => setNewUserName(e.target.value)} />
<Button
onClick={() => {
createEntity({ name: newUserName });
setNewUserName('');
}}
>
Create User
</Button>
</div>
{users.map((user) => (
<UserEntry key={user.id} {...user} />
))}
</>
);
};
11 changes: 9 additions & 2 deletions apps/events/src/routes/space/$spaceId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { useSelector } from '@xstate/store/react';

import { DevTool } from '@/components/dev-tool';
import { Todos } from '@/components/todos';
import { TodosReadOnly } from '@/components/todos-read-only';
import { Button } from '@/components/ui/button';
import { Users } from '@/components/users';
import { availableAccounts } from '@/lib/availableAccounts';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { getAddress } from 'viem';

export const Route = createFileRoute('/space/$spaceId')({
Expand All @@ -23,6 +25,7 @@ function Space() {
subscribeToSpace({ spaceId });
}
}, [loading, subscribeToSpace, spaceId]);
const [show2ndTodos, setShow2ndTodos] = useState(false);

const space = spaces.find((space) => space.id === spaceId);

Expand All @@ -37,7 +40,10 @@ function Space() {
return (
<div className="flex flex-col gap-4 max-w-screen-sm mx-auto py-8">
<HypergraphSpaceProvider space={spaceId}>
<Users />
<Todos />
<TodosReadOnly />
{show2ndTodos && <Todos />}
<h3 className="text-xl font-bold">Invite people</h3>
<div className="flex flex-row gap-2">
{availableAccounts.map((invitee) => {
Expand All @@ -56,8 +62,9 @@ function Space() {
);
})}
</div>
<div className="mt-12">
<div className="mt-12 flex flex-row gap-2">
<DevTool spaceId={spaceId} />
<Button onClick={() => setShow2ndTodos((prevShow2ndTodos) => !prevShow2ndTodos)}>Toggle Todos</Button>
</div>
</HypergraphSpaceProvider>
</div>
Expand Down
6 changes: 6 additions & 0 deletions apps/events/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Entity } from '@graphprotocol/hypergraph';

export class User extends Entity.Class<User>('User')({
id: Entity.Generated(Entity.Text),
name: Entity.Text,
}) {}

export class Todo extends Entity.Class<Todo>('Todo')({
id: Entity.Generated(Entity.Text),
name: Entity.Text,
completed: Entity.Checkbox,
assignees: Entity.Reference(Entity.ReferenceArray(User)),
}) {}
59 changes: 8 additions & 51 deletions packages/hypergraph-react/src/HypergraphSpaceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { AnyDocumentId, DocHandle, Repo } from '@automerge/automerge-repo';
import { useRepo } from '@automerge/automerge-repo-react-hooks';
import { Entity, Utils } from '@graphprotocol/hypergraph';
import * as Schema from 'effect/Schema';
import { type ReactNode, createContext, useContext, useRef, useSyncExternalStore } from 'react';
import { type ReactNode, createContext, useContext, useRef, useState, useSyncExternalStore } from 'react';

export type HypergraphContext = {
space: string;
Expand All @@ -31,11 +31,13 @@ export function HypergraphSpaceProvider({ space, children }: { space: string; ch
let current = ref.current;
if (current === undefined || space !== current.space || repo !== current.repo) {
const id = Utils.idToAutomergeId(space) as AnyDocumentId;
const handle = repo.find<Entity.DocumentContent>(id);

current = ref.current = {
space,
repo,
id,
handle: repo.find(id),
handle,
};
}

Expand All @@ -59,37 +61,11 @@ export function useDeleteEntity() {

export function useQueryEntities<const S extends Entity.AnyNoContext>(type: S) {
const hypergraph = useHypergraph();
const equal = isEqual(type);

// store as a map of type to array of entities of the type
const prevEntitiesRef = useRef<Readonly<Array<Entity.Entity<S>>>>([]);

const subscribe = (callback: () => void) => {
const handleChange = () => {
callback();
};

const handleDelete = () => {
callback();
};

hypergraph.handle.on('change', handleChange);
hypergraph.handle.on('delete', handleDelete);

return () => {
hypergraph.handle.off('change', handleChange);
hypergraph.handle.off('delete', handleDelete);
};
};

return useSyncExternalStore<Readonly<Array<Entity.Entity<S>>>>(subscribe, () => {
const filtered = Entity.findMany(hypergraph.handle, type);
if (!equal(filtered, prevEntitiesRef.current)) {
prevEntitiesRef.current = filtered;
}

return prevEntitiesRef.current;
const [subscription] = useState(() => {
return Entity.subscribeToFindMany(hypergraph.handle, type);
});

return useSyncExternalStore(subscription.subscribe, subscription.getEntities, () => []);
}

export function useQueryEntity<const S extends Entity.AnyNoContext>(type: S, id: string) {
Expand Down Expand Up @@ -135,22 +111,3 @@ export function useQueryEntity<const S extends Entity.AnyNoContext>(type: S, id:
return prevEntityRef.current;
});
}

/** @internal */
const isEqual = <A, E>(type: Schema.Schema<A, E, never>) => {
const equals = Schema.equivalence(type);

return (a: ReadonlyArray<A>, b: ReadonlyArray<A>) => {
if (a.length !== b.length) {
return false;
}

for (let i = 0; i < a.length; i++) {
if (!equals(a[i], b[i])) {
return false;
}
}

return true;
};
};
Loading
Loading