a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 <celine@hyperlink.academy>

* 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 <celine@hyperlink.academy>

authored by

Jared Pereira
celine
and committed by
GitHub
cc2e7dc2 12e98069

+1209 -236
+48
actions/createIdentity.ts
··· 1 + import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 2 + import { 3 + entities, 4 + permission_tokens, 5 + permission_token_rights, 6 + entity_sets, 7 + facts, 8 + identities, 9 + } from "drizzle/schema"; 10 + import { redirect } from "next/navigation"; 11 + import postgres from "postgres"; 12 + import { v7 } from "uuid"; 13 + import { sql } from "drizzle-orm"; 14 + import { cookies } from "next/headers"; 15 + export async function createIdentity(db: PostgresJsDatabase) { 16 + return db.transaction(async (tx) => { 17 + // Create a new entity set 18 + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); 19 + // Create a root-entity 20 + let [entity] = await tx 21 + .insert(entities) 22 + // And add it to that permission set 23 + .values({ set: entity_set.id, id: v7() }) 24 + .returning(); 25 + //Create a new permission token 26 + let [permissionToken] = await tx 27 + .insert(permission_tokens) 28 + .values({ root_entity: entity.id }) 29 + .returning(); 30 + //and give it all the permission on that entity set 31 + let [rights] = await tx 32 + .insert(permission_token_rights) 33 + .values({ 34 + token: permissionToken.id, 35 + entity_set: entity_set.id, 36 + read: true, 37 + write: true, 38 + create_token: true, 39 + change_entity_set: true, 40 + }) 41 + .returning(); 42 + let [identity] = await tx 43 + .insert(identities) 44 + .values({ home_page: permissionToken.id }) 45 + .returning(); 46 + return identity; 47 + }); 48 + }
+92
actions/createNewDoc.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/postgres-js"; 4 + import { 5 + entities, 6 + permission_tokens, 7 + permission_token_rights, 8 + entity_sets, 9 + facts, 10 + permission_token_on_homepage, 11 + } from "drizzle/schema"; 12 + import { redirect } from "next/navigation"; 13 + import postgres from "postgres"; 14 + import { v7 } from "uuid"; 15 + import { sql } from "drizzle-orm"; 16 + import { cookies } from "next/headers"; 17 + import { createIdentity } from "./createIdentity"; 18 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 19 + const db = drizzle(client); 20 + 21 + export async function createNewDoc() { 22 + let cookieStore = cookies(); 23 + let identity = cookieStore.get("identity")?.value; 24 + if (!identity) { 25 + let newIdentity = await createIdentity(db); 26 + cookieStore.set("identity", newIdentity.id, { sameSite: "strict" }); 27 + identity = newIdentity.id; 28 + } 29 + 30 + let { permissionToken } = await db.transaction(async (tx) => { 31 + // Create a new entity set 32 + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); 33 + // Create a root-entity 34 + let [entity] = await tx 35 + .insert(entities) 36 + // And add it to that permission set 37 + .values({ set: entity_set.id, id: v7() }) 38 + .returning(); 39 + //Create a new permission token 40 + let [permissionToken] = await tx 41 + .insert(permission_tokens) 42 + .values({ root_entity: entity.id }) 43 + .returning(); 44 + //and give it all the permission on that entity set 45 + let [rights] = await tx 46 + .insert(permission_token_rights) 47 + .values({ 48 + token: permissionToken.id, 49 + entity_set: entity_set.id, 50 + read: true, 51 + write: true, 52 + create_token: true, 53 + change_entity_set: true, 54 + }) 55 + .returning(); 56 + 57 + // and add it to created_by for the identity 58 + await tx 59 + .insert(permission_token_on_homepage) 60 + .values({ identity, token: permissionToken.id }); 61 + let [blockEntity] = await tx 62 + .insert(entities) 63 + // And add it to that permission set 64 + .values({ set: entity_set.id, id: v7() }) 65 + .returning(); 66 + 67 + await tx.insert(facts).values([ 68 + { 69 + id: v7(), 70 + entity: entity.id, 71 + attribute: "card/block", 72 + data: sql`${{ type: "ordered-reference", value: blockEntity.id, position: "a0" }}::jsonb`, 73 + }, 74 + { 75 + id: v7(), 76 + entity: blockEntity.id, 77 + attribute: "block/type", 78 + data: sql`${{ type: "block-type-union", value: "heading" }}::jsonb`, 79 + }, 80 + { 81 + id: v7(), 82 + entity: blockEntity.id, 83 + attribute: "block/heading-level", 84 + data: sql`${{ type: "number", value: 1 }}::jsonb`, 85 + }, 86 + ]); 87 + 88 + return { permissionToken, rights, entity, entity_set }; 89 + }); 90 + 91 + redirect(`/${permissionToken.id}?focusFirstBlock`); 92 + }
+40
actions/deleteDoc.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/postgres-js"; 4 + import { 5 + entities, 6 + permission_tokens, 7 + permission_token_rights, 8 + } from "drizzle/schema"; 9 + import { redirect } from "next/navigation"; 10 + import postgres from "postgres"; 11 + import { v7 } from "uuid"; 12 + import { eq, sql } from "drizzle-orm"; 13 + import { cookies } from "next/headers"; 14 + import { PermissionToken } from "src/replicache"; 15 + import { revalidatePath } from "next/cache"; 16 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 17 + const db = drizzle(client); 18 + 19 + export async function deleteDoc(permission_token: PermissionToken) { 20 + await db.transaction(async (tx) => { 21 + let [token] = await tx 22 + .select() 23 + .from(permission_tokens) 24 + .leftJoin( 25 + permission_token_rights, 26 + eq(permission_tokens.id, permission_token_rights.token), 27 + ) 28 + .where(eq(permission_tokens.id, permission_token.id)); 29 + 30 + console.log(token); 31 + if (!token.permission_token_rights?.write) return; 32 + await tx 33 + .delete(entities) 34 + .where(eq(entities.set, token.permission_token_rights.entity_set)); 35 + await tx 36 + .delete(permission_tokens) 37 + .where(eq(permission_tokens.id, permission_token.id)); 38 + }); 39 + return revalidatePath("/docs"); 40 + }
+15 -10
app/[doc_id]/Doc.tsx
··· 4 4 import { createServerClient } from "@supabase/ssr"; 5 5 import { SelectionManager } from "components/SelectionManager"; 6 6 import { Cards } from "components/Cards"; 7 - import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 7 + import { 8 + ThemeBackgroundProvider, 9 + ThemeProvider, 10 + } from "components/ThemeManager/ThemeProvider"; 8 11 import { MobileFooter } from "components/MobileFooter"; 9 12 import { PopUpProvider } from "components/Toast"; 10 13 import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; ··· 30 33 > 31 34 <PopUpProvider> 32 35 <ThemeProvider entityID={props.doc_id}> 33 - <UpdatePageTitle entityID={props.doc_id} /> 34 - <SelectionManager /> 35 - <div 36 - className="pageContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full" 37 - id="card-carousel" 38 - > 39 - <Cards rootCard={props.doc_id} /> 40 - </div> 41 - <MobileFooter entityID={props.doc_id} /> 36 + <ThemeBackgroundProvider entityID={props.doc_id}> 37 + <UpdatePageTitle entityID={props.doc_id} /> 38 + <SelectionManager /> 39 + <div 40 + className="pageContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full" 41 + id="card-carousel" 42 + > 43 + <Cards rootCard={props.doc_id} /> 44 + </div> 45 + <MobileFooter entityID={props.doc_id} /> 46 + </ThemeBackgroundProvider> 42 47 </ThemeProvider> 43 48 </PopUpProvider> 44 49 </EntitySetProvider>
+16 -8
app/[doc_id]/page.tsx
··· 1 - import { Metadata, ResolvingMetadata } from "next"; 1 + import { Metadata } from "next"; 2 2 import * as Y from "yjs"; 3 3 import * as base64 from "base64-js"; 4 4 5 - import { Fact, ReplicacheProvider } from "src/replicache"; 5 + import { Fact } from "src/replicache"; 6 6 import { Database } from "../../supabase/database.types"; 7 7 import { Attributes } from "src/replicache/attributes"; 8 8 import { createServerClient } from "@supabase/ssr"; 9 - import { SelectionManager } from "components/SelectionManager"; 10 - import { Cards } from "components/Cards"; 11 - import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 12 - import { MobileFooter } from "components/MobileFooter"; 13 - import { PopUpProvider } from "components/Toast"; 14 9 import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 15 10 import { Doc } from "./Doc"; 11 + import { cookies } from "next/headers"; 16 12 17 13 export const preferredRegion = ["sfo1"]; 18 14 export const dynamic = "force-dynamic"; ··· 30 26 export default async function DocumentPage(props: Props) { 31 27 let res = await supabase 32 28 .from("permission_tokens") 33 - .select("*, permission_token_rights(*)") 29 + .select("*, permission_token_rights(*), permission_token_on_homepage(*)") 34 30 .eq("id", props.params.doc_id) 35 31 .single(); 36 32 let rootEntity = res.data?.root_entity; ··· 51 47 </div> 52 48 </div> 53 49 ); 50 + let identity = cookies().get("identity"); 51 + if ( 52 + identity?.value && 53 + !res.data.permission_token_on_homepage.find( 54 + (f) => f.identity === identity.value, 55 + ) 56 + ) { 57 + await supabase.from("permission_token_on_homepage").insert({ 58 + identity: identity.value, 59 + token: res.data.id, 60 + }); 61 + } 54 62 let { data } = await supabase.rpc("get_facts", { 55 63 root: rootEntity, 56 64 });
+1 -1
app/globals.css
··· 58 58 59 59 h4 { 60 60 @apply text-base; 61 - @apply font-bold italic; 61 + @apply font-bold; 62 62 } 63 63 64 64 p {
+48
app/home/DocOptions.tsx
··· 1 + "use client"; 2 + import { PopoverArrow } from "components/Icons"; 3 + import { DeleteSmall, MoreOptionsTiny } from "components/Icons"; 4 + import * as Popover from "@radix-ui/react-popover"; 5 + import { Menu, MenuItem } from "components/Layout"; 6 + import { theme } from "tailwind.config"; 7 + import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; 8 + import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 9 + 10 + export const DocOptions = (props: { 11 + doc_id: string; 12 + setState: (s: "normal" | "deleting") => void; 13 + }) => { 14 + return ( 15 + <> 16 + <div className="absolute -bottom-6 sm:bottom-1 right-1 "> 17 + <Popover.Root> 18 + <Popover.Trigger className="bg-accent-1 text-accent-2 px-2 py-1 border border-accent-2 rounded-md w-fit place-self-end "> 19 + <MoreOptionsTiny className=" " /> 20 + </Popover.Trigger> 21 + <Popover.Anchor /> 22 + <Popover.Portal> 23 + <Popover.Content align="end"> 24 + <ThemeProvider entityID={props.doc_id} local> 25 + <Menu> 26 + <MenuItem 27 + onClick={(e) => { 28 + props.setState("deleting"); 29 + }} 30 + > 31 + <DeleteSmall /> 32 + Delete Doc 33 + </MenuItem> 34 + </Menu> 35 + <Popover.Arrow asChild width={16} height={8} viewBox="0 0 16 8"> 36 + <PopoverArrow 37 + arrowFill={theme.colors["bg-card"]} 38 + arrowStroke={theme.colors["border"]} 39 + /> 40 + </Popover.Arrow> 41 + </ThemeProvider> 42 + </Popover.Content> 43 + </Popover.Portal> 44 + </Popover.Root> 45 + </div> 46 + </> 47 + ); 48 + };
+106
app/home/DocPreview.tsx
··· 1 + "use client"; 2 + import { BlockPreview, CardPreview } from "components/Blocks/CardBlock"; 3 + import { 4 + ThemeBackgroundProvider, 5 + ThemeProvider, 6 + } from "components/ThemeManager/ThemeProvider"; 7 + import { useRef, useState } from "react"; 8 + import { Link } from "react-aria-components"; 9 + import { useBlocks } from "src/hooks/queries/useBlocks"; 10 + import { PermissionToken } from "src/replicache"; 11 + import { DocOptions } from "./DocOptions"; 12 + import { deleteDoc } from "actions/deleteDoc"; 13 + 14 + export const DocPreview = (props: { 15 + token: PermissionToken; 16 + doc_id: string; 17 + }) => { 18 + let [state, setState] = useState<"normal" | "deleting">("normal"); 19 + return ( 20 + <div className="relative h-40"> 21 + <ThemeProvider local entityID={props.doc_id}> 22 + {state === "normal" ? ( 23 + <Link 24 + href={"/" + props.token.id} 25 + className={`no-underline text-primary h-full`} 26 + > 27 + <div className="rounded-lg overflow-clip border border-border bg-bg-page grow w-full h-full"> 28 + <ThemeBackgroundProvider entityID={props.doc_id}> 29 + <div className="docPreview grow shrink-0 h-full w-full px-2 pt-2 sm:px-3 sm:pt-3 flex items-end"> 30 + <div 31 + className="docContentWrapper w-full h-full max-w-48 mx-auto border border-border-light border-b-0 rounded-t-md px-1 pt-1 sm:px-[6px] sm:pt-2 overflow-clip" 32 + style={{ 33 + backgroundColor: 34 + "rgba(var(--bg-card), var(--bg-card-alpha))", 35 + }} 36 + > 37 + <DocContent entityID={props.doc_id} /> 38 + </div> 39 + </div> 40 + </ThemeBackgroundProvider> 41 + </div> 42 + </Link> 43 + ) : ( 44 + <div className="docPreview grow shrink-0 w-full flex items-end"> 45 + <div 46 + className="docContentWrapper w-full h-full border border-border-light border-b-0 rounded-lg px-1 pt-1 sm:px-[6px] sm:pt-2 overflow-clip flex flex-col gap-2 place-items-center justify-center items-center " 47 + style={{ 48 + backgroundColor: "rgba(var(--bg-card), var(--bg-card-alpha))", 49 + }} 50 + > 51 + <div className="font-bold text-lg">Delete this Page?</div> 52 + <div className="flex gap-2"> 53 + <button 54 + className="bg-accent-1 text-accent-2 px-2 py-1 rounded-md " 55 + onMouseDown={(e) => { 56 + e.stopPropagation(); 57 + e.preventDefault(); 58 + deleteDoc(props.token); 59 + }} 60 + > 61 + Delete 62 + </button> 63 + <button 64 + className="text-accent-1" 65 + onMouseDown={(e) => { 66 + e.stopPropagation(); 67 + e.preventDefault(); 68 + setState("normal"); 69 + }} 70 + > 71 + Nevermind 72 + </button> 73 + </div> 74 + </div> 75 + </div> 76 + )} 77 + {state === "normal" && ( 78 + <DocOptions doc_id={props.doc_id} setState={setState} /> 79 + )} 80 + </ThemeProvider> 81 + </div> 82 + ); 83 + }; 84 + 85 + const DocContent = (props: { entityID: string }) => { 86 + let blocks = useBlocks(props.entityID); 87 + let previewRef = useRef<HTMLDivElement | null>(null); 88 + 89 + return ( 90 + <div 91 + ref={previewRef} 92 + className={`cardBlockPreview w-full h-full overflow-clip flex flex-col gap-0.5 no-underline `} 93 + > 94 + {blocks.slice(0, 10).map((b) => { 95 + return ( 96 + <BlockPreview 97 + previewRef={previewRef} 98 + {...b} 99 + key={b.factID} 100 + size="large" 101 + /> 102 + ); 103 + })} 104 + </div> 105 + ); 106 + };
+43
app/home/HomeHelp.tsx
··· 1 + "use client"; 2 + import { InfoSmall, PopoverArrow } from "components/Icons"; 3 + import { HoverButton } from "components/Buttons"; 4 + import * as Popover from "@radix-ui/react-popover"; 5 + 6 + export const HomeHelp = () => { 7 + return ( 8 + <Popover.Root> 9 + <Popover.Trigger> 10 + <HoverButton 11 + icon=<InfoSmall /> 12 + label="Info" 13 + background="bg-accent-1" 14 + text="text-accent-2" 15 + /> 16 + </Popover.Trigger> 17 + <Popover.Portal> 18 + <Popover.Content 19 + className="z-20 bg-white border border-[#CCCCCC] text-[#595959] rounded-md text-sm max-w-sm p-2" 20 + align="center" 21 + sideOffset={4} 22 + collisionPadding={16} 23 + > 24 + <div className="flex flex-col gap-2"> 25 + <div> 26 + These docs are saved using cookies,{" "} 27 + <strong> 28 + if you clear your cookies you will lose access to them. 29 + </strong> 30 + </div> 31 + <div> 32 + Please <a href="mailto:contact@hyperlink.academy">contact us</a>{" "} 33 + and we&apos;ll help recover them! 34 + </div> 35 + </div> 36 + <Popover.Arrow asChild width={16} height={8} viewBox="0 0 16 8"> 37 + <PopoverArrow arrowFill="#FFFFFF" arrowStroke="#CCCCCC" /> 38 + </Popover.Arrow> 39 + </Popover.Content> 40 + </Popover.Portal> 41 + </Popover.Root> 42 + ); 43 + };
+13
app/home/IdentitySetter.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect } from "react"; 4 + 5 + export function IdentitySetter(props: { 6 + cb: () => Promise<void>; 7 + call: boolean; 8 + }) { 9 + useEffect(() => { 10 + if (props.call) props.cb(); 11 + }, [props]); 12 + return null; 13 + }
+106
app/home/icon.tsx
··· 1 + import { ImageResponse } from "next/og"; 2 + import { Fact } from "src/replicache"; 3 + import { Attributes } from "src/replicache/attributes"; 4 + import { Database } from "../../supabase/database.types"; 5 + import { createServerClient } from "@supabase/ssr"; 6 + import { parseHSBToRGB } from "src/utils/parseHSB"; 7 + import { cookies } from "next/headers"; 8 + 9 + // Route segment config 10 + export const revalidate = 0; 11 + export const preferredRegion = ["sfo1"]; 12 + export const dynamic = "force-dynamic"; 13 + export const fetchCache = "force-no-store"; 14 + 15 + // Image metadata 16 + export const size = { 17 + width: 32, 18 + height: 32, 19 + }; 20 + export const contentType = "image/png"; 21 + 22 + // Image generation 23 + let supabase = createServerClient<Database>( 24 + process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 25 + process.env.SUPABASE_SERVICE_ROLE_KEY as string, 26 + { cookies: {} }, 27 + ); 28 + export default async function Icon() { 29 + let cookieStore = cookies(); 30 + let identity = cookieStore.get("identity"); 31 + let rootEntity: string | null = null; 32 + if (identity) { 33 + let res = await supabase 34 + .from("identities") 35 + .select( 36 + `*, 37 + permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)), 38 + permission_token_on_homepage( 39 + *, permission_tokens(*, permission_token_rights(*)) 40 + ) 41 + `, 42 + ) 43 + .eq("id", identity?.value) 44 + .single(); 45 + rootEntity = res.data?.permission_tokens?.root_entity || null; 46 + } 47 + let outlineColor, fillColor; 48 + if (rootEntity) { 49 + let { data } = await supabase.rpc("get_facts", { 50 + root: rootEntity, 51 + }); 52 + let initialFacts = 53 + (data as unknown as Fact<keyof typeof Attributes>[]) || []; 54 + let themeCardBG = initialFacts.find( 55 + (f) => f.attribute === "theme/card-background", 56 + ) as Fact<"theme/card-background"> | undefined; 57 + 58 + let themePrimary = initialFacts.find( 59 + (f) => f.attribute === "theme/primary", 60 + ) as Fact<"theme/primary"> | undefined; 61 + 62 + outlineColor = parseHSBToRGB(`hsba(${themeCardBG?.data.value})`); 63 + 64 + fillColor = parseHSBToRGB(`hsba(${themePrimary?.data.value})`); 65 + } 66 + 67 + return new ImageResponse( 68 + ( 69 + // ImageResponse JSX element 70 + <div style={{ display: "flex" }}> 71 + <svg 72 + width="32" 73 + height="32" 74 + viewBox="0 0 32 32" 75 + fill="none" 76 + xmlns="http://www.w3.org/2000/svg" 77 + > 78 + {/* outline */} 79 + <path 80 + fillRule="evenodd" 81 + clipRule="evenodd" 82 + d="M3.09628 21.8809C2.1044 23.5376 1.19806 25.3395 0.412496 27.2953C-0.200813 28.8223 0.539843 30.5573 2.06678 31.1706C3.59372 31.7839 5.32873 31.0433 5.94204 29.5163C6.09732 29.1297 6.24696 28.7489 6.39151 28.3811L6.39286 28.3777C6.94334 26.9769 7.41811 25.7783 7.99246 24.6987C8.63933 24.6636 9.37895 24.6582 10.2129 24.6535L10.3177 24.653C11.8387 24.6446 13.6711 24.6345 15.2513 24.3147C16.8324 23.9947 18.789 23.2382 19.654 21.2118C19.8881 20.6633 20.1256 19.8536 19.9176 19.0311C19.98 19.0311 20.044 19.031 20.1096 19.031C20.1447 19.031 20.1805 19.0311 20.2169 19.0311C21.0513 19.0316 22.2255 19.0324 23.2752 18.7469C24.5 18.4137 25.7878 17.6248 26.3528 15.9629C26.557 15.3624 26.5948 14.7318 26.4186 14.1358C26.4726 14.1262 26.528 14.1165 26.5848 14.1065C26.6121 14.1018 26.6398 14.0969 26.6679 14.092C27.3851 13.9667 28.3451 13.7989 29.1653 13.4921C29.963 13.1936 31.274 12.5268 31.6667 10.9987C31.8906 10.1277 31.8672 9.20568 31.3642 8.37294C31.1551 8.02669 30.889 7.75407 30.653 7.55302C30.8728 7.27791 31.1524 6.89517 31.345 6.47292C31.6791 5.74032 31.8513 4.66394 31.1679 3.61078C30.3923 2.4155 29.0623 2.2067 28.4044 2.1526C27.7203 2.09635 26.9849 2.15644 26.4564 2.2042C26.3846 2.02839 26.2858 1.84351 26.1492 1.66106C25.4155 0.681263 24.2775 0.598914 23.6369 0.61614C22.3428 0.650943 21.3306 1.22518 20.5989 1.82076C20.2149 2.13334 19.8688 2.48545 19.5698 2.81786C18.977 2.20421 18.1625 1.90193 17.3552 1.77751C15.7877 1.53594 14.5082 2.58853 13.6056 3.74374C12.4805 5.18375 11.7295 6.8566 10.7361 8.38059C10.3814 8.14984 9.83685 7.89945 9.16529 7.93065C8.05881 7.98204 7.26987 8.73225 6.79424 9.24551C5.96656 10.1387 5.46273 11.5208 5.10424 12.7289C4.71615 14.0368 4.38077 15.5845 4.06569 17.1171C3.87054 18.0664 3.82742 18.5183 4.01638 20.2489C3.43705 21.1826 3.54993 21.0505 3.09628 21.8809Z" 83 + fill={outlineColor ? outlineColor : "#FFFFFF"} 84 + /> 85 + 86 + {/* fill */} 87 + <path 88 + fillRule="evenodd" 89 + clipRule="evenodd" 90 + d="M9.86889 10.2435C10.1927 10.528 10.5723 10.8615 11.3911 10.5766C11.9265 10.3903 12.6184 9.17682 13.3904 7.82283C14.5188 5.84367 15.8184 3.56431 17.0505 3.7542C18.5368 3.98325 18.4453 4.80602 18.3749 5.43886C18.3255 5.88274 18.2866 6.23317 18.8098 6.21972C19.3427 6.20601 19.8613 5.57971 20.4632 4.8529C21.2945 3.84896 22.2847 2.65325 23.6906 2.61544C24.6819 2.58879 24.6663 3.01595 24.6504 3.44913C24.6403 3.72602 24.63 4.00537 24.8826 4.17024C25.1314 4.33266 25.7571 4.2759 26.4763 4.21065C27.6294 4.10605 29.023 3.97963 29.4902 4.6995C29.9008 5.33235 29.3776 5.96135 28.8762 6.56423C28.4514 7.07488 28.0422 7.56679 28.2293 8.02646C28.3819 8.40149 28.6952 8.61278 29.0024 8.81991C29.5047 9.15866 29.9905 9.48627 29.7297 10.5009C29.4539 11.5737 27.7949 11.8642 26.2398 12.1366C24.937 12.3647 23.7072 12.5801 23.4247 13.2319C23.2475 13.6407 23.5414 13.8311 23.8707 14.0444C24.2642 14.2992 24.7082 14.5869 24.4592 15.3191C23.8772 17.031 21.9336 17.031 20.1095 17.0311C18.5438 17.0311 17.0661 17.0311 16.6131 18.1137C16.3515 18.7387 16.7474 18.849 17.1818 18.9701C17.7135 19.1183 18.3029 19.2826 17.8145 20.4267C16.8799 22.6161 13.3934 22.6357 10.2017 22.6536C9.03136 22.6602 7.90071 22.6665 6.95003 22.7795C6.84152 22.7924 6.74527 22.8547 6.6884 22.948C5.81361 24.3834 5.19318 25.9622 4.53139 27.6462C4.38601 28.0162 4.23862 28.3912 4.08611 28.7709C3.88449 29.2729 3.31413 29.5163 2.81217 29.3147C2.31021 29.1131 2.06673 28.5427 2.26834 28.0408C3.01927 26.1712 3.88558 24.452 4.83285 22.8739C6.37878 20.027 9.42621 16.5342 12.6488 13.9103C15.5162 11.523 18.2544 9.73614 21.4413 8.38026C21.8402 8.21054 21.7218 7.74402 21.3053 7.86437C18.4789 8.68119 15.9802 10.3013 13.3904 11.9341C10.5735 13.71 8.21288 16.1115 6.76027 17.8575C6.50414 18.1653 5.94404 17.9122 6.02468 17.5199C6.65556 14.4512 7.30668 11.6349 8.26116 10.605C9.16734 9.62708 9.47742 9.8995 9.86889 10.2435Z" 91 + fill={fillColor ? fillColor : "#272727"} 92 + /> 93 + </svg> 94 + </div> 95 + ), 96 + // ImageResponse options 97 + { 98 + // For convenience, we can re-use the exported icons size metadata 99 + // config to also set the ImageResponse's width and height. 100 + ...size, 101 + headers: { 102 + "Cache-Control": "no-cache", 103 + }, 104 + }, 105 + ); 106 + }
+120
app/home/page.tsx
··· 1 + import { AddTiny } from "components/Icons"; 2 + import { cookies } from "next/headers"; 3 + import { Fact, ReplicacheProvider } from "src/replicache"; 4 + import { createServerClient } from "@supabase/ssr"; 5 + import { Database } from "supabase/database.types"; 6 + import { DocPreview } from "./DocPreview"; 7 + import { Attributes } from "src/replicache/attributes"; 8 + import { 9 + ThemeBackgroundProvider, 10 + ThemeProvider, 11 + } from "components/ThemeManager/ThemeProvider"; 12 + import { EntitySetProvider } from "components/EntitySetProvider"; 13 + import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 14 + import { createNewDoc } from "actions/createNewDoc"; 15 + import { createIdentity } from "actions/createIdentity"; 16 + import postgres from "postgres"; 17 + import { drizzle } from "drizzle-orm/postgres-js"; 18 + import { IdentitySetter } from "./IdentitySetter"; 19 + import { HoverButton } from "components/Buttons"; 20 + import { HomeHelp } from "./HomeHelp"; 21 + 22 + let supabase = createServerClient<Database>( 23 + process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 24 + process.env.SUPABASE_SERVICE_ROLE_KEY as string, 25 + { cookies: {} }, 26 + ); 27 + export default async function Home() { 28 + let cookieStore = cookies(); 29 + let identity = cookieStore.get("identity")?.value; 30 + let needstosetcookie = false; 31 + if (!identity) { 32 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 33 + const db = drizzle(client); 34 + let newIdentity = await createIdentity(db); 35 + client.end(); 36 + identity = newIdentity.id; 37 + needstosetcookie = true; 38 + } 39 + 40 + async function setCookie() { 41 + "use server"; 42 + 43 + cookies().set("identity", identity as string, { sameSite: "strict" }); 44 + } 45 + 46 + let res = await supabase 47 + .from("identities") 48 + .select( 49 + `*, 50 + permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)), 51 + permission_token_on_homepage( 52 + *, permission_tokens(*, permission_token_rights(*)) 53 + ) 54 + `, 55 + ) 56 + .eq("id", identity) 57 + .single(); 58 + if (!res.data) return <div>{JSON.stringify(res.error)}</div>; 59 + let docs = res.data.permission_token_on_homepage 60 + .map((d) => d.permission_tokens) 61 + .filter((d) => d !== null); 62 + if (!res.data.permission_tokens) return <div>no home page wierdly</div>; 63 + let { data } = await supabase.rpc("get_facts", { 64 + root: res.data.permission_tokens?.root_entity, 65 + }); 66 + let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 67 + let root_entity = res.data.permission_tokens.root_entity; 68 + return ( 69 + <ReplicacheProvider 70 + rootEntity={root_entity} 71 + token={res.data.permission_tokens} 72 + name={root_entity} 73 + initialFacts={initialFacts} 74 + > 75 + <IdentitySetter cb={setCookie} call={needstosetcookie} /> 76 + <EntitySetProvider 77 + set={res.data.permission_tokens.permission_token_rights[0].entity_set} 78 + > 79 + <ThemeProvider entityID={root_entity}> 80 + <div className="flex h-full bg-bg-page"> 81 + <ThemeBackgroundProvider entityID={root_entity}> 82 + <div className="home relative max-w-screen-lg w-full h-full mx-auto flex sm:flex-row flex-col-reverse sm:gap-4 px-2 sm:px-6 "> 83 + <div className="homeOptions z-10 shrink-0 sm:static absolute bottom-0 place-self-end sm:place-self-start flex sm:flex-col flex-row-reverse gap-2 sm:w-fit w-full items-center pb-2 pt-1 sm:pt-7"> 84 + <form action={createNewDoc}> 85 + <button className="contents"> 86 + <HoverButton 87 + icon=<AddTiny className="m-1 shrink-0" /> 88 + label="Create New" 89 + background="bg-accent-1" 90 + text="text-accent-2" 91 + /> 92 + </button> 93 + </form> 94 + <HomeHelp /> 95 + 96 + <ThemePopover entityID={root_entity} home /> 97 + </div> 98 + <div className="homeDocGrid grow w-full h-full overflow-y-scroll no-scrollbar pt-3 pb-28 sm:pt-6 sm:pb-12 "> 99 + <div className="grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-8 gap-x-4 sm:gap-6 grow"> 100 + {docs.map((doc) => ( 101 + <ReplicacheProvider 102 + key={doc.id} 103 + rootEntity={doc.root_entity} 104 + token={doc} 105 + name={doc.root_entity} 106 + initialFacts={[]} 107 + > 108 + <DocPreview token={doc} doc_id={doc.root_entity} /> 109 + </ReplicacheProvider> 110 + ))} 111 + </div> 112 + </div> 113 + </div> 114 + </ThemeBackgroundProvider> 115 + </div> 116 + </ThemeProvider> 117 + </EntitySetProvider> 118 + </ReplicacheProvider> 119 + ); 120 + }
-81
app/page.tsx
··· 1 - import { drizzle } from "drizzle-orm/postgres-js"; 2 - import { 3 - entities, 4 - permission_tokens, 5 - permission_token_rights, 6 - entity_sets, 7 - facts, 8 - } from "drizzle/schema"; 9 - import { redirect } from "next/navigation"; 10 - import postgres from "postgres"; 11 - import { v7 } from "uuid"; 12 - import { sql } from "drizzle-orm"; 13 - const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 14 - const db = drizzle(client); 15 - 16 - export const preferredRegion = ["sfo1"]; 17 - export const dynamic = "force-dynamic"; 18 - export const fetchCache = "force-no-store"; 19 - 20 - export default async function RootPage() { 21 - // Creating a new document 22 - let { permissionToken, rights, entity, entity_set } = await db.transaction( 23 - async (tx) => { 24 - // Create a new entity set 25 - let [entity_set] = await tx.insert(entity_sets).values({}).returning(); 26 - // Create a root-entity 27 - let [entity] = await tx 28 - .insert(entities) 29 - // And add it to that permission set 30 - .values({ set: entity_set.id, id: v7() }) 31 - .returning(); 32 - //Create a new permission token 33 - let [permissionToken] = await tx 34 - .insert(permission_tokens) 35 - .values({ root_entity: entity.id }) 36 - .returning(); 37 - //and give it all the permission on that entity set 38 - let [rights] = await tx 39 - .insert(permission_token_rights) 40 - .values({ 41 - token: permissionToken.id, 42 - entity_set: entity_set.id, 43 - read: true, 44 - write: true, 45 - create_token: true, 46 - change_entity_set: true, 47 - }) 48 - .returning(); 49 - let [blockEntity] = await tx 50 - .insert(entities) 51 - // And add it to that permission set 52 - .values({ set: entity_set.id, id: v7() }) 53 - .returning(); 54 - 55 - await tx.insert(facts).values([ 56 - { 57 - id: v7(), 58 - entity: entity.id, 59 - attribute: "card/block", 60 - data: sql`${{ type: "ordered-reference", value: blockEntity.id, position: "a0" }}::jsonb`, 61 - }, 62 - { 63 - id: v7(), 64 - entity: blockEntity.id, 65 - attribute: "block/type", 66 - data: sql`${{ type: "block-type-union", value: "heading" }}::jsonb`, 67 - }, 68 - { 69 - id: v7(), 70 - entity: blockEntity.id, 71 - attribute: "block/heading-level", 72 - data: sql`${{ type: "number", value: 1 }}::jsonb`, 73 - }, 74 - ]); 75 - 76 - return { permissionToken, rights, entity, entity_set }; 77 - }, 78 - ); 79 - 80 - redirect(`/${permissionToken.id}?focusFirstBlock`); 81 - }
+9
app/route.ts
··· 1 + import { createNewDoc } from "actions/createNewDoc"; 2 + 3 + export const preferredRegion = ["sfo1"]; 4 + export const dynamic = "force-dynamic"; 5 + export const fetchCache = "force-no-store"; 6 + 7 + export async function GET() { 8 + await createNewDoc(); 9 + }
+35 -11
components/Blocks/CardBlock.tsx
··· 1 + "use client"; 1 2 import { Block, BlockProps, focusBlock, ListMarker } from "components/Blocks"; 2 3 import { focusCard } from "components/Cards"; 3 4 import { useEntity, useReplicache } from "src/replicache"; ··· 193 194 ); 194 195 } 195 196 196 - function CardPreview(props: { entityID: string }) { 197 + export function CardPreview(props: { entityID: string }) { 197 198 let blocks = useBlocks(props.entityID); 198 199 let previewRef = useRef<HTMLDivElement | null>(null); 199 200 ··· 209 210 ); 210 211 } 211 212 212 - function BlockPreview( 213 - b: Block & { previewRef: React.RefObject<HTMLDivElement> }, 213 + export function BlockPreview( 214 + b: Block & { 215 + previewRef: React.RefObject<HTMLDivElement>; 216 + size?: "small" | "large"; 217 + }, 214 218 ) { 215 219 let headingLevel = useEntity(b.value, "block/heading-level")?.data.value; 216 220 let ref = useRef<HTMLDivElement | null>(null); ··· 256 260 /> 257 261 </div> 258 262 259 - {isVisible && <PreviewBlockContent {...b} />} 263 + {isVisible && <PreviewBlockContent {...b} size={b.size} />} 260 264 </div> 261 265 ); 262 266 return ( 263 267 <div ref={ref}> 264 - {isVisible && <PreviewBlockContent {...b} key={b.factID} />} 268 + {isVisible && <PreviewBlockContent {...b} key={b.factID} size={b.size} />} 265 269 </div> 266 270 ); 267 271 } 268 272 269 - function PreviewBlockContent(props: Block) { 273 + function PreviewBlockContent(props: Block & { size?: "small" | "large" }) { 270 274 switch (props.type) { 271 275 case "text": { 272 276 return ( 273 - <div style={{ fontSize: "4px" }}> 277 + <div style={{ fontSize: `${props.size === "large" ? "6px" : "4px"}` }}> 274 278 <RenderedTextBlock entityID={props.value} className="p-0" /> 275 279 </div> 276 280 ); 277 281 } 282 + case "link": { 283 + return ( 284 + <div className="w-full h-5 shrink-0 rounded-md border border-border-light" /> 285 + ); 286 + } 278 287 case "heading": 279 - return <HeadingPreviewBlock entityID={props.value} />; 288 + return <HeadingPreviewBlock entityID={props.value} size={props.size} />; 280 289 case "card": 281 290 return ( 282 - <div className="w-full h-4 shrink-0 rounded-md border border-border-light" /> 291 + <div className="w-full h-5 shrink-0 rounded-md border border-border-light" /> 283 292 ); 284 293 case "image": 285 294 return <ImagePreviewBlock entityID={props.value} />; ··· 288 297 } 289 298 } 290 299 291 - function HeadingPreviewBlock(props: { entityID: string }) { 300 + function HeadingPreviewBlock(props: { 301 + entityID: string; 302 + size?: "small" | "large"; 303 + }) { 292 304 let headingLevel = useEntity(props.entityID, "block/heading-level"); 293 305 return ( 294 - <div className={HeadingStyle[headingLevel?.data.value || 1]}> 306 + <div 307 + className={ 308 + props.size === "large" 309 + ? LargeHeadingStyle[headingLevel?.data.value || 1] 310 + : HeadingStyle[headingLevel?.data.value || 1] 311 + } 312 + > 295 313 <RenderedTextBlock entityID={props.entityID} className="p-0 " /> 296 314 </div> 297 315 ); ··· 301 319 1: "text-[6px] font-bold", 302 320 2: "text-[5px] font-bold ", 303 321 3: "text-[4px] font-bold italic text-secondary ", 322 + } as { [level: number]: string }; 323 + 324 + const LargeHeadingStyle = { 325 + 1: "text-[9px] font-bold", 326 + 2: "text-[7px] font-bold ", 327 + 3: "text-[6px] font-bold italic text-secondary ", 304 328 } as { [level: number]: string }; 305 329 306 330 function ImagePreviewBlock(props: { entityID: string }) {
+1 -1
components/Blocks/TextBlock/index.tsx
··· 126 126 if (!initialFact) 127 127 // show a blank line if the block is empty. blocks with content are styled elsewhere! update both! 128 128 return ( 129 - <pre className={`${props.className}`}> 129 + <pre className={`${props.className} italic text-tertiary`}> 130 130 {/* Render a placeholder if there are no other blocks in the card, else just show the blank line*/} 131 131 {props.first ? "Title" : <br />} 132 132 </pre>
+32
components/Buttons.tsx
··· 1 + import React from "react"; 2 + 1 3 type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">; 2 4 export function ButtonPrimary( 3 5 props: { ··· 21 23 </button> 22 24 ); 23 25 } 26 + 27 + export const HoverButton = (props: { 28 + icon: React.ReactNode; 29 + label: string; 30 + background: string; 31 + text: string; 32 + backgroundImage?: React.CSSProperties; 33 + noLabelOnMobile?: boolean; 34 + }) => { 35 + return ( 36 + <div className="sm:w-8 sm:h-8 relative "> 37 + <div 38 + className={` 39 + z-10 group/hover-button 40 + w-max h-max rounded-full p-1 flex gap-2 41 + sm:absolute top-0 left-0 42 + place-items-center justify-center 43 + ${props.background} ${props.text}`} 44 + style={props.backgroundImage} 45 + > 46 + {props.icon} 47 + <div 48 + className={`font-bold pr-[6px] group-hover/hover-button:block ${props.noLabelOnMobile ? "hidden" : "sm:hidden"}`} 49 + > 50 + {props.label} 51 + </div> 52 + </div> 53 + </div> 54 + ); 55 + };
+9 -4
components/Cards.tsx
··· 14 14 import { ShareOptions } from "./ShareOptions"; 15 15 import { MenuItem, Menu } from "./Layout"; 16 16 import { useEntitySetContext } from "./EntitySetProvider"; 17 + import { HomeButton } from "./HomeButton"; 17 18 18 19 export function Cards(props: { rootCard: string }) { 19 20 let openCards = useUIState((s) => s.openCards); ··· 34 35 e.currentTarget === e.target && blurCard(); 35 36 }} 36 37 > 37 - <Media mobile={false}> 38 - <div className="flex flex-col justify-center gap-2 mr-4 mt-2"> 39 - <ShareOptions rootEntity={props.rootCard} /> 40 - <PageOptions entityID={props.rootCard} /> 38 + <Media mobile={false} className="h-full"> 39 + <div className="flex flex-col h-full justify-between mr-4 mt-1"> 40 + <div className="flex flex-col justify-center gap-2 "> 41 + <ShareOptions rootEntity={props.rootCard} /> 42 + <PageOptions entityID={props.rootCard} /> 43 + <hr className="text-border my-3" /> 44 + <HomeButton /> 45 + </div> 41 46 </div> 42 47 </Media> 43 48 </div>
+20
components/HomeButton.tsx
··· 1 + import Link from "next/link"; 2 + import { useEntitySetContext } from "./EntitySetProvider"; 3 + import { HomeSmall } from "./Icons"; 4 + import { HoverButton } from "./Buttons"; 5 + 6 + export function HomeButton() { 7 + let entity_set = useEntitySetContext(); 8 + if (!entity_set.permissions.write) return; 9 + return ( 10 + <Link href="/home"> 11 + <HoverButton 12 + noLabelOnMobile 13 + icon=<HomeSmall /> 14 + label="Go Home" 15 + background="bg-accent-1" 16 + text="text-accent-2" 17 + /> 18 + </Link> 19 + ); 20 + }
+79
components/Icons.tsx
··· 24 24 25 25 // SMALL ICONS 24X24 26 26 27 + export const AddSmall = (props: Props) => { 28 + return ( 29 + <svg 30 + width="24" 31 + height="24" 32 + viewBox="0 0 24 24" 33 + fill="none" 34 + xmlns="http://www.w3.org/2000/svg" 35 + {...props} 36 + > 37 + <path 38 + fillRule="evenodd" 39 + clipRule="evenodd" 40 + d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM18.6569 12C18.6569 12.5523 18.2091 13 17.6569 13H13V17.6569C13 18.2091 12.5523 18.6569 12 18.6569C11.4477 18.6569 11 18.2091 11 17.6569V13H6.34315C5.79086 13 5.34315 12.5523 5.34315 12C5.34315 11.4477 5.79086 11 6.34315 11H11L11 6.34314C11 5.79086 11.4477 5.34315 12 5.34315C12.5523 5.34315 13 5.79086 13 6.34315V11H17.6569C18.2091 11 18.6569 11.4477 18.6569 12Z" 41 + fill="currentColor" 42 + /> 43 + </svg> 44 + ); 45 + }; 46 + 27 47 export const BlockSmall = (props: Props) => { 28 48 return ( 29 49 <svg ··· 150 170 ); 151 171 }; 152 172 173 + export const HomeSmall = (props: Props) => { 174 + return ( 175 + <svg 176 + width="24" 177 + height="24" 178 + viewBox="0 0 24 24" 179 + fill="none" 180 + xmlns="http://www.w3.org/2000/svg" 181 + {...props} 182 + > 183 + <path 184 + fillRule="evenodd" 185 + clipRule="evenodd" 186 + d="M18.7066 1.65454C18.3926 1.51114 18.0218 1.64942 17.8784 1.9634C17.8089 2.11556 17.8056 2.28108 17.8563 2.42758C17.85 2.43434 17.8456 2.43822 17.844 2.43967C17.7408 2.50967 17.6154 2.61065 17.5169 2.74933C17.4138 2.89456 17.3166 3.11886 17.3652 3.38982C17.412 3.65115 17.573 3.83231 17.7183 3.94652C17.8637 4.06083 18.041 4.14714 18.2284 4.21706C18.3145 4.2492 18.3824 4.27985 18.4345 4.30764L18.4258 4.31721C18.4258 4.31721 18.4243 4.31876 18.4213 4.32147C18.4182 4.32435 18.4135 4.3284 18.4067 4.33377C18.3924 4.34523 18.3738 4.35873 18.3449 4.37865L18.3253 4.39198C18.2748 4.42626 18.1786 4.49152 18.0989 4.56396C17.9946 4.65876 17.8596 4.81464 17.804 5.04782C17.7485 5.28088 17.7936 5.50019 17.87 5.68345C18.0027 6.0021 18.3686 6.15282 18.6872 6.02011C18.9424 5.91384 19.0899 5.65804 19.0704 5.39672C19.1399 5.34841 19.2466 5.27096 19.3447 5.16465C19.5076 4.98797 19.6493 4.78073 19.7161 4.54056C19.7881 4.28148 19.7625 4.01854 19.641 3.78406C19.4757 3.46487 19.1687 3.2667 18.8882 3.13789C18.9696 3.02693 19.0348 2.90823 19.0802 2.79358C19.1293 2.66937 19.1798 2.48891 19.1581 2.2888C19.1331 2.0588 19.0038 1.79027 18.7066 1.65454ZM15.5641 4.19457C15.3529 3.92421 14.9852 3.83164 14.6711 3.96978L7.26728 7.22658C7.19603 7.25793 7.13008 7.30018 7.07182 7.35181L1.32232 12.4474C1.01233 12.7221 0.98375 13.1961 1.25848 13.5061C1.53322 13.8161 2.00723 13.8447 2.31722 13.57L2.33722 13.5522V18.9057C2.33722 19.215 2.52717 19.4927 2.81552 19.6047L5.39115 20.6058C5.56538 20.6735 5.75637 20.673 5.92742 20.6085L5.92897 20.6112L5.94654 20.601C5.99519 20.5811 6.04209 20.556 6.08636 20.5257C6.09904 20.517 6.1114 20.508 6.12344 20.4986L8.31377 19.2311V21.0113C8.31377 21.3204 8.50338 21.5978 8.79135 21.7101L12.0554 22.9825C12.2574 23.0613 12.4836 23.0488 12.6757 22.9482L20.6991 18.7473C20.9463 18.6179 21.1012 18.3619 21.1012 18.0828V13.7295L21.1012 13.7248L21.6403 13.4842C21.8511 13.3902 22.0068 13.2044 22.0626 12.9805C22.1183 12.7566 22.0679 12.5194 21.9258 12.3376C21.5519 11.859 21.2022 11.3379 20.7921 10.8902V7.34595C20.7921 6.93173 20.4563 6.59595 20.0421 6.59595H17.066C16.6518 6.59595 16.316 6.93173 16.316 7.34595V9.39619C16.316 9.8104 16.6518 10.1462 17.066 10.1462C17.4802 10.1462 17.816 9.8104 17.816 9.39619V8.09595H19.2921V11.0584C19.2921 11.3559 19.4862 11.6314 19.7162 11.9458L20.1532 12.5052L13.646 15.408L8.7016 8.23437L14.7412 5.57768L15.3052 6.29968C15.5602 6.62609 16.0316 6.68397 16.358 6.42897C16.6844 6.17396 16.7423 5.70263 16.4873 5.37622L15.5641 4.19457ZM8.31377 17.7868L6.41285 18.8869V16.6879C6.41285 16.3214 6.54125 16.0013 6.70702 15.8103C6.86035 15.6337 7.00539 15.5976 7.14764 15.6302C7.39512 15.6869 7.6903 15.9316 7.94479 16.4091C8.19138 16.8716 8.31377 17.3994 8.31377 17.7463V17.7868ZM19.6012 14.3939L13.6878 17.0318C13.3578 17.179 12.9698 17.07 12.7648 16.7725L7.4326 9.03638L3.83622 12.2237C3.83688 12.2366 3.83722 12.2496 3.83722 12.2627V18.3925L4.91285 18.8106V16.6879C4.91285 16.0011 5.14607 15.3204 5.57419 14.8271C6.01475 14.3195 6.69585 13.9878 7.48268 14.1681C8.34767 14.3663 8.92679 15.0625 9.26848 15.7035C9.61808 16.3593 9.81377 17.1276 9.81377 17.7463V20.4987L11.6843 21.2279V18.5704C11.6843 18.2252 11.9641 17.9454 12.3093 17.9454C12.6544 17.9454 12.9343 18.2252 12.9343 18.5704V21.1196L19.6012 17.629V14.3939Z" 187 + fill="currentColor" 188 + /> 189 + </svg> 190 + ); 191 + }; 192 + 193 + export const InfoSmall = (props: Props) => { 194 + return ( 195 + <svg 196 + width="24" 197 + height="24" 198 + viewBox="0 0 24 24" 199 + fill="none" 200 + xmlns="http://www.w3.org/2000/svg" 201 + {...props} 202 + > 203 + <path 204 + fillRule="evenodd" 205 + clipRule="evenodd" 206 + d="M20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12ZM22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM11.6031 8.38389C11.8058 8.56522 12.1471 8.65589 12.6271 8.65589C13.1071 8.65589 13.4645 8.56522 13.6991 8.38389C13.9338 8.20255 14.0778 7.94122 14.1311 7.59989C14.1631 7.40789 14.1845 7.26922 14.1951 7.18389C14.2058 7.08789 14.2111 7.01322 14.2111 6.95989C14.2111 6.68255 14.1098 6.45322 13.9071 6.27189C13.7151 6.07989 13.3791 5.98389 12.8991 5.98389C12.4191 5.98389 12.0618 6.07455 11.8271 6.25589C11.5925 6.43722 11.4485 6.69855 11.3951 7.03989C11.3631 7.23189 11.3418 7.37589 11.3311 7.47189C11.3205 7.55722 11.3151 7.62655 11.3151 7.67989C11.3151 7.95722 11.4111 8.19189 11.6031 8.38389ZM10.4831 17.6799C10.8138 18.0212 11.3098 18.1919 11.9711 18.1919C12.5258 18.1919 13.0378 18.0372 13.5071 17.7279C13.9871 17.4186 14.4991 16.9599 15.0431 16.3519L16.0191 15.2319L14.7391 14.0959L12.4991 16.3839L12.2271 16.3039L13.4591 9.74389H8.59514L8.29114 11.5679H10.7871L10.1471 14.9919C10.0831 15.3226 10.0405 15.5732 10.0191 15.7439C9.9978 15.9146 9.98714 16.0906 9.98714 16.2719C9.98714 16.8586 10.1525 17.3279 10.4831 17.6799Z" 207 + fill="currentColor" 208 + /> 209 + </svg> 210 + ); 211 + }; 212 + 153 213 export const LinkSmall = (props: Props) => { 154 214 return ( 155 215 <svg ··· 212 272 213 273 // TINY ICONS 16x16 214 274 275 + export const AddTiny = (props: Props) => { 276 + return ( 277 + <svg 278 + width="16" 279 + height="16" 280 + viewBox="0 0 16 16" 281 + fill="none" 282 + xmlns="http://www.w3.org/2000/svg" 283 + {...props} 284 + > 285 + <path 286 + fillRule="evenodd" 287 + clipRule="evenodd" 288 + d="M8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0ZM8 2.5C8.41421 2.5 8.75 2.83579 8.75 3.25V7.25H12.75C13.1642 7.25 13.5 7.58579 13.5 8C13.5 8.41421 13.1642 8.75 12.75 8.75H8.75V12.75C8.75 13.1642 8.41421 13.5 8 13.5C7.58579 13.5 7.25 13.1642 7.25 12.75V8.75H3.25C2.83579 8.75 2.5 8.41421 2.5 8C2.5 7.58579 2.83579 7.25 3.25 7.25H7.25V3.25C7.25 2.83579 7.58579 2.5 8 2.5Z" 289 + fill="currentColor" 290 + /> 291 + </svg> 292 + ); 293 + }; 215 294 export const ArrowRightTiny = (props: Props) => { 216 295 return ( 217 296 <svg
+1 -1
components/Layout.tsx
··· 19 19 return ( 20 20 <button 21 21 onClick={(e) => props.onClick(e)} 22 - className="MenuItem z-10 text-left text-secondary py-1 px-3 flex gap-2 hover:bg-border-light hover:text-secondary " 22 + className="MenuItem font-bold z-10 text-left text-secondary py-1 px-3 flex gap-2 hover:bg-border-light hover:text-secondary " 23 23 > 24 24 {props.children} 25 25 </button>
+7 -3
components/MobileFooter.tsx
··· 4 4 import { ThemePopover } from "./ThemeManager/ThemeSetter"; 5 5 import { TextToolbar } from "components/Toolbar"; 6 6 import { ShareOptions } from "./ShareOptions"; 7 + import { HomeButton } from "./HomeButton"; 7 8 8 9 export function MobileFooter(props: { entityID: string }) { 9 10 let focusedBlock = useUIState((s) => s.focusedBlock); ··· 18 19 /> 19 20 </div> 20 21 ) : ( 21 - <div className="z-10 pb-2 px-2 flex gap-[6px] items-center justify-end"> 22 - <ThemePopover entityID={props.entityID} /> 23 - <ShareOptions rootEntity={props.entityID} /> 22 + <div className="z-10 pb-2 px-2 flex justify-between"> 23 + <HomeButton /> 24 + <div className="flex flex-row gap-[6px] items-center "> 25 + <ThemePopover entityID={props.entityID} /> 26 + <ShareOptions rootEntity={props.entityID} /> 27 + </div> 24 28 </div> 25 29 )} 26 30 </Media>
+17 -13
components/ShareOptions/index.tsx
··· 7 7 import * as Popover from "@radix-ui/react-popover"; 8 8 import { Menu, MenuItem } from "components/Layout"; 9 9 import { theme } from "tailwind.config"; 10 + import { HoverButton } from "components/Buttons"; 10 11 11 12 export function ShareOptions(props: { rootEntity: string }) { 12 13 let { permission_token } = useReplicache(); ··· 38 39 return ( 39 40 <Popover.Root> 40 41 <Popover.Trigger> 41 - <div className="sm:w-8 sm:h-8 relative"> 42 - <div className="z-10 group/share sm:absolute top-0 left-0 rounded-full w-fit h-max bg-accent-1 text-accent-2 flex gap-2 p-1 place-items-center justify-center"> 43 - <ShareSmall /> 44 - <div className="font-bold pr-[6px] sm:hidden group-hover/share:block"> 45 - Share 46 - </div> 47 - </div> 48 - </div> 42 + <HoverButton 43 + icon=<ShareSmall /> 44 + label="Share" 45 + background="bg-accent-1" 46 + text="text-accent-2" 47 + /> 49 48 </Popover.Trigger> 50 49 <Popover.Portal> 51 - <Popover.Content align="center" sideOffset={4} collisionPadding={16}> 50 + <Popover.Content 51 + className="z-20" 52 + align="center" 53 + sideOffset={4} 54 + collisionPadding={16} 55 + > 52 56 <Menu> 53 57 <MenuItem 54 58 onClick={(e) => { ··· 64 68 }} 65 69 > 66 70 <div className="group/publish"> 67 - <div className="font-bold group-hover/publish:text-accent-contrast"> 71 + <div className=" group-hover/publish:text-accent-contrast"> 68 72 Publish 69 73 </div> 70 - <div className="text-sm text-tertiary group-hover/publish:text-accent-contrast"> 74 + <div className="text-sm font-normal text-tertiary group-hover/publish:text-accent-contrast"> 71 75 Share a read only version of this doc 72 76 </div> 73 77 </div> ··· 84 88 }} 85 89 > 86 90 <div className="group/collab"> 87 - <div className="font-bold group-hover/collab:text-accent-contrast"> 91 + <div className="group-hover/collab:text-accent-contrast"> 88 92 Collaborate 89 93 </div> 90 - <div className="text-sm text-tertiary group-hover/collab:text-accent-contrast"> 94 + <div className="text-sm font-bold text-tertiary group-hover/collab:text-accent-contrast"> 91 95 Invite people to work together 92 96 </div> 93 97 </div>
+31 -11
components/ThemeManager/ThemeProvider.tsx
··· 43 43 } 44 44 export function ThemeProvider(props: { 45 45 entityID: string; 46 + local?: boolean; 46 47 children: React.ReactNode; 47 48 }) { 48 49 let bgPage = useColorAttribute(props.entityID, "theme/page-background"); 49 50 let bgCard = useColorAttribute(props.entityID, "theme/card-background"); 50 51 let primary = useColorAttribute(props.entityID, "theme/primary"); 51 52 52 - let backgroundImage = useEntity(props.entityID, "theme/background-image"); 53 - let backgroundImageRepeat = useEntity( 54 - props.entityID, 55 - "theme/background-image-repeat", 56 - ); 57 53 let highlight1 = useEntity(props.entityID, "theme/highlight-1"); 58 54 let highlight2 = useColorAttribute(props.entityID, "theme/highlight-2"); 59 55 let highlight3 = useColorAttribute(props.entityID, "theme/highlight-3"); ··· 69 65 })[0]; 70 66 71 67 useEffect(() => { 68 + if (props.local) return; 72 69 let el = document.querySelector(":root") as HTMLElement; 73 70 if (!el) return; 74 71 setCSSVariableToColor(el, "--bg-page", bgPage); ··· 102 99 colorToString(accentContrast, "rgb"), 103 100 ); 104 101 }, [ 102 + props.local, 105 103 bgPage, 106 104 bgCard, 107 105 primary, ··· 114 112 ]); 115 113 return ( 116 114 <div 117 - className="pageWrapper w-full bg-bg-page text-primary h-full flex flex-col bg-cover bg-center bg-no-repeat items-stretch" 115 + className="pageWrapper w-full text-primary h-full flex flex-col bg-center items-stretch" 118 116 style={ 119 117 { 120 - backgroundImage: `url(${backgroundImage?.data.src}), url(${backgroundImage?.data.fallback})`, 121 - backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 122 - backgroundSize: !backgroundImageRepeat 123 - ? "cover" 124 - : backgroundImageRepeat?.data.value, 125 118 "--bg-page": colorToString(bgPage, "rgb"), 126 119 "--bg-card": colorToString(bgCard, "rgb"), 127 120 "--bg-card-alpha": bgCard.getChannelValue("alpha"), ··· 141 134 </div> 142 135 ); 143 136 } 137 + 138 + export const ThemeBackgroundProvider = (props: { 139 + entityID: string; 140 + children: React.ReactNode; 141 + }) => { 142 + let backgroundImage = useEntity(props.entityID, "theme/background-image"); 143 + let backgroundImageRepeat = useEntity( 144 + props.entityID, 145 + "theme/background-image-repeat", 146 + ); 147 + return ( 148 + <div 149 + className="pageBackgroundWrapper w-full bg-bg-page text-primary h-full flex flex-col bg-cover bg-center bg-no-repeat items-stretch" 150 + style={ 151 + { 152 + backgroundImage: `url(${backgroundImage?.data.src}), url(${backgroundImage?.data.fallback})`, 153 + backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 154 + backgroundSize: !backgroundImageRepeat 155 + ? "cover" 156 + : backgroundImageRepeat?.data.value, 157 + } as CSSProperties 158 + } 159 + > 160 + {props.children} 161 + </div> 162 + ); 163 + }; 144 164 145 165 function getColorContrast(color1: string, color2: string) { 146 166 ColorSpace.register(sRGB);
+76 -62
components/ThemeManager/ThemeSetter.tsx
··· 20 20 import { 21 21 BlockImageSmall, 22 22 CloseContrastSmall, 23 + PaintSmall, 23 24 PopoverArrow, 24 25 } from "components/Icons"; 25 26 import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; ··· 34 35 import { useEntitySetContext } from "components/EntitySetProvider"; 35 36 import { isIOS, useViewportSize } from "@react-aria/utils"; 36 37 import { onMouseDown } from "src/utils/iosInputMouseDown"; 38 + import { HoverButton } from "components/Buttons"; 37 39 38 40 export type pickers = 39 41 | "null" ··· 58 60 data: { type: "color", value: colorToString(color, "hsba") }, 59 61 }); 60 62 } 61 - export const ThemePopover = (props: { entityID: string }) => { 63 + export const ThemePopover = (props: { entityID: string; home?: boolean }) => { 62 64 let { rep } = useReplicache(); 63 65 // 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. 64 66 let pageValue = useColorAttribute(props.entityID, "theme/page-background"); ··· 76 78 "theme/background-image-repeat", 77 79 ); 78 80 79 - let [openPicker, setOpenPicker] = useState<pickers>("null"); 81 + let [openPicker, setOpenPicker] = useState<pickers>( 82 + props.home === true ? "page" : "null", 83 + ); 80 84 let set = useMemo(() => { 81 85 return setColorAttribute(rep, props.entityID); 82 86 }, [rep, props.entityID]); ··· 103 107 <> 104 108 <Popover.Root> 105 109 <Popover.Trigger> 106 - <div 107 - className="rounded-full w-7 h-7 border border-border" 108 - style={{ 110 + <HoverButton 111 + icon=<PaintSmall /> 112 + label="Theme" 113 + background="bg-bg-card" 114 + text="text-bg-card" 115 + backgroundImage={{ 109 116 backgroundColor: pageValue.toString("hex"), 110 117 backgroundImage: gradient, 111 118 }} 112 119 /> 113 - 114 - <div className="relative z-10"></div> 115 120 </Popover.Trigger> 116 121 <Popover.Portal> 117 122 <Popover.Content ··· 132 137 setValue={set("theme/page-background")} 133 138 /> 134 139 </div> 140 + 135 141 <div 136 142 style={{ 137 143 backgroundImage: `url(${backgroundImage?.data.src})`, ··· 140 146 ? "cover" 141 147 : `calc(${backgroundRepeat.data.value}px / 2 )`, 142 148 }} 143 - className="bg-bg-page mx-2 p-3 pb-0 mb-3 flex flex-col rounded-md border border-border" 149 + className={`bg-bg-page mx-2 p-3 mb-3 flex flex-col rounded-md border border-border ${props.home ? "" : "pb-0"}`} 144 150 > 145 - <div className="flex flex-col mt-4 -mb-[6px] z-10"> 151 + <div className={`flex flex-col z-10 mt-4 -mb-[6px] `}> 146 152 <div 147 153 className="themePageControls text-accent-2 flex flex-col gap-2 h-full bg-bg-page p-2 rounded-md border border-accent-2 shadow-[0_0_0_1px_rgb(var(--accent))]" 148 - style={{ backgroundColor: "rgba(var(--accent-1), 0.6)" }} 154 + style={{ 155 + backgroundColor: "rgba(var(--accent-1), 0.6)", 156 + }} 149 157 > 150 158 <ColorPicker 151 159 label="Accent" ··· 176 184 <div className="font-bold relative text-center text-lg py-2 rounded-md bg-accent-1 text-accent-2 shadow-md"> 177 185 Example Button 178 186 </div> 179 - {/* <hr className="my-3" /> */} 180 - <div className="flex flex-col pt-8 -mb-[6px] z-10"> 181 - <div 182 - className="themePageControls flex flex-col gap-2 h-full text-primary bg-bg-page p-2 rounded-md border border-primary shadow-[0_0_0_1px_rgb(var(--bg-card))]" 183 - style={{ backgroundColor: "rgba(var(--bg-card), 0.6)" }} 184 - > 185 - <div className="themePageColor flex items-start "> 186 - <ColorPicker 187 - label="Page" 188 - alpha 189 - value={cardValue} 190 - setValue={set("theme/card-background")} 191 - thisPicker={"card"} 192 - openPicker={openPicker} 193 - setOpenPicker={setOpenPicker} 194 - closePicker={() => setOpenPicker("null")} 195 - /> 196 - </div> 197 - <div className="themePageTextColor w-full flex pr-2 items-start"> 198 - <ColorPicker 199 - label="Text" 200 - value={primaryValue} 201 - setValue={set("theme/primary")} 202 - thisPicker={"text"} 203 - openPicker={openPicker} 204 - setOpenPicker={setOpenPicker} 205 - closePicker={() => setOpenPicker("null")} 187 + {!props.home && ( 188 + <> 189 + {/* <hr className="my-3" /> */} 190 + <div className="flex flex-col pt-8 -mb-[6px] z-10"> 191 + <div 192 + className="themePageControls flex flex-col gap-2 h-full text-primary bg-bg-page p-2 rounded-md border border-primary shadow-[0_0_0_1px_rgb(var(--bg-card))]" 193 + style={{ backgroundColor: "rgba(var(--bg-card), 0.6)" }} 194 + > 195 + <div className="themePageColor flex items-start "> 196 + <ColorPicker 197 + label="Page" 198 + alpha 199 + value={cardValue} 200 + setValue={set("theme/card-background")} 201 + thisPicker={"card"} 202 + openPicker={openPicker} 203 + setOpenPicker={setOpenPicker} 204 + closePicker={() => setOpenPicker("null")} 205 + /> 206 + </div> 207 + <div className="themePageTextColor w-full flex pr-2 items-start"> 208 + <ColorPicker 209 + label="Text" 210 + value={primaryValue} 211 + setValue={set("theme/primary")} 212 + thisPicker={"text"} 213 + openPicker={openPicker} 214 + setOpenPicker={setOpenPicker} 215 + closePicker={() => setOpenPicker("null")} 216 + /> 217 + </div> 218 + </div> 219 + <SectionArrow 220 + fill={theme.colors["primary"]} 221 + stroke={theme.colors["bg-card"]} 222 + className=" ml-2" 206 223 /> 207 224 </div> 208 - </div> 209 - <SectionArrow 210 - fill={theme.colors["primary"]} 211 - stroke={theme.colors["bg-card"]} 212 - className=" ml-2" 213 - /> 214 - </div> 215 225 216 - <div 217 - className="rounded-t-lg p-2 border border-border border-b-transparent shadow-md text-primary" 218 - style={{ 219 - backgroundColor: 220 - "rgba(var(--bg-card), var(--bg-card-alpha))", 221 - }} 222 - > 223 - <p className="font-bold">Hello!</p> 224 - <small className=""> 225 - Welcome to{" "} 226 - <span className="font-bold text-accent-contrast"> 227 - Leaflet 228 - </span> 229 - . It&apos;s a super easy and fun way to make, share, and 230 - collab on little bits of paper 231 - </small> 232 - </div> 226 + <div 227 + className="rounded-t-lg p-2 border border-border border-b-transparent shadow-md text-primary" 228 + style={{ 229 + backgroundColor: 230 + "rgba(var(--bg-card), var(--bg-card-alpha))", 231 + }} 232 + > 233 + <p className="font-bold">Hello!</p> 234 + <small className=""> 235 + Welcome to{" "} 236 + <span className="font-bold text-accent-contrast"> 237 + Leaflet 238 + </span> 239 + . It&apos;s a super easy and fun way to make, share, and 240 + collab on little bits of paper 241 + </small> 242 + </div> 243 + </> 244 + )} 233 245 </div> 234 246 </div> 235 247 <Popover.Arrow asChild width={16} height={8} viewBox="0 0 16 8"> ··· 387 399 let { rep } = useReplicache(); 388 400 389 401 return ( 390 - <div className="bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full px-2 pt-3"> 402 + <div 403 + className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full px-2 pt-3`} 404 + > 391 405 <div className="bgPickerBody w-full flex flex-col gap-2 p-2 border border-[#CCCCCC] rounded-md"> 392 406 <div className="bgPickerLabel flex justify-between place-items-center "> 393 407 <div className="bgPickerColorLabel flex gap-2 items-center">
+22 -1
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { entity_sets, entities, permission_tokens, facts, permission_token_rights } from "./schema"; 2 + import { entity_sets, entities, permission_tokens, identities, facts, permission_token_on_homepage, permission_token_rights } from "./schema"; 3 3 4 4 export const entitiesRelations = relations(entities, ({one, many}) => ({ 5 5 entity_set: one(entity_sets, { ··· 15 15 permission_token_rights: many(permission_token_rights), 16 16 })); 17 17 18 + export const identitiesRelations = relations(identities, ({one, many}) => ({ 19 + permission_token: one(permission_tokens, { 20 + fields: [identities.home_page], 21 + references: [permission_tokens.id] 22 + }), 23 + permission_token_on_homepages: many(permission_token_on_homepage), 24 + })); 25 + 18 26 export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 27 + identities: many(identities), 19 28 entity: one(entities, { 20 29 fields: [permission_tokens.root_entity], 21 30 references: [entities.id] 22 31 }), 32 + permission_token_on_homepages: many(permission_token_on_homepage), 23 33 permission_token_rights: many(permission_token_rights), 24 34 })); 25 35 ··· 27 37 entity: one(entities, { 28 38 fields: [facts.entity], 29 39 references: [entities.id] 40 + }), 41 + })); 42 + 43 + export const permission_token_on_homepageRelations = relations(permission_token_on_homepage, ({one}) => ({ 44 + identity: one(identities, { 45 + fields: [permission_token_on_homepage.identity], 46 + references: [identities.id] 47 + }), 48 + permission_token: one(permission_tokens, { 49 + fields: [permission_token_on_homepage.token], 50 + references: [permission_tokens.id] 30 51 }), 31 52 })); 32 53
+16
drizzle/schema.ts
··· 31 31 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 32 32 }); 33 33 34 + export const identities = pgTable("identities", { 35 + id: uuid("id").defaultRandom().primaryKey().notNull(), 36 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 37 + home_page: uuid("home_page").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), 38 + }); 39 + 34 40 export const permission_tokens = pgTable("permission_tokens", { 35 41 id: uuid("id").defaultRandom().primaryKey().notNull(), 36 42 root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), ··· 45 51 updated_at: timestamp("updated_at", { mode: 'string' }), 46 52 // You can use { mode: "bigint" } if numbers are exceeding js number limitations 47 53 version: bigint("version", { mode: "number" }).default(0).notNull(), 54 + }); 55 + 56 + export const permission_token_on_homepage = pgTable("permission_token_on_homepage", { 57 + token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), 58 + identity: uuid("identity").notNull().references(() => identities.id, { onDelete: "cascade" } ), 59 + }, 60 + (table) => { 61 + return { 62 + permission_token_creator_pkey: primaryKey({ columns: [table.token, table.identity], name: "permission_token_creator_pkey"}), 63 + } 48 64 }); 49 65 50 66 export const permission_token_rights = pgTable("permission_token_rights", {
+29 -28
package-lock.json
··· 62 62 "eslint-config-next": "14.2.3", 63 63 "postcss": "^8.4.38", 64 64 "prettier": "3.2.5", 65 - "supabase": "^1.167.4", 65 + "supabase": "^1.187.3", 66 66 "tailwindcss": "^3.4.3", 67 67 "typescript": "^5.5.3", 68 68 "wrangler": "^3.56.0" ··· 10140 10140 } 10141 10141 }, 10142 10142 "node_modules/minizlib/node_modules/glob": { 10143 - "version": "10.4.1", 10144 - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", 10145 - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", 10143 + "version": "10.4.5", 10144 + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 10145 + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 10146 10146 "dev": true, 10147 10147 "dependencies": { 10148 10148 "foreground-child": "^3.1.0", 10149 10149 "jackspeak": "^3.1.2", 10150 10150 "minimatch": "^9.0.4", 10151 10151 "minipass": "^7.1.2", 10152 + "package-json-from-dist": "^1.0.0", 10152 10153 "path-scurry": "^1.11.1" 10153 10154 }, 10154 10155 "bin": { 10155 10156 "glob": "dist/esm/bin.mjs" 10156 10157 }, 10157 - "engines": { 10158 - "node": ">=16 || 14 >=14.18" 10159 - }, 10160 10158 "funding": { 10161 10159 "url": "https://github.com/sponsors/isaacs" 10162 10160 } 10163 10161 }, 10164 10162 "node_modules/minizlib/node_modules/jackspeak": { 10165 - "version": "3.1.2", 10166 - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", 10167 - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", 10163 + "version": "3.4.3", 10164 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 10165 + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 10168 10166 "dev": true, 10169 10167 "dependencies": { 10170 10168 "@isaacs/cliui": "^8.0.2" 10171 10169 }, 10172 - "engines": { 10173 - "node": ">=14" 10174 - }, 10175 10170 "funding": { 10176 10171 "url": "https://github.com/sponsors/isaacs" 10177 10172 }, ··· 10180 10175 } 10181 10176 }, 10182 10177 "node_modules/minizlib/node_modules/minimatch": { 10183 - "version": "9.0.4", 10184 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", 10185 - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", 10178 + "version": "9.0.5", 10179 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 10180 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 10186 10181 "dev": true, 10187 10182 "dependencies": { 10188 10183 "brace-expansion": "^2.0.1" ··· 10195 10190 } 10196 10191 }, 10197 10192 "node_modules/minizlib/node_modules/rimraf": { 10198 - "version": "5.0.7", 10199 - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", 10200 - "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", 10193 + "version": "5.0.9", 10194 + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz", 10195 + "integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==", 10201 10196 "dev": true, 10202 10197 "dependencies": { 10203 10198 "glob": "^10.3.7" ··· 10206 10201 "rimraf": "dist/esm/bin.mjs" 10207 10202 }, 10208 10203 "engines": { 10209 - "node": ">=14.18" 10204 + "node": "14 >=14.20 || 16 >=16.20 || >=18" 10210 10205 }, 10211 10206 "funding": { 10212 10207 "url": "https://github.com/sponsors/isaacs" ··· 10666 10661 "funding": { 10667 10662 "url": "https://github.com/sponsors/sindresorhus" 10668 10663 } 10664 + }, 10665 + "node_modules/package-json-from-dist": { 10666 + "version": "1.0.0", 10667 + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", 10668 + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", 10669 + "dev": true 10669 10670 }, 10670 10671 "node_modules/parent-module": { 10671 10672 "version": "1.0.1", ··· 12185 12186 } 12186 12187 }, 12187 12188 "node_modules/supabase": { 12188 - "version": "1.169.8", 12189 - "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.169.8.tgz", 12190 - "integrity": "sha512-39vOiK2qZBZXUo0iWIRyJB1DLMk0kNb6iW166uvWRWkKWVaBTOErL510AdD1tVGn+sELsW7erl2vy9qLFmBX0Q==", 12189 + "version": "1.187.3", 12190 + "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.187.3.tgz", 12191 + "integrity": "sha512-44yzHpxMrd88cUKWw3GVPPIUx6oCqgfqmN4Mlxp7a7GeNNZSTe433vmKBcj+ue3E7K08XcIyEX6G1TjhVyto+g==", 12191 12192 "dev": true, 12192 12193 "hasInstallScript": true, 12193 12194 "dependencies": { 12194 12195 "bin-links": "^4.0.3", 12195 12196 "https-proxy-agent": "^7.0.2", 12196 12197 "node-fetch": "^3.3.2", 12197 - "tar": "7.1.0" 12198 + "tar": "7.4.0" 12198 12199 }, 12199 12200 "bin": { 12200 12201 "supabase": "bin/supabase" ··· 12273 12274 } 12274 12275 }, 12275 12276 "node_modules/tar": { 12276 - "version": "7.1.0", 12277 - "resolved": "https://registry.npmjs.org/tar/-/tar-7.1.0.tgz", 12278 - "integrity": "sha512-ENhg4W6BmjYxl8GTaE7/h99f0aXiSWv4kikRZ9n2/JRxypZniE84ILZqimAhxxX7Zb8Px6pFdheW3EeHfhnXQQ==", 12277 + "version": "7.4.0", 12278 + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", 12279 + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", 12279 12280 "dev": true, 12280 12281 "dependencies": { 12281 12282 "@isaacs/fs-minipass": "^4.0.0", 12282 12283 "chownr": "^3.0.0", 12283 - "minipass": "^7.1.0", 12284 + "minipass": "^7.1.2", 12284 12285 "minizlib": "^3.0.1", 12285 12286 "mkdirp": "^3.0.1", 12286 12287 "yallist": "^5.0.0"
+1 -1
package.json
··· 64 64 "eslint-config-next": "14.2.3", 65 65 "postcss": "^8.4.38", 66 66 "prettier": "3.2.5", 67 - "supabase": "^1.167.4", 67 + "supabase": "^1.187.3", 68 68 "tailwindcss": "^3.4.3", 69 69 "typescript": "^5.5.3", 70 70 "wrangler": "^3.56.0"
+56
supabase/database.types.ts
··· 113 113 }, 114 114 ] 115 115 } 116 + identities: { 117 + Row: { 118 + created_at: string 119 + home_page: string 120 + id: string 121 + } 122 + Insert: { 123 + created_at?: string 124 + home_page: string 125 + id?: string 126 + } 127 + Update: { 128 + created_at?: string 129 + home_page?: string 130 + id?: string 131 + } 132 + Relationships: [ 133 + { 134 + foreignKeyName: "identities_home_page_fkey" 135 + columns: ["home_page"] 136 + isOneToOne: false 137 + referencedRelation: "permission_tokens" 138 + referencedColumns: ["id"] 139 + }, 140 + ] 141 + } 142 + permission_token_on_homepage: { 143 + Row: { 144 + identity: string 145 + token: string 146 + } 147 + Insert: { 148 + identity: string 149 + token: string 150 + } 151 + Update: { 152 + identity?: string 153 + token?: string 154 + } 155 + Relationships: [ 156 + { 157 + foreignKeyName: "permission_token_creator_identity_fkey" 158 + columns: ["identity"] 159 + isOneToOne: false 160 + referencedRelation: "identities" 161 + referencedColumns: ["id"] 162 + }, 163 + { 164 + foreignKeyName: "permission_token_creator_token_fkey" 165 + columns: ["token"] 166 + isOneToOne: false 167 + referencedRelation: "permission_tokens" 168 + referencedColumns: ["id"] 169 + }, 170 + ] 171 + } 116 172 permission_token_rights: { 117 173 Row: { 118 174 change_entity_set: boolean
+120
supabase/migrations/20240725190635_add identity tables.sql
··· 1 + create table "public"."identities" ( 2 + "id" uuid not null default gen_random_uuid(), 3 + "created_at" timestamp with time zone not null default now(), 4 + "home_page" uuid not null 5 + ); 6 + 7 + 8 + alter table "public"."identities" enable row level security; 9 + 10 + create table "public"."permission_token_on_homepage" ( 11 + "token" uuid not null, 12 + "identity" uuid not null 13 + ); 14 + 15 + 16 + alter table "public"."permission_token_on_homepage" enable row level security; 17 + 18 + CREATE UNIQUE INDEX identities_pkey ON public.identities USING btree (id); 19 + 20 + CREATE UNIQUE INDEX permission_token_creator_pkey ON public.permission_token_on_homepage USING btree (token, identity); 21 + 22 + alter table "public"."identities" add constraint "identities_pkey" PRIMARY KEY using index "identities_pkey"; 23 + 24 + alter table "public"."permission_token_on_homepage" add constraint "permission_token_creator_pkey" PRIMARY KEY using index "permission_token_creator_pkey"; 25 + 26 + alter table "public"."identities" add constraint "identities_home_page_fkey" FOREIGN KEY (home_page) REFERENCES permission_tokens(id) ON DELETE CASCADE not valid; 27 + 28 + alter table "public"."identities" validate constraint "identities_home_page_fkey"; 29 + 30 + 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; 31 + 32 + alter table "public"."permission_token_on_homepage" validate constraint "permission_token_creator_identity_fkey"; 33 + 34 + 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; 35 + 36 + alter table "public"."permission_token_on_homepage" validate constraint "permission_token_creator_token_fkey"; 37 + 38 + grant delete on table "public"."identities" to "anon"; 39 + 40 + grant insert on table "public"."identities" to "anon"; 41 + 42 + grant references on table "public"."identities" to "anon"; 43 + 44 + grant select on table "public"."identities" to "anon"; 45 + 46 + grant trigger on table "public"."identities" to "anon"; 47 + 48 + grant truncate on table "public"."identities" to "anon"; 49 + 50 + grant update on table "public"."identities" to "anon"; 51 + 52 + grant delete on table "public"."identities" to "authenticated"; 53 + 54 + grant insert on table "public"."identities" to "authenticated"; 55 + 56 + grant references on table "public"."identities" to "authenticated"; 57 + 58 + grant select on table "public"."identities" to "authenticated"; 59 + 60 + grant trigger on table "public"."identities" to "authenticated"; 61 + 62 + grant truncate on table "public"."identities" to "authenticated"; 63 + 64 + grant update on table "public"."identities" to "authenticated"; 65 + 66 + grant delete on table "public"."identities" to "service_role"; 67 + 68 + grant insert on table "public"."identities" to "service_role"; 69 + 70 + grant references on table "public"."identities" to "service_role"; 71 + 72 + grant select on table "public"."identities" to "service_role"; 73 + 74 + grant trigger on table "public"."identities" to "service_role"; 75 + 76 + grant truncate on table "public"."identities" to "service_role"; 77 + 78 + grant update on table "public"."identities" to "service_role"; 79 + 80 + grant delete on table "public"."permission_token_on_homepage" to "anon"; 81 + 82 + grant insert on table "public"."permission_token_on_homepage" to "anon"; 83 + 84 + grant references on table "public"."permission_token_on_homepage" to "anon"; 85 + 86 + grant select on table "public"."permission_token_on_homepage" to "anon"; 87 + 88 + grant trigger on table "public"."permission_token_on_homepage" to "anon"; 89 + 90 + grant truncate on table "public"."permission_token_on_homepage" to "anon"; 91 + 92 + grant update on table "public"."permission_token_on_homepage" to "anon"; 93 + 94 + grant delete on table "public"."permission_token_on_homepage" to "authenticated"; 95 + 96 + grant insert on table "public"."permission_token_on_homepage" to "authenticated"; 97 + 98 + grant references on table "public"."permission_token_on_homepage" to "authenticated"; 99 + 100 + grant select on table "public"."permission_token_on_homepage" to "authenticated"; 101 + 102 + grant trigger on table "public"."permission_token_on_homepage" to "authenticated"; 103 + 104 + grant truncate on table "public"."permission_token_on_homepage" to "authenticated"; 105 + 106 + grant update on table "public"."permission_token_on_homepage" to "authenticated"; 107 + 108 + grant delete on table "public"."permission_token_on_homepage" to "service_role"; 109 + 110 + grant insert on table "public"."permission_token_on_homepage" to "service_role"; 111 + 112 + grant references on table "public"."permission_token_on_homepage" to "service_role"; 113 + 114 + grant select on table "public"."permission_token_on_homepage" to "service_role"; 115 + 116 + grant trigger on table "public"."permission_token_on_homepage" to "service_role"; 117 + 118 + grant truncate on table "public"."permission_token_on_homepage" to "service_role"; 119 + 120 + grant update on table "public"."permission_token_on_homepage" to "service_role";