From cc2e7dc22c0e360c3824525dc678c44e27027b21 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 30 Jul 2024 00:07:21 -0400 Subject: [PATCH] Feature/home (#41) * init home * Feature/lists (#39) * update shortcuts to use os specific key * add basic list handling! You should be able to indent, outdent, create and delete list blocks. We're storing the data heierarchicaly but we're rendering it as a flat list in the UI. This makes all the next/previous block logic more or less just work. There are still lots of places in the app where we're assuming a card's block/children relations are the only children, which is no longer the case, we need to use the query to recursively get all the lists children. We also need to wire up the copy and paste logic to handle lists, as well as the previews on cards. Finally, we need to to add shortcuts for list item movement. That should be realtively easy! * fixup! Merge branch 'main' into feature/lists * add factID to stuff * make linkblock w-full * properly get list block type * simplify enter adding block logic * handle lists in enter on non text blocks * don't show block previews in first list blocks * fix initial blocks query * fix deleting empty list block * remove old entity id in ui * render lists in card preview * added initial styling for lists * use input rule to trigger list * handle enter on blank line to outdent * remove bullet on first block delete * handle copying lists! * fixed issue where block options was causing empty text blocks to flicker in size * got the rendered text blocks to match up with the base text blocks for lists, pulled out the block padding logic * added bullet points to the card block content * removing some stray console logs * handle pasting lists, basic * minor adjusments to make it looks nice on mobile * added select state to image, cleaned up cardblock select state * adding back a hidden class in block options that i forgot to put back in last commit * slapped a bandaid on the oddest little big where the cardpreview in card block would scroll up if the text block directly under it was turned into a header via markdown input * prevent default in backspace in selection handler * handle last block properly with list stuff * bulk remove blocks * make thing optionall * don't remove list block on backspace * debounce selection handlers * handle enter in lists better * calculate nextPosition at top level * handle copying list depth better * even more tweaking copy and past list parsing * handle shift-tab and backspace better * fix a bug in calculating next position * handle tab and shift tab w/ multiselect * add indent and outdent buttons * add move block shortcuts * fix outdent logic applying in reverse * handle moving blocks between parents on cmd-up/down * add folding! * fix bug w/ moving top level list up * unfold lists on list operations * reuse fact id in mutation --------- Co-authored-by: celine * use markdown/html parsers to improve copy/paste flows * add shift to move block shortcuts * fix bug in next position calc * tooltip fixes and make list shortcut * tweak focusfirstblock * add basic homepage db stuff * WIP styling home * wire up data for homepage theme and such * add back identity data migration * add in favicon, edits to th docPreview styling * remove accidentally commit changes to useUIState * add link block stuff * restyled home page ot fit better on mobile * add create new doc button * pulled background out of theme provider into its own component, added more options / delete to docs in home * wire up deleting docs * styled the add doc button to home, fixed up home layout to better match doc layout * create identity if neccessary on visit to home * add home button * removed uneccesary parts of theme picker in home, more futzing with mobile layout * added a homeSmall icon, styled home button, componentized hover button, and futzed with layout, as always * small styling tweak to menu item component * fixed styling issue with placeholder text in new docs on first render * rename permission_token_creator and auto add editable links to homepage * added a little help tooltip on home in case of user clearing cookies, removed a duplicate file for favicon * remove icon copy and fix home icon logic --------- Co-authored-by: celine --- actions/createIdentity.ts | 48 ++++++ actions/createNewDoc.ts | 92 ++++++++++++ actions/deleteDoc.ts | 40 +++++ app/[doc_id]/Doc.tsx | 25 ++-- app/[doc_id]/page.tsx | 24 ++- app/globals.css | 2 +- app/home/DocOptions.tsx | 48 ++++++ app/home/DocPreview.tsx | 106 ++++++++++++++ app/home/HomeHelp.tsx | 43 ++++++ app/home/IdentitySetter.tsx | 13 ++ app/home/icon.tsx | 106 ++++++++++++++ app/home/page.tsx | 120 +++++++++++++++ app/page.tsx | 81 ---------- app/route.ts | 9 ++ components/Blocks/CardBlock.tsx | 46 ++++-- components/Blocks/TextBlock/index.tsx | 2 +- components/Buttons.tsx | 32 ++++ components/Cards.tsx | 13 +- components/HomeButton.tsx | 20 +++ components/Icons.tsx | 79 ++++++++++ components/Layout.tsx | 2 +- components/MobileFooter.tsx | 10 +- components/ShareOptions/index.tsx | 30 ++-- components/ThemeManager/ThemeProvider.tsx | 42 ++++-- components/ThemeManager/ThemeSetter.tsx | 138 ++++++++++-------- drizzle/relations.ts | 23 ++- drizzle/schema.ts | 16 ++ package-lock.json | 57 ++++---- package.json | 2 +- supabase/database.types.ts | 56 +++++++ .../20240725190635_add identity tables.sql | 120 +++++++++++++++ 31 files changed, 1209 insertions(+), 236 deletions(-) create mode 100644 actions/createIdentity.ts create mode 100644 actions/createNewDoc.ts create mode 100644 actions/deleteDoc.ts create mode 100644 app/home/DocOptions.tsx create mode 100644 app/home/DocPreview.tsx create mode 100644 app/home/HomeHelp.tsx create mode 100644 app/home/IdentitySetter.tsx create mode 100644 app/home/icon.tsx create mode 100644 app/home/page.tsx delete mode 100644 app/page.tsx create mode 100644 app/route.ts create mode 100644 components/HomeButton.tsx create mode 100644 supabase/migrations/20240725190635_add identity tables.sql diff --git a/actions/createIdentity.ts b/actions/createIdentity.ts new file mode 100644 index 00000000..51ec0f7d --- /dev/null +++ b/actions/createIdentity.ts @@ -0,0 +1,48 @@ +import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import { + entities, + permission_tokens, + permission_token_rights, + entity_sets, + facts, + identities, +} from "drizzle/schema"; +import { redirect } from "next/navigation"; +import postgres from "postgres"; +import { v7 } from "uuid"; +import { sql } from "drizzle-orm"; +import { cookies } from "next/headers"; +export async function createIdentity(db: PostgresJsDatabase) { + return db.transaction(async (tx) => { + // Create a new entity set + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); + // Create a root-entity + let [entity] = await tx + .insert(entities) + // And add it to that permission set + .values({ set: entity_set.id, id: v7() }) + .returning(); + //Create a new permission token + let [permissionToken] = await tx + .insert(permission_tokens) + .values({ root_entity: entity.id }) + .returning(); + //and give it all the permission on that entity set + let [rights] = await tx + .insert(permission_token_rights) + .values({ + token: permissionToken.id, + entity_set: entity_set.id, + read: true, + write: true, + create_token: true, + change_entity_set: true, + }) + .returning(); + let [identity] = await tx + .insert(identities) + .values({ home_page: permissionToken.id }) + .returning(); + return identity; + }); +} diff --git a/actions/createNewDoc.ts b/actions/createNewDoc.ts new file mode 100644 index 00000000..761528a2 --- /dev/null +++ b/actions/createNewDoc.ts @@ -0,0 +1,92 @@ +"use server"; + +import { drizzle } from "drizzle-orm/postgres-js"; +import { + entities, + permission_tokens, + permission_token_rights, + entity_sets, + facts, + permission_token_on_homepage, +} from "drizzle/schema"; +import { redirect } from "next/navigation"; +import postgres from "postgres"; +import { v7 } from "uuid"; +import { sql } from "drizzle-orm"; +import { cookies } from "next/headers"; +import { createIdentity } from "./createIdentity"; +const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); +const db = drizzle(client); + +export async function createNewDoc() { + let cookieStore = cookies(); + let identity = cookieStore.get("identity")?.value; + if (!identity) { + let newIdentity = await createIdentity(db); + cookieStore.set("identity", newIdentity.id, { sameSite: "strict" }); + identity = newIdentity.id; + } + + let { permissionToken } = await db.transaction(async (tx) => { + // Create a new entity set + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); + // Create a root-entity + let [entity] = await tx + .insert(entities) + // And add it to that permission set + .values({ set: entity_set.id, id: v7() }) + .returning(); + //Create a new permission token + let [permissionToken] = await tx + .insert(permission_tokens) + .values({ root_entity: entity.id }) + .returning(); + //and give it all the permission on that entity set + let [rights] = await tx + .insert(permission_token_rights) + .values({ + token: permissionToken.id, + entity_set: entity_set.id, + read: true, + write: true, + create_token: true, + change_entity_set: true, + }) + .returning(); + + // and add it to created_by for the identity + await tx + .insert(permission_token_on_homepage) + .values({ identity, token: permissionToken.id }); + let [blockEntity] = await tx + .insert(entities) + // And add it to that permission set + .values({ set: entity_set.id, id: v7() }) + .returning(); + + await tx.insert(facts).values([ + { + id: v7(), + entity: entity.id, + attribute: "card/block", + data: sql`${{ type: "ordered-reference", value: blockEntity.id, position: "a0" }}::jsonb`, + }, + { + id: v7(), + entity: blockEntity.id, + attribute: "block/type", + data: sql`${{ type: "block-type-union", value: "heading" }}::jsonb`, + }, + { + id: v7(), + entity: blockEntity.id, + attribute: "block/heading-level", + data: sql`${{ type: "number", value: 1 }}::jsonb`, + }, + ]); + + return { permissionToken, rights, entity, entity_set }; + }); + + redirect(`/${permissionToken.id}?focusFirstBlock`); +} diff --git a/actions/deleteDoc.ts b/actions/deleteDoc.ts new file mode 100644 index 00000000..b15d65d5 --- /dev/null +++ b/actions/deleteDoc.ts @@ -0,0 +1,40 @@ +"use server"; + +import { drizzle } from "drizzle-orm/postgres-js"; +import { + entities, + permission_tokens, + permission_token_rights, +} from "drizzle/schema"; +import { redirect } from "next/navigation"; +import postgres from "postgres"; +import { v7 } from "uuid"; +import { eq, sql } from "drizzle-orm"; +import { cookies } from "next/headers"; +import { PermissionToken } from "src/replicache"; +import { revalidatePath } from "next/cache"; +const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); +const db = drizzle(client); + +export async function deleteDoc(permission_token: PermissionToken) { + await db.transaction(async (tx) => { + let [token] = await tx + .select() + .from(permission_tokens) + .leftJoin( + permission_token_rights, + eq(permission_tokens.id, permission_token_rights.token), + ) + .where(eq(permission_tokens.id, permission_token.id)); + + console.log(token); + if (!token.permission_token_rights?.write) return; + await tx + .delete(entities) + .where(eq(entities.set, token.permission_token_rights.entity_set)); + await tx + .delete(permission_tokens) + .where(eq(permission_tokens.id, permission_token.id)); + }); + return revalidatePath("/docs"); +} diff --git a/app/[doc_id]/Doc.tsx b/app/[doc_id]/Doc.tsx index c1a175d9..eacb865d 100644 --- a/app/[doc_id]/Doc.tsx +++ b/app/[doc_id]/Doc.tsx @@ -4,7 +4,10 @@ import { Attributes } from "src/replicache/attributes"; import { createServerClient } from "@supabase/ssr"; import { SelectionManager } from "components/SelectionManager"; import { Cards } from "components/Cards"; -import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; +import { + ThemeBackgroundProvider, + ThemeProvider, +} from "components/ThemeManager/ThemeProvider"; import { MobileFooter } from "components/MobileFooter"; import { PopUpProvider } from "components/Toast"; import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; @@ -30,15 +33,17 @@ export function Doc(props: { > - - - - + + + + + + diff --git a/app/[doc_id]/page.tsx b/app/[doc_id]/page.tsx index 24c2565b..1965a7c3 100644 --- a/app/[doc_id]/page.tsx +++ b/app/[doc_id]/page.tsx @@ -1,18 +1,14 @@ -import { Metadata, ResolvingMetadata } from "next"; +import { Metadata } from "next"; import * as Y from "yjs"; import * as base64 from "base64-js"; -import { Fact, ReplicacheProvider } from "src/replicache"; +import { Fact } from "src/replicache"; import { Database } from "../../supabase/database.types"; import { Attributes } from "src/replicache/attributes"; import { createServerClient } from "@supabase/ssr"; -import { SelectionManager } from "components/SelectionManager"; -import { Cards } from "components/Cards"; -import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; -import { MobileFooter } from "components/MobileFooter"; -import { PopUpProvider } from "components/Toast"; import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; import { Doc } from "./Doc"; +import { cookies } from "next/headers"; export const preferredRegion = ["sfo1"]; export const dynamic = "force-dynamic"; @@ -30,7 +26,7 @@ type Props = { export default async function DocumentPage(props: Props) { let res = await supabase .from("permission_tokens") - .select("*, permission_token_rights(*)") + .select("*, permission_token_rights(*), permission_token_on_homepage(*)") .eq("id", props.params.doc_id) .single(); let rootEntity = res.data?.root_entity; @@ -51,6 +47,18 @@ export default async function DocumentPage(props: Props) { ); + let identity = cookies().get("identity"); + if ( + identity?.value && + !res.data.permission_token_on_homepage.find( + (f) => f.identity === identity.value, + ) + ) { + await supabase.from("permission_token_on_homepage").insert({ + identity: identity.value, + token: res.data.id, + }); + } let { data } = await supabase.rpc("get_facts", { root: rootEntity, }); diff --git a/app/globals.css b/app/globals.css index e7890d1c..e10313e3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -58,7 +58,7 @@ h4 { @apply text-base; - @apply font-bold italic; + @apply font-bold; } p { diff --git a/app/home/DocOptions.tsx b/app/home/DocOptions.tsx new file mode 100644 index 00000000..fb3af355 --- /dev/null +++ b/app/home/DocOptions.tsx @@ -0,0 +1,48 @@ +"use client"; +import { PopoverArrow } from "components/Icons"; +import { DeleteSmall, MoreOptionsTiny } from "components/Icons"; +import * as Popover from "@radix-ui/react-popover"; +import { Menu, MenuItem } from "components/Layout"; +import { theme } from "tailwind.config"; +import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; +import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; + +export const DocOptions = (props: { + doc_id: string; + setState: (s: "normal" | "deleting") => void; +}) => { + return ( + <> +
+ + + + + + + + + + { + props.setState("deleting"); + }} + > + + Delete Doc + + + + + + + + + +
+ + ); +}; diff --git a/app/home/DocPreview.tsx b/app/home/DocPreview.tsx new file mode 100644 index 00000000..1da05129 --- /dev/null +++ b/app/home/DocPreview.tsx @@ -0,0 +1,106 @@ +"use client"; +import { BlockPreview, CardPreview } from "components/Blocks/CardBlock"; +import { + ThemeBackgroundProvider, + ThemeProvider, +} from "components/ThemeManager/ThemeProvider"; +import { useRef, useState } from "react"; +import { Link } from "react-aria-components"; +import { useBlocks } from "src/hooks/queries/useBlocks"; +import { PermissionToken } from "src/replicache"; +import { DocOptions } from "./DocOptions"; +import { deleteDoc } from "actions/deleteDoc"; + +export const DocPreview = (props: { + token: PermissionToken; + doc_id: string; +}) => { + let [state, setState] = useState<"normal" | "deleting">("normal"); + return ( +
+ + {state === "normal" ? ( + +
+ +
+
+ +
+
+
+
+ + ) : ( +
+
+
Delete this Page?
+
+ + +
+
+
+ )} + {state === "normal" && ( + + )} +
+
+ ); +}; + +const DocContent = (props: { entityID: string }) => { + let blocks = useBlocks(props.entityID); + let previewRef = useRef(null); + + return ( +
+ {blocks.slice(0, 10).map((b) => { + return ( + + ); + })} +
+ ); +}; diff --git a/app/home/HomeHelp.tsx b/app/home/HomeHelp.tsx new file mode 100644 index 00000000..9efe9b08 --- /dev/null +++ b/app/home/HomeHelp.tsx @@ -0,0 +1,43 @@ +"use client"; +import { InfoSmall, PopoverArrow } from "components/Icons"; +import { HoverButton } from "components/Buttons"; +import * as Popover from "@radix-ui/react-popover"; + +export const HomeHelp = () => { + return ( + + + + label="Info" + background="bg-accent-1" + text="text-accent-2" + /> + + + +
+
+ These docs are saved using cookies,{" "} + + if you clear your cookies you will lose access to them. + +
+
+ Please contact us{" "} + and we'll help recover them! +
+
+ + + +
+
+
+ ); +}; diff --git a/app/home/IdentitySetter.tsx b/app/home/IdentitySetter.tsx new file mode 100644 index 00000000..ecd889c0 --- /dev/null +++ b/app/home/IdentitySetter.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { useEffect } from "react"; + +export function IdentitySetter(props: { + cb: () => Promise; + call: boolean; +}) { + useEffect(() => { + if (props.call) props.cb(); + }, [props]); + return null; +} diff --git a/app/home/icon.tsx b/app/home/icon.tsx new file mode 100644 index 00000000..e1b89452 --- /dev/null +++ b/app/home/icon.tsx @@ -0,0 +1,106 @@ +import { ImageResponse } from "next/og"; +import { Fact } from "src/replicache"; +import { Attributes } from "src/replicache/attributes"; +import { Database } from "../../supabase/database.types"; +import { createServerClient } from "@supabase/ssr"; +import { parseHSBToRGB } from "src/utils/parseHSB"; +import { cookies } from "next/headers"; + +// Route segment config +export const revalidate = 0; +export const preferredRegion = ["sfo1"]; +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; + +// Image metadata +export const size = { + width: 32, + height: 32, +}; +export const contentType = "image/png"; + +// Image generation +let supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string, + { cookies: {} }, +); +export default async function Icon() { + let cookieStore = cookies(); + let identity = cookieStore.get("identity"); + let rootEntity: string | null = null; + if (identity) { + let res = await supabase + .from("identities") + .select( + `*, + permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)), + permission_token_on_homepage( + *, permission_tokens(*, permission_token_rights(*)) + ) + `, + ) + .eq("id", identity?.value) + .single(); + rootEntity = res.data?.permission_tokens?.root_entity || null; + } + let outlineColor, fillColor; + if (rootEntity) { + let { data } = await supabase.rpc("get_facts", { + root: rootEntity, + }); + let initialFacts = + (data as unknown as Fact[]) || []; + let themeCardBG = initialFacts.find( + (f) => f.attribute === "theme/card-background", + ) as Fact<"theme/card-background"> | undefined; + + let themePrimary = initialFacts.find( + (f) => f.attribute === "theme/primary", + ) as Fact<"theme/primary"> | undefined; + + outlineColor = parseHSBToRGB(`hsba(${themeCardBG?.data.value})`); + + fillColor = parseHSBToRGB(`hsba(${themePrimary?.data.value})`); + } + + return new ImageResponse( + ( + // ImageResponse JSX element +
+ + {/* outline */} + + + {/* fill */} + + +
+ ), + // ImageResponse options + { + // For convenience, we can re-use the exported icons size metadata + // config to also set the ImageResponse's width and height. + ...size, + headers: { + "Cache-Control": "no-cache", + }, + }, + ); +} diff --git a/app/home/page.tsx b/app/home/page.tsx new file mode 100644 index 00000000..f154f4d7 --- /dev/null +++ b/app/home/page.tsx @@ -0,0 +1,120 @@ +import { AddTiny } from "components/Icons"; +import { cookies } from "next/headers"; +import { Fact, ReplicacheProvider } from "src/replicache"; +import { createServerClient } from "@supabase/ssr"; +import { Database } from "supabase/database.types"; +import { DocPreview } from "./DocPreview"; +import { Attributes } from "src/replicache/attributes"; +import { + ThemeBackgroundProvider, + ThemeProvider, +} from "components/ThemeManager/ThemeProvider"; +import { EntitySetProvider } from "components/EntitySetProvider"; +import { ThemePopover } from "components/ThemeManager/ThemeSetter"; +import { createNewDoc } from "actions/createNewDoc"; +import { createIdentity } from "actions/createIdentity"; +import postgres from "postgres"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { IdentitySetter } from "./IdentitySetter"; +import { HoverButton } from "components/Buttons"; +import { HomeHelp } from "./HomeHelp"; + +let supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string, + { cookies: {} }, +); +export default async function Home() { + let cookieStore = cookies(); + let identity = cookieStore.get("identity")?.value; + let needstosetcookie = false; + if (!identity) { + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); + const db = drizzle(client); + let newIdentity = await createIdentity(db); + client.end(); + identity = newIdentity.id; + needstosetcookie = true; + } + + async function setCookie() { + "use server"; + + cookies().set("identity", identity as string, { sameSite: "strict" }); + } + + let res = await supabase + .from("identities") + .select( + `*, + permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)), + permission_token_on_homepage( + *, permission_tokens(*, permission_token_rights(*)) + ) + `, + ) + .eq("id", identity) + .single(); + if (!res.data) return
{JSON.stringify(res.error)}
; + let docs = res.data.permission_token_on_homepage + .map((d) => d.permission_tokens) + .filter((d) => d !== null); + if (!res.data.permission_tokens) return
no home page wierdly
; + let { data } = await supabase.rpc("get_facts", { + root: res.data.permission_tokens?.root_entity, + }); + let initialFacts = (data as unknown as Fact[]) || []; + let root_entity = res.data.permission_tokens.root_entity; + return ( + + + + +
+ +
+
+
+ +
+ + + +
+
+
+ {docs.map((doc) => ( + + + + ))} +
+
+
+
+
+
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index b605c2ab..00000000 --- a/app/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js"; -import { - entities, - permission_tokens, - permission_token_rights, - entity_sets, - facts, -} from "drizzle/schema"; -import { redirect } from "next/navigation"; -import postgres from "postgres"; -import { v7 } from "uuid"; -import { sql } from "drizzle-orm"; -const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); -const db = drizzle(client); - -export const preferredRegion = ["sfo1"]; -export const dynamic = "force-dynamic"; -export const fetchCache = "force-no-store"; - -export default async function RootPage() { - // Creating a new document - let { permissionToken, rights, entity, entity_set } = await db.transaction( - async (tx) => { - // Create a new entity set - let [entity_set] = await tx.insert(entity_sets).values({}).returning(); - // Create a root-entity - let [entity] = await tx - .insert(entities) - // And add it to that permission set - .values({ set: entity_set.id, id: v7() }) - .returning(); - //Create a new permission token - let [permissionToken] = await tx - .insert(permission_tokens) - .values({ root_entity: entity.id }) - .returning(); - //and give it all the permission on that entity set - let [rights] = await tx - .insert(permission_token_rights) - .values({ - token: permissionToken.id, - entity_set: entity_set.id, - read: true, - write: true, - create_token: true, - change_entity_set: true, - }) - .returning(); - let [blockEntity] = await tx - .insert(entities) - // And add it to that permission set - .values({ set: entity_set.id, id: v7() }) - .returning(); - - await tx.insert(facts).values([ - { - id: v7(), - entity: entity.id, - attribute: "card/block", - data: sql`${{ type: "ordered-reference", value: blockEntity.id, position: "a0" }}::jsonb`, - }, - { - id: v7(), - entity: blockEntity.id, - attribute: "block/type", - data: sql`${{ type: "block-type-union", value: "heading" }}::jsonb`, - }, - { - id: v7(), - entity: blockEntity.id, - attribute: "block/heading-level", - data: sql`${{ type: "number", value: 1 }}::jsonb`, - }, - ]); - - return { permissionToken, rights, entity, entity_set }; - }, - ); - - redirect(`/${permissionToken.id}?focusFirstBlock`); -} diff --git a/app/route.ts b/app/route.ts new file mode 100644 index 00000000..1fa9fa68 --- /dev/null +++ b/app/route.ts @@ -0,0 +1,9 @@ +import { createNewDoc } from "actions/createNewDoc"; + +export const preferredRegion = ["sfo1"]; +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; + +export async function GET() { + await createNewDoc(); +} diff --git a/components/Blocks/CardBlock.tsx b/components/Blocks/CardBlock.tsx index 89c6cf18..92bce099 100644 --- a/components/Blocks/CardBlock.tsx +++ b/components/Blocks/CardBlock.tsx @@ -1,3 +1,4 @@ +"use client"; import { Block, BlockProps, focusBlock, ListMarker } from "components/Blocks"; import { focusCard } from "components/Cards"; import { useEntity, useReplicache } from "src/replicache"; @@ -193,7 +194,7 @@ export function CardBlock(props: BlockProps) { ); } -function CardPreview(props: { entityID: string }) { +export function CardPreview(props: { entityID: string }) { let blocks = useBlocks(props.entityID); let previewRef = useRef(null); @@ -209,8 +210,11 @@ function CardPreview(props: { entityID: string }) { ); } -function BlockPreview( - b: Block & { previewRef: React.RefObject }, +export function BlockPreview( + b: Block & { + previewRef: React.RefObject; + size?: "small" | "large"; + }, ) { let headingLevel = useEntity(b.value, "block/heading-level")?.data.value; let ref = useRef(null); @@ -256,30 +260,35 @@ function BlockPreview( /> - {isVisible && } + {isVisible && } ); return (
- {isVisible && } + {isVisible && }
); } -function PreviewBlockContent(props: Block) { +function PreviewBlockContent(props: Block & { size?: "small" | "large" }) { switch (props.type) { case "text": { return ( -
+
); } + case "link": { + return ( +
+ ); + } case "heading": - return ; + return ; case "card": return ( -
+
); case "image": return ; @@ -288,10 +297,19 @@ function PreviewBlockContent(props: Block) { } } -function HeadingPreviewBlock(props: { entityID: string }) { +function HeadingPreviewBlock(props: { + entityID: string; + size?: "small" | "large"; +}) { let headingLevel = useEntity(props.entityID, "block/heading-level"); return ( -
+
); @@ -303,6 +321,12 @@ const HeadingStyle = { 3: "text-[4px] font-bold italic text-secondary ", } as { [level: number]: string }; +const LargeHeadingStyle = { + 1: "text-[9px] font-bold", + 2: "text-[7px] font-bold ", + 3: "text-[6px] font-bold italic text-secondary ", +} as { [level: number]: string }; + function ImagePreviewBlock(props: { entityID: string }) { let image = useEntity(props.entityID, "block/image"); return ( diff --git a/components/Blocks/TextBlock/index.tsx b/components/Blocks/TextBlock/index.tsx index 10321fd6..4d0763e2 100644 --- a/components/Blocks/TextBlock/index.tsx +++ b/components/Blocks/TextBlock/index.tsx @@ -126,7 +126,7 @@ export function RenderedTextBlock(props: { if (!initialFact) // show a blank line if the block is empty. blocks with content are styled elsewhere! update both! return ( -
+      
         {/* Render a placeholder if there are no other blocks in the card, else just show the blank line*/}
         {props.first ? "Title" : 
}
diff --git a/components/Buttons.tsx b/components/Buttons.tsx index 943eaf89..08224c26 100644 --- a/components/Buttons.tsx +++ b/components/Buttons.tsx @@ -1,3 +1,5 @@ +import React from "react"; + type ButtonProps = Omit; export function ButtonPrimary( props: { @@ -21,3 +23,33 @@ export function ButtonPrimary( ); } + +export const HoverButton = (props: { + icon: React.ReactNode; + label: string; + background: string; + text: string; + backgroundImage?: React.CSSProperties; + noLabelOnMobile?: boolean; +}) => { + return ( +
+
+ {props.icon} +
+ {props.label} +
+
+
+ ); +}; diff --git a/components/Cards.tsx b/components/Cards.tsx index bd9947c9..c6085de7 100644 --- a/components/Cards.tsx +++ b/components/Cards.tsx @@ -14,6 +14,7 @@ import { useToaster } from "./Toast"; import { ShareOptions } from "./ShareOptions"; import { MenuItem, Menu } from "./Layout"; import { useEntitySetContext } from "./EntitySetProvider"; +import { HomeButton } from "./HomeButton"; export function Cards(props: { rootCard: string }) { let openCards = useUIState((s) => s.openCards); @@ -34,10 +35,14 @@ export function Cards(props: { rootCard: string }) { e.currentTarget === e.target && blurCard(); }} > - -
- - + +
+
+ + +
+ +
diff --git a/components/HomeButton.tsx b/components/HomeButton.tsx new file mode 100644 index 00000000..640e31e3 --- /dev/null +++ b/components/HomeButton.tsx @@ -0,0 +1,20 @@ +import Link from "next/link"; +import { useEntitySetContext } from "./EntitySetProvider"; +import { HomeSmall } from "./Icons"; +import { HoverButton } from "./Buttons"; + +export function HomeButton() { + let entity_set = useEntitySetContext(); + if (!entity_set.permissions.write) return; + return ( + + + label="Go Home" + background="bg-accent-1" + text="text-accent-2" + /> + + ); +} diff --git a/components/Icons.tsx b/components/Icons.tsx index ffb57575..f106d8dc 100644 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -24,6 +24,26 @@ export const HomeMedium = (props: Props) => { // SMALL ICONS 24X24 +export const AddSmall = (props: Props) => { + return ( + + + + ); +}; + export const BlockSmall = (props: Props) => { return ( { ); }; +export const HomeSmall = (props: Props) => { + return ( + + + + ); +}; + +export const InfoSmall = (props: Props) => { + return ( + + + + ); +}; + export const LinkSmall = (props: Props) => { return ( { // TINY ICONS 16x16 +export const AddTiny = (props: Props) => { + return ( + + + + ); +}; export const ArrowRightTiny = (props: Props) => { return ( props.onClick(e)} - className="MenuItem z-10 text-left text-secondary py-1 px-3 flex gap-2 hover:bg-border-light hover:text-secondary " + className="MenuItem font-bold z-10 text-left text-secondary py-1 px-3 flex gap-2 hover:bg-border-light hover:text-secondary " > {props.children} diff --git a/components/MobileFooter.tsx b/components/MobileFooter.tsx index 53f1def2..b89794e7 100644 --- a/components/MobileFooter.tsx +++ b/components/MobileFooter.tsx @@ -4,6 +4,7 @@ import { Media } from "./Media"; import { ThemePopover } from "./ThemeManager/ThemeSetter"; import { TextToolbar } from "components/Toolbar"; import { ShareOptions } from "./ShareOptions"; +import { HomeButton } from "./HomeButton"; export function MobileFooter(props: { entityID: string }) { let focusedBlock = useUIState((s) => s.focusedBlock); @@ -18,9 +19,12 @@ export function MobileFooter(props: { entityID: string }) { />
) : ( -
- - +
+ +
+ + +
)} diff --git a/components/ShareOptions/index.tsx b/components/ShareOptions/index.tsx index ffdee5ca..5f6c6285 100644 --- a/components/ShareOptions/index.tsx +++ b/components/ShareOptions/index.tsx @@ -7,6 +7,7 @@ import { useSmoker } from "components/Toast"; import * as Popover from "@radix-ui/react-popover"; import { Menu, MenuItem } from "components/Layout"; import { theme } from "tailwind.config"; +import { HoverButton } from "components/Buttons"; export function ShareOptions(props: { rootEntity: string }) { let { permission_token } = useReplicache(); @@ -38,17 +39,20 @@ export function ShareOptions(props: { rootEntity: string }) { return ( -
-
- -
- Share -
-
-
+ + label="Share" + background="bg-accent-1" + text="text-accent-2" + />
- + { @@ -64,10 +68,10 @@ export function ShareOptions(props: { rootEntity: string }) { }} >
-
+
Publish
-
+
Share a read only version of this doc
@@ -84,10 +88,10 @@ export function ShareOptions(props: { rootEntity: string }) { }} >
-
+
Collaborate
-
+
Invite people to work together
diff --git a/components/ThemeManager/ThemeProvider.tsx b/components/ThemeManager/ThemeProvider.tsx index 373e4bab..838f52a0 100644 --- a/components/ThemeManager/ThemeProvider.tsx +++ b/components/ThemeManager/ThemeProvider.tsx @@ -43,17 +43,13 @@ function setCSSVariableToColor( } export function ThemeProvider(props: { entityID: string; + local?: boolean; children: React.ReactNode; }) { let bgPage = useColorAttribute(props.entityID, "theme/page-background"); let bgCard = useColorAttribute(props.entityID, "theme/card-background"); let primary = useColorAttribute(props.entityID, "theme/primary"); - let backgroundImage = useEntity(props.entityID, "theme/background-image"); - let backgroundImageRepeat = useEntity( - props.entityID, - "theme/background-image-repeat", - ); let highlight1 = useEntity(props.entityID, "theme/highlight-1"); let highlight2 = useColorAttribute(props.entityID, "theme/highlight-2"); let highlight3 = useColorAttribute(props.entityID, "theme/highlight-3"); @@ -69,6 +65,7 @@ export function ThemeProvider(props: { })[0]; useEffect(() => { + if (props.local) return; let el = document.querySelector(":root") as HTMLElement; if (!el) return; setCSSVariableToColor(el, "--bg-page", bgPage); @@ -102,6 +99,7 @@ export function ThemeProvider(props: { colorToString(accentContrast, "rgb"), ); }, [ + props.local, bgPage, bgCard, primary, @@ -114,14 +112,9 @@ export function ThemeProvider(props: { ]); return (
{ + let backgroundImage = useEntity(props.entityID, "theme/background-image"); + let backgroundImageRepeat = useEntity( + props.entityID, + "theme/background-image-repeat", + ); + return ( +
+ {props.children} +
+ ); +}; + function getColorContrast(color1: string, color2: string) { ColorSpace.register(sRGB); diff --git a/components/ThemeManager/ThemeSetter.tsx b/components/ThemeManager/ThemeSetter.tsx index f0a71933..151ca94f 100644 --- a/components/ThemeManager/ThemeSetter.tsx +++ b/components/ThemeManager/ThemeSetter.tsx @@ -20,6 +20,7 @@ import { useEffect, useMemo, useState } from "react"; import { BlockImageSmall, CloseContrastSmall, + PaintSmall, PopoverArrow, } from "components/Icons"; import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; @@ -34,6 +35,7 @@ import { Separator } from "components/Layout"; import { useEntitySetContext } from "components/EntitySetProvider"; import { isIOS, useViewportSize } from "@react-aria/utils"; import { onMouseDown } from "src/utils/iosInputMouseDown"; +import { HoverButton } from "components/Buttons"; export type pickers = | "null" @@ -58,7 +60,7 @@ export function setColorAttribute( data: { type: "color", value: colorToString(color, "hsba") }, }); } -export const ThemePopover = (props: { entityID: string }) => { +export const ThemePopover = (props: { entityID: string; home?: boolean }) => { let { rep } = useReplicache(); // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here. let pageValue = useColorAttribute(props.entityID, "theme/page-background"); @@ -76,7 +78,9 @@ export const ThemePopover = (props: { entityID: string }) => { "theme/background-image-repeat", ); - let [openPicker, setOpenPicker] = useState("null"); + let [openPicker, setOpenPicker] = useState( + props.home === true ? "page" : "null", + ); let set = useMemo(() => { return setColorAttribute(rep, props.entityID); }, [rep, props.entityID]); @@ -103,15 +107,16 @@ export const ThemePopover = (props: { entityID: string }) => { <> -
+ label="Theme" + background="bg-bg-card" + text="text-bg-card" + backgroundImage={{ backgroundColor: pageValue.toString("hex"), backgroundImage: gradient, }} /> - -
{ setValue={set("theme/page-background")} />
+
{ ? "cover" : `calc(${backgroundRepeat.data.value}px / 2 )`, }} - className="bg-bg-page mx-2 p-3 pb-0 mb-3 flex flex-col rounded-md border border-border" + className={`bg-bg-page mx-2 p-3 mb-3 flex flex-col rounded-md border border-border ${props.home ? "" : "pb-0"}`} > -
+
{
Example Button
- {/*
*/} -
-
-
- setOpenPicker("null")} - /> -
-
- setOpenPicker("null")} + {!props.home && ( + <> + {/*
*/} +
+
+
+ setOpenPicker("null")} + /> +
+
+ setOpenPicker("null")} + /> +
+
+
-
- -
-
-

Hello!

- - Welcome to{" "} - - Leaflet - - . It's a super easy and fun way to make, share, and - collab on little bits of paper - -
+
+

Hello!

+ + Welcome to{" "} + + Leaflet + + . It's a super easy and fun way to make, share, and + collab on little bits of paper + +
+ + )}
@@ -387,7 +399,9 @@ const BGPicker = (props: { let { rep } = useReplicache(); return ( -
+
diff --git a/drizzle/relations.ts b/drizzle/relations.ts index 69d279a8..bf9c923d 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm/relations"; -import { entity_sets, entities, permission_tokens, facts, permission_token_rights } from "./schema"; +import { entity_sets, entities, permission_tokens, identities, facts, permission_token_on_homepage, permission_token_rights } from "./schema"; export const entitiesRelations = relations(entities, ({one, many}) => ({ entity_set: one(entity_sets, { @@ -15,11 +15,21 @@ export const entity_setsRelations = relations(entity_sets, ({many}) => ({ permission_token_rights: many(permission_token_rights), })); +export const identitiesRelations = relations(identities, ({one, many}) => ({ + permission_token: one(permission_tokens, { + fields: [identities.home_page], + references: [permission_tokens.id] + }), + permission_token_on_homepages: many(permission_token_on_homepage), +})); + export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ + identities: many(identities), entity: one(entities, { fields: [permission_tokens.root_entity], references: [entities.id] }), + permission_token_on_homepages: many(permission_token_on_homepage), permission_token_rights: many(permission_token_rights), })); @@ -30,6 +40,17 @@ export const factsRelations = relations(facts, ({one}) => ({ }), })); +export const permission_token_on_homepageRelations = relations(permission_token_on_homepage, ({one}) => ({ + identity: one(identities, { + fields: [permission_token_on_homepage.identity], + references: [identities.id] + }), + permission_token: one(permission_tokens, { + fields: [permission_token_on_homepage.token], + references: [permission_tokens.id] + }), +})); + export const permission_token_rightsRelations = relations(permission_token_rights, ({one}) => ({ entity_set: one(entity_sets, { fields: [permission_token_rights.entity_set], diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 138d6a8c..3c73ba5e 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -31,6 +31,12 @@ export const entity_sets = pgTable("entity_sets", { created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), }); +export const identities = pgTable("identities", { + id: uuid("id").defaultRandom().primaryKey().notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + home_page: uuid("home_page").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), +}); + export const permission_tokens = pgTable("permission_tokens", { id: uuid("id").defaultRandom().primaryKey().notNull(), root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), @@ -47,6 +53,16 @@ export const facts = pgTable("facts", { version: bigint("version", { mode: "number" }).default(0).notNull(), }); +export const permission_token_on_homepage = pgTable("permission_token_on_homepage", { + token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), + identity: uuid("identity").notNull().references(() => identities.id, { onDelete: "cascade" } ), +}, +(table) => { + return { + permission_token_creator_pkey: primaryKey({ columns: [table.token, table.identity], name: "permission_token_creator_pkey"}), + } +}); + export const permission_token_rights = pgTable("permission_token_rights", { token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade", onUpdate: "cascade" } ), entity_set: uuid("entity_set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), diff --git a/package-lock.json b/package-lock.json index a860f42a..b67ab413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ "eslint-config-next": "14.2.3", "postcss": "^8.4.38", "prettier": "3.2.5", - "supabase": "^1.167.4", + "supabase": "^1.187.3", "tailwindcss": "^3.4.3", "typescript": "^5.5.3", "wrangler": "^3.56.0" @@ -10140,38 +10140,33 @@ } }, "node_modules/minizlib/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minizlib/node_modules/jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -10180,9 +10175,9 @@ } }, "node_modules/minizlib/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -10195,9 +10190,9 @@ } }, "node_modules/minizlib/node_modules/rimraf": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", - "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz", + "integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==", "dev": true, "dependencies": { "glob": "^10.3.7" @@ -10206,7 +10201,7 @@ "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14.18" + "node": "14 >=14.20 || 16 >=16.20 || >=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10667,6 +10662,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12185,16 +12186,16 @@ } }, "node_modules/supabase": { - "version": "1.169.8", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.169.8.tgz", - "integrity": "sha512-39vOiK2qZBZXUo0iWIRyJB1DLMk0kNb6iW166uvWRWkKWVaBTOErL510AdD1tVGn+sELsW7erl2vy9qLFmBX0Q==", + "version": "1.187.3", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.187.3.tgz", + "integrity": "sha512-44yzHpxMrd88cUKWw3GVPPIUx6oCqgfqmN4Mlxp7a7GeNNZSTe433vmKBcj+ue3E7K08XcIyEX6G1TjhVyto+g==", "dev": true, "hasInstallScript": true, "dependencies": { "bin-links": "^4.0.3", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", - "tar": "7.1.0" + "tar": "7.4.0" }, "bin": { "supabase": "bin/supabase" @@ -12273,14 +12274,14 @@ } }, "node_modules/tar": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.1.0.tgz", - "integrity": "sha512-ENhg4W6BmjYxl8GTaE7/h99f0aXiSWv4kikRZ9n2/JRxypZniE84ILZqimAhxxX7Zb8Px6pFdheW3EeHfhnXQQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", "dev": true, "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", - "minipass": "^7.1.0", + "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" diff --git a/package.json b/package.json index 6fbb7778..a43960dd 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "eslint-config-next": "14.2.3", "postcss": "^8.4.38", "prettier": "3.2.5", - "supabase": "^1.167.4", + "supabase": "^1.187.3", "tailwindcss": "^3.4.3", "typescript": "^5.5.3", "wrangler": "^3.56.0" diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 6378e30f..c215ecb9 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -113,6 +113,62 @@ export type Database = { }, ] } + identities: { + Row: { + created_at: string + home_page: string + id: string + } + Insert: { + created_at?: string + home_page: string + id?: string + } + Update: { + created_at?: string + home_page?: string + id?: string + } + Relationships: [ + { + foreignKeyName: "identities_home_page_fkey" + columns: ["home_page"] + isOneToOne: false + referencedRelation: "permission_tokens" + referencedColumns: ["id"] + }, + ] + } + permission_token_on_homepage: { + Row: { + identity: string + token: string + } + Insert: { + identity: string + token: string + } + Update: { + identity?: string + token?: string + } + Relationships: [ + { + foreignKeyName: "permission_token_creator_identity_fkey" + columns: ["identity"] + isOneToOne: false + referencedRelation: "identities" + referencedColumns: ["id"] + }, + { + foreignKeyName: "permission_token_creator_token_fkey" + columns: ["token"] + isOneToOne: false + referencedRelation: "permission_tokens" + referencedColumns: ["id"] + }, + ] + } permission_token_rights: { Row: { change_entity_set: boolean diff --git a/supabase/migrations/20240725190635_add identity tables.sql b/supabase/migrations/20240725190635_add identity tables.sql new file mode 100644 index 00000000..af2d41a9 --- /dev/null +++ b/supabase/migrations/20240725190635_add identity tables.sql @@ -0,0 +1,120 @@ +create table "public"."identities" ( + "id" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now(), + "home_page" uuid not null +); + + +alter table "public"."identities" enable row level security; + +create table "public"."permission_token_on_homepage" ( + "token" uuid not null, + "identity" uuid not null +); + + +alter table "public"."permission_token_on_homepage" enable row level security; + +CREATE UNIQUE INDEX identities_pkey ON public.identities USING btree (id); + +CREATE UNIQUE INDEX permission_token_creator_pkey ON public.permission_token_on_homepage USING btree (token, identity); + +alter table "public"."identities" add constraint "identities_pkey" PRIMARY KEY using index "identities_pkey"; + +alter table "public"."permission_token_on_homepage" add constraint "permission_token_creator_pkey" PRIMARY KEY using index "permission_token_creator_pkey"; + +alter table "public"."identities" add constraint "identities_home_page_fkey" FOREIGN KEY (home_page) REFERENCES permission_tokens(id) ON DELETE CASCADE not valid; + +alter table "public"."identities" validate constraint "identities_home_page_fkey"; + +alter table "public"."permission_token_on_homepage" add constraint "permission_token_creator_identity_fkey" FOREIGN KEY (identity) REFERENCES identities(id) ON DELETE CASCADE not valid; + +alter table "public"."permission_token_on_homepage" validate constraint "permission_token_creator_identity_fkey"; + +alter table "public"."permission_token_on_homepage" add constraint "permission_token_creator_token_fkey" FOREIGN KEY (token) REFERENCES permission_tokens(id) ON DELETE CASCADE not valid; + +alter table "public"."permission_token_on_homepage" validate constraint "permission_token_creator_token_fkey"; + +grant delete on table "public"."identities" to "anon"; + +grant insert on table "public"."identities" to "anon"; + +grant references on table "public"."identities" to "anon"; + +grant select on table "public"."identities" to "anon"; + +grant trigger on table "public"."identities" to "anon"; + +grant truncate on table "public"."identities" to "anon"; + +grant update on table "public"."identities" to "anon"; + +grant delete on table "public"."identities" to "authenticated"; + +grant insert on table "public"."identities" to "authenticated"; + +grant references on table "public"."identities" to "authenticated"; + +grant select on table "public"."identities" to "authenticated"; + +grant trigger on table "public"."identities" to "authenticated"; + +grant truncate on table "public"."identities" to "authenticated"; + +grant update on table "public"."identities" to "authenticated"; + +grant delete on table "public"."identities" to "service_role"; + +grant insert on table "public"."identities" to "service_role"; + +grant references on table "public"."identities" to "service_role"; + +grant select on table "public"."identities" to "service_role"; + +grant trigger on table "public"."identities" to "service_role"; + +grant truncate on table "public"."identities" to "service_role"; + +grant update on table "public"."identities" to "service_role"; + +grant delete on table "public"."permission_token_on_homepage" to "anon"; + +grant insert on table "public"."permission_token_on_homepage" to "anon"; + +grant references on table "public"."permission_token_on_homepage" to "anon"; + +grant select on table "public"."permission_token_on_homepage" to "anon"; + +grant trigger on table "public"."permission_token_on_homepage" to "anon"; + +grant truncate on table "public"."permission_token_on_homepage" to "anon"; + +grant update on table "public"."permission_token_on_homepage" to "anon"; + +grant delete on table "public"."permission_token_on_homepage" to "authenticated"; + +grant insert on table "public"."permission_token_on_homepage" to "authenticated"; + +grant references on table "public"."permission_token_on_homepage" to "authenticated"; + +grant select on table "public"."permission_token_on_homepage" to "authenticated"; + +grant trigger on table "public"."permission_token_on_homepage" to "authenticated"; + +grant truncate on table "public"."permission_token_on_homepage" to "authenticated"; + +grant update on table "public"."permission_token_on_homepage" to "authenticated"; + +grant delete on table "public"."permission_token_on_homepage" to "service_role"; + +grant insert on table "public"."permission_token_on_homepage" to "service_role"; + +grant references on table "public"."permission_token_on_homepage" to "service_role"; + +grant select on table "public"."permission_token_on_homepage" to "service_role"; + +grant trigger on table "public"."permission_token_on_homepage" to "service_role"; + +grant truncate on table "public"."permission_token_on_homepage" to "service_role"; + +grant update on table "public"."permission_token_on_homepage" to "service_role"; -- 2.51.2