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";