From cc2e7dc22c0e360c3824525dc678c44e27027b21 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 30 Jul 2024 04:07:21 +0000 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 --- package-lock.json | 57 +++++++++++++++++++++++++++++---------------------------- package.json | 2 +- actions/createIdentity.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ actions/createNewDoc.ts | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ actions/deleteDoc.ts | 40 ++++++++++++++++++++++++++++++++++++++++ app/globals.css | 2 +- app/page.tsx | 81 --------------------------------------------------------------------------------- app/route.ts | 9 +++++++++ components/Buttons.tsx | 32 ++++++++++++++++++++++++++++++++ components/Cards.tsx | 13 +++++++++---- components/HomeButton.tsx | 20 ++++++++++++++++++++ components/Icons.tsx | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ components/Layout.tsx | 2 +- components/MobileFooter.tsx | 10 +++++++--- drizzle/relations.ts | 23 ++++++++++++++++++++++- drizzle/schema.ts | 16 ++++++++++++++++ supabase/database.types.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/[doc_id]/Doc.tsx | 25 +++++++++++++++---------- app/[doc_id]/page.tsx | 24 ++++++++++++++++-------- 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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ components/Blocks/CardBlock.tsx | 46 +++++++++++++++++++++++++++++++++++----------- components/ShareOptions/index.tsx | 30 +++++++++++++++++------------- components/ThemeManager/ThemeProvider.tsx | 42 +++++++++++++++++++++++++++++++----------- components/ThemeManager/ThemeSetter.tsx | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------- supabase/migrations/20240725190635_add identity tables.sql | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ components/Blocks/TextBlock/index.tsx | 2 +- 31 file(s) changed, 1209 insertion(s)(+), 236 deletion(s)(-) diff --git a/package-lock.json b/package-lock.json --- 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,37 +10140,32 @@ } }, "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" @@ -10666,6 +10661,12 @@ "funding": { "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", @@ -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 --- 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/actions/createIdentity.ts b/actions/createIdentity.ts new file mode 100644 --- /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 --- /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 --- /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/globals.css b/app/globals.css --- 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/page.tsx b/app/page.tsx deleted file mode 100644 --- 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 --- /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/Buttons.tsx b/components/Buttons.tsx --- 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 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 --- a/components/Cards.tsx +++ b/components/Cards.tsx @@ -14,6 +14,7 @@ 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 @@ e.currentTarget === e.target && blurCard(); }} > - -
- - + +
+
+ + +
+ +
diff --git a/components/HomeButton.tsx b/components/HomeButton.tsx new file mode 100644 --- /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 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -24,6 +24,26 @@ // SMALL ICONS 24X24 +export const AddSmall = (props: Props) => { + return ( + + + + ); +}; + export const BlockSmall = (props: Props) => { return ( { + return ( + + + + ); +}; + +export const InfoSmall = (props: Props) => { + return ( + + + + ); +}; + export const LinkSmall = (props: Props) => { return ( { + 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 --- a/components/MobileFooter.tsx +++ b/components/MobileFooter.tsx @@ -4,6 +4,7 @@ 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 @@ /> ) : ( -
- - +
+ +
+ + +
)} diff --git a/drizzle/relations.ts b/drizzle/relations.ts --- 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 @@ 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), })); @@ -27,6 +37,17 @@ entity: one(entities, { fields: [facts.entity], references: [entities.id] + }), +})); + +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] }), })); diff --git a/drizzle/schema.ts b/drizzle/schema.ts --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -31,6 +31,12 @@ 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" } ), @@ -45,6 +51,16 @@ updated_at: timestamp("updated_at", { mode: 'string' }), // You can use { mode: "bigint" } if numbers are exceeding js number limitations 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", { diff --git a/supabase/database.types.ts b/supabase/database.types.ts --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -113,6 +113,62 @@ }, ] } + 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/app/[doc_id]/Doc.tsx b/app/[doc_id]/Doc.tsx --- a/app/[doc_id]/Doc.tsx +++ b/app/[doc_id]/Doc.tsx @@ -4,7 +4,10 @@ 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 @@ > - - - - + + + + + + diff --git a/app/[doc_id]/page.tsx b/app/[doc_id]/page.tsx --- 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 @@ 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 @@
); + 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/home/DocOptions.tsx b/app/home/DocOptions.tsx new file mode 100644 --- /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 --- /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 --- /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 --- /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 --- /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 --- /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/components/Blocks/CardBlock.tsx b/components/Blocks/CardBlock.tsx --- 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 @@ ); } -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 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 @@ /> - {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 HeadingPreviewBlock(props: { entityID: string }) { +function HeadingPreviewBlock(props: { + entityID: string; + size?: "small" | "large"; +}) { let headingLevel = useEntity(props.entityID, "block/heading-level"); return ( -
+
); @@ -301,6 +319,12 @@ 1: "text-[6px] font-bold", 2: "text-[5px] font-bold ", 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 }) { diff --git a/components/ShareOptions/index.tsx b/components/ShareOptions/index.tsx --- a/components/ShareOptions/index.tsx +++ b/components/ShareOptions/index.tsx @@ -7,6 +7,7 @@ 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 @@ return ( -
-
- -
- Share -
-
-
+ + label="Share" + background="bg-accent-1" + text="text-accent-2" + />
- + { @@ -64,10 +68,10 @@ }} >
-
+
Publish
-
+
Share a read only version of this doc
@@ -84,10 +88,10 @@ }} >
-
+
Collaborate
-
+
Invite people to work together
diff --git a/components/ThemeManager/ThemeProvider.tsx b/components/ThemeManager/ThemeProvider.tsx --- a/components/ThemeManager/ThemeProvider.tsx +++ b/components/ThemeManager/ThemeProvider.tsx @@ -43,17 +43,13 @@ } 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 @@ })[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 @@ colorToString(accentContrast, "rgb"), ); }, [ + props.local, bgPage, bgCard, primary, @@ -114,14 +112,9 @@ ]); return (
); } + +export const ThemeBackgroundProvider = (props: { + entityID: string; + children: React.ReactNode; +}) => { + 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 --- a/components/ThemeManager/ThemeSetter.tsx +++ b/components/ThemeManager/ThemeSetter.tsx @@ -20,6 +20,7 @@ import { BlockImageSmall, CloseContrastSmall, + PaintSmall, PopoverArrow, } from "components/Icons"; import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; @@ -34,6 +35,7 @@ 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 @@ 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 @@ "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 @@ <> -
+ label="Theme" + background="bg-bg-card" + text="text-bg-card" + backgroundImage={{ backgroundColor: pageValue.toString("hex"), backgroundImage: gradient, }} /> - -
+
-
+
Example Button
- {/*
*/} -
-
-
- setOpenPicker("null")} + {!props.home && ( + <> + {/*
*/} +
+
+
+ setOpenPicker("null")} + /> +
+
+ 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 @@ let { rep } = useReplicache(); return ( -
+
diff --git a/supabase/migrations/20240725190635_add identity tables.sql b/supabase/migrations/20240725190635_add identity tables.sql new file mode 100644 --- /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"; diff --git a/components/Blocks/TextBlock/index.tsx b/components/Blocks/TextBlock/index.tsx --- a/components/Blocks/TextBlock/index.tsx +++ b/components/Blocks/TextBlock/index.tsx @@ -126,7 +126,7 @@ 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" : 
}
-- tangled.sh