From 6cfb8a7da1fbe0155cf5c5bc549aa9698100bc15 Mon Sep 17 00:00:00 2001 From: fengkx Date: Sat, 21 Aug 2021 03:31:18 +0800 Subject: [PATCH] Cloud sync with supbase (#1) * buggy adapter * chore: update dependencies * feat: basic input output * fix: fallback when no supabase setting * fix: types * feat: functional import export * fix: loading optimize * fix: tweak debounce timeout * feat: login logout button * fix: title of created page * fix: delete block issue * refactor: optimize delete block ui * chore: Docker build * chore: github workflow * fix: favicon * chore: static Docker * feat: directly return 404 * chore: enable docker image push on master branch * feat: github login and init sql * chore: build static in workflow --- .dockerignore | 25 + .github/workflows/dockerimage.yml | 61 + Dockerfile | 20 + Dockerfile-ci | 9 + README.md | 2 +- atom-util/atomWithDebouncedStorage.ts | 22 +- components/404.tsx | 5 + components/Block/index.tsx | 26 +- components/Editor/adapters/AdapterContext.tsx | 22 +- components/Editor/adapters/memory.ts | 50 +- components/Editor/adapters/supabase.ts | 363 ++ components/Editor/adapters/types.d.ts | 4 + components/LeftAside/ToolButton.tsx | 7 +- components/LeftAside/index.tsx | 46 +- components/Loading.tsx | 7 +- components/Main/index.tsx | 5 +- components/Navbar/PageSearchInput.tsx | 32 +- components/Navbar/index.tsx | 6 +- components/Note.tsx | 13 +- db/index.ts | 11 + db/init.sql | 113 + docker-entry-static.sh | 7 + hooks/signin-signout.ts | 66 + next.config.js | 3 +- nginx/nginx.conf | 354 + nginx/site-common.conf | 11 + package-lock.json | 5314 --------------- package.json | 40 +- pages/_app.tsx | 2 +- pages/api/hello.ts | 13 - pages/docs/index.tsx | 3 +- pages/index.tsx | 7 +- pages/login/index.tsx | 99 + pages/note/[pageId].tsx | 2 +- pages/signup/index.tsx | 100 + pnpm-lock.yaml | 5759 +++++++++++++++++ public/icons/log-in.svg | 1 + public/icons/log-out.svg | 1 + types/supabase.ts | 490 ++ 39 files changed, 7694 insertions(+), 5427 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/dockerimage.yml create mode 100644 Dockerfile create mode 100644 Dockerfile-ci create mode 100644 components/404.tsx create mode 100644 components/Editor/adapters/supabase.ts create mode 100644 db/index.ts create mode 100644 db/init.sql create mode 100644 docker-entry-static.sh create mode 100644 hooks/signin-signout.ts create mode 100644 nginx/nginx.conf create mode 100644 nginx/site-common.conf delete mode 100644 package-lock.json delete mode 100644 pages/api/hello.ts create mode 100644 pages/login/index.tsx create mode 100644 pages/signup/index.tsx create mode 100644 pnpm-lock.yaml create mode 100644 public/icons/log-in.svg create mode 100644 public/icons/log-out.svg create mode 100644 types/supabase.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..90442e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# Docker +Dockerfile + +# dependencies +/node_modules +/.pnp +.pnp.js + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/.github/workflows/dockerimage.yml b/.github/workflows/dockerimage.yml new file mode 100644 index 0000000..2790961 --- /dev/null +++ b/.github/workflows/dockerimage.yml @@ -0,0 +1,61 @@ +name: Publish Docker +on: + - push + - pull_request_target + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Cache pnpm modules + uses: actions/cache@v2 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}- + - name: Set up pnpm + uses: pnpm/action-setup@v2.0.1 + with: + version: 6 + run_install: true + - name: Build static + shell: bash + id: build-static + run: | + export NEXT_PUBLIC_SUPABASE_URL=ECALPER_EB_OT_GNIRTS_EUQINU_YREV_EMOS_SUPABASE_URL + export NEXT_PUBLIC_SUPABSE_PUBLIC_ANON_KEY=ECALPER_EB_OT_GNIRTS_EUQINU_YREV_EMOS_SUPABSE_PUBLIC_ANON_KEY + pnpm run build + pnpm run export + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + if: ${{ github.ref == 'refs/heads/master' }} + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: output-docker-tag + shell: bash + id: docker-tag + run: | + unset BRANCH_NAME + BRANCH_NAME=`echo $GITHUB_REF | cut -d '/' -f3 -` + echo "BRANCH_NAME: $BRANCH_NAME" + unset IMG_TAGS ; if [[ $BRANCH_NAME == "master" ]]; then IMG_TAGS='latest' ; else IMG_TAGS="$BRANCH_NAME"; fi + SHA_TAG=`echo $GITHUB_SHA | head -c 7` + echo "SHA_TAG: $SHA_TAG" + echo ::set-output name=DOCKER_BRANCH_TAG::${IMG_TAGS} + echo ::set-output name=DOCKER_SHA_TAG::${SHA_TAG} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile-ci + push: ${{ github.ref == 'refs/heads/master' }} + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + tags: fengkx/plastic-editor:${{steps.docker-tag.outputs.DOCKER_BRANCH_TAG}},fengkx/plastic-editor:${{steps.docker-tag.outputs.DOCKER_SHA_TAG}} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94f512e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:lts AS builder +RUN apt update && apt install -y git build-essential +WORKDIR /app +COPY package.json pnpm-lock.yaml /app/ +RUN npm i -g pnpm +RUN pnpm install --frozen-lockfile +COPY . /app/ +ARG NEXT_PUBLIC_SUPABASE_URL=ECALPER_EB_OT_GNIRTS_EUQINU_YREV_EMOS_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABSE_PUBLIC_ANON_KEY=ECALPER_EB_OT_GNIRTS_EUQINU_YREV_EMOS_SUPABSE_PUBLIC_ANON_KEY +RUN pnpm run build && pnpm run export + +FROM ranadeeppolavarapu/nginx-http3:latest +ENV NGINX_ENVSUBST_OUTPUT_DIR /etc/nginx + +COPY nginx/nginx.conf /etc/nginx/templates/nginx.conf.template +COPY nginx/site-common.conf /etc/nginx/site-common.conf +COPY --from=builder /app/out/ /var/www/static/ +COPY docker-entry-static.sh /app/docker-entry-static.sh +WORKDIR /var/www/static +CMD ["sh", "/app/docker-entry-static.sh"] diff --git a/Dockerfile-ci b/Dockerfile-ci new file mode 100644 index 0000000..035e394 --- /dev/null +++ b/Dockerfile-ci @@ -0,0 +1,9 @@ +FROM ranadeeppolavarapu/nginx-http3:latest +ENV NGINX_ENVSUBST_OUTPUT_DIR /etc/nginx + +COPY nginx/nginx.conf /etc/nginx/templates/nginx.conf.template +COPY nginx/site-common.conf /etc/nginx/site-common.conf +COPY out/ /var/www/static/ +COPY docker-entry-static.sh /app/docker-entry-static.sh +WORKDIR /var/www/static +CMD ["sh", "/app/docker-entry-static.sh"] diff --git a/README.md b/README.md index 5994d4e..1f23f7c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A block-based note app. - # Prior Art + - https://github.com/djyde/plastic-editor/ - https://github.com/pmndrs/jotai diff --git a/atom-util/atomWithDebouncedStorage.ts b/atom-util/atomWithDebouncedStorage.ts index 5a21f8c..69aeff9 100644 --- a/atom-util/atomWithDebouncedStorage.ts +++ b/atom-util/atomWithDebouncedStorage.ts @@ -2,21 +2,23 @@ import type { PrimitiveAtom, SetStateAction, WritableAtom } from "jotai"; import { atom } from "jotai"; import _debounce from "lodash.debounce"; -type Unsubscribe = () => void; +export type Unsubscribe = () => void; -type Storage = { +export type Storage = { getItem: (key: string) => Value | Promise; setItem: (key: string, newValue: Value) => void | Promise; delayInit?: boolean; subscribe?: (key: string, callback: (value: Value) => void) => Unsubscribe; }; -type StringStorage = { - getItem: (key: string) => string | null | Promise; - setItem: (key: string, newValue: string) => void | Promise; +export type SimpleStorage = { + getItem: (key: string) => Value | null | Promise; + setItem: (key: string, newValue: Value) => void | Promise; }; -export const createJSONStorage = ( +export type StringStorage = SimpleStorage; + +export const createJSONStorage = ( getStringStorage: () => StringStorage ): Storage => ({ getItem: (key) => { @@ -27,6 +29,7 @@ export const createJSONStorage = ( return JSON.parse(value || ""); }, setItem: (key, newValue) => { + // @ts-ignore getStringStorage().setItem(key, JSON.stringify(newValue)); }, }); @@ -39,13 +42,16 @@ export function atomWithDebouncedStorage( isStaleAtom: WritableAtom, wait: number, debounceOptions: Parameters[2], - storage: Storage = defaultStorage as Storage + storage: Storage = defaultStorage as Storage, + fallback: boolean = false ): PrimitiveAtom { const getInitialValue = () => { try { const value = storage.getItem(key); if (value instanceof Promise) { - return value.catch(() => initialValue); + return value + .then((v) => (fallback ? v ?? initialValue : v)) + .catch(() => initialValue); } return value; } catch { diff --git a/components/404.tsx b/components/404.tsx new file mode 100644 index 0000000..8cf146a --- /dev/null +++ b/components/404.tsx @@ -0,0 +1,5 @@ +import Error from "next/error"; + +export default function NotFound() { + return ; +} diff --git a/components/Block/index.tsx b/components/Block/index.tsx index 3d30192..f308acb 100644 --- a/components/Block/index.tsx +++ b/components/Block/index.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import type { ShallowBlock } from "@plastic-editor/protocol/lib/protocol"; import { useMountEffect, useSafeState } from "@react-hookz/web"; import clsx from "clsx"; @@ -11,6 +12,7 @@ import { ID_LEN } from "../Editor/adapters/memory"; import { editingBlockIdAtom } from "../Editor/store"; import { BlockContent } from "./BlockContent"; import { LineDirection } from "./LineDirection"; +import { DotFlashing } from "../Loading"; export type PropsType = { debugMode?: boolean; path: number[]; @@ -100,17 +102,19 @@ const BlockImpl: React.FC = ({ <>
- + }> + +
{shallowBlock.children.length > 0 && ( (memoryAdapter); export type PropsType = { - adapter: typeof memoryAdapter; + adapter?: IAdapter; }; -export const AdapterProvider: React.FC = ({ adapter, children }) => { +export const AdapterProvider: React.FC = ({ children, adapter }) => { + if (!adapter) { + if (hasSupabase) { + const session = supabase.auth.session(); + console.log(session, Boolean(session)); + adapter = Boolean(session) ? supbaseAdapter : memoryAdapter; + } else { + adapter = memoryAdapter; + } + } return ( {children} @@ -14,6 +26,6 @@ export const AdapterProvider: React.FC = ({ adapter, children }) => { ); }; -export function useAdapter() { - return useContext(AdapterContext); +export function useAdapter() { + return useContext(AdapterContext) as T; } diff --git a/components/Editor/adapters/memory.ts b/components/Editor/adapters/memory.ts index bce6d1c..5b4429b 100644 --- a/components/Editor/adapters/memory.ts +++ b/components/Editor/adapters/memory.ts @@ -13,7 +13,7 @@ import { nanoid } from "nanoid"; import { NextRouter } from "next/router"; import { atomWithDebouncedStorage } from "../../../atom-util/atomWithDebouncedStorage"; import { anchorOffsetAtom, editingBlockIdAtom } from "../store"; -import { Note } from "./types"; +import { Note, PartialPick } from "./types"; export const ID_LEN = 15; const DEBOUNCE_WAIT = 500; @@ -21,10 +21,6 @@ const DEBOUNCE_MAX_WAIT = 2000; const isStaleAtom = atom(false); -type PartialPick = { - [P in K]?: T[P]; -}; - const defaultPageIdFromRoute = () => { if (process.browser) { const matches = window?.location?.pathname?.match( @@ -44,7 +40,7 @@ const pagesAtom = atomWithDebouncedStorage>( DEBOUNCE_WAIT, { maxWait: DEBOUNCE_MAX_WAIT } ); -const pageDefault = (id: string): Page => ({ +export const pageDefault = (id: string): Page => ({ id, type: "default" as const, title: `${format(new Date(), "MMMM, dd, yyyy")}`, @@ -71,6 +67,9 @@ const pageFamily = atomFamily< const shallow: ShallowBlock = { id: block.id, children: [] }; defaultValue.children = [shallow]; } + if (title) { + defaultValue.title = title; + } return defaultValue; }, (get, set, update) => { @@ -88,7 +87,7 @@ const pageFamily = atomFamily< (a, b) => a.id === b.id ); -const blockDefault = (id: string, pageId: string): Block => ({ +export const blockDefault = (id: string, pageId: string): Block => ({ id, pageId, content: "", @@ -197,19 +196,22 @@ const newBlockAtom = atom< } }); -const pageTitleAtom = atom( - (get) => { - const title = get(pageFamily({ id: get(pageIdAtom) })).title; - return title; - }, - (get, set, update) => { - const page = get(pageFamily({ id: get(pageIdAtom) })); - const newPage = produce(page, (draft) => { - draft.title = update as string; - }); - set(pageFamily({ id: get(pageIdAtom) }), newPage); +const newPageAtom = atom< + null, + { + newPageId: string; + title: string; + children?: ShallowBlock[]; + goto?: boolean; } -); +>(null, (get, set, update) => { + const { newPageId, title, children, goto } = update; + const newPageAtom = pageFamily({ id: newPageId, title, children }); + set(newPageAtom, get(newPageAtom)); + if (goto) { + set(pageIdAtom, newPageId); + } +}); const moveBlockAtom = atom( null, @@ -229,9 +231,11 @@ const moveBlockAtom = atom( } ); -const pageValuesAtom = atom((get) => { - return Object.values(get(pagesAtom)); -}); +const pageValuesAtom = atom<(Partial & Pick)[]>( + (get) => { + return Object.values(get(pagesAtom)); + } +); const saveNotesAtom = atom(null, (get) => { const pages = Object.values(get(pagesAtom)); @@ -307,6 +311,7 @@ export const memoryAdapter = { moveBlockAtom, deleteBlockAtom, newBlockAtom, + newPageAtom, usePage, useBlock, saveNotesAtom, @@ -314,6 +319,5 @@ export const memoryAdapter = { starsAtom, gotoPageAtom, isStaleAtom, - pageTitleAtom, pageValuesAtom, } as const; diff --git a/components/Editor/adapters/supabase.ts b/components/Editor/adapters/supabase.ts new file mode 100644 index 0000000..421017d --- /dev/null +++ b/components/Editor/adapters/supabase.ts @@ -0,0 +1,363 @@ +import { blockDefault, ID_LEN, memoryAdapter, pageDefault } from "./memory"; +import { atomFamily, useAtomValue } from "jotai/utils"; +import { + Block, + Page, + ShallowBlock, +} from "@plastic-editor/protocol/lib/protocol"; +import { atom, useAtom } from "jotai"; +import { Note, PartialPick } from "./types"; +import { supabase } from "../../../db"; +import { definitions } from "../../../types/supabase"; +import { + atomWithDebouncedStorage, + createJSONStorage, + SimpleStorage, + Storage, +} from "../../../atom-util/atomWithDebouncedStorage"; +import { PageEngine } from "@plastic-editor/protocol"; +import { anchorOffsetAtom, editingBlockIdAtom } from "../store"; +import FileSaver from "file-saver"; + +const DEBOUNCE_WAIT = 500; +const DEBOUNCE_MAX_WAIT = 1500; + +export type TSupabaseAdapter = Omit< + typeof memoryAdapter, + "pagesAtom" | "blocksAtom" +>; +const { isStaleAtom, gotoPageAtom, pageIdAtom } = memoryAdapter; + +const blockStoarge: SimpleStorage = { + getItem: async (id) => { + const resp = await supabase + .from("blocks") + .select() + .eq("block_id", id); + const dbValue = resp.data?.[0]; + return dbValue?.content ?? null; + }, + setItem: async (id, value) => { + await supabase.from("blocks").upsert( + { + block_id: id, + content: value, + }, + { onConflict: "block_id" } + ); + return; + }, +}; + +const pageStorage: SimpleStorage< + Omit & Page +> = { + getItem: async (id) => { + const resp = await supabase + .from("page_content") + .select() + .eq("page_id", id); + const dbValue = resp.data?.[0]; + return dbValue?.content ?? null; + }, + setItem: async (id, value) => { + const { is_public = false, is_writable = false } = value; + await supabase.from("page_metas").upsert( + { + page_id: id, + is_public, + is_writable, + }, + { onConflict: "page_id" } + ); + const page: Page = { + children: value.children, + title: value.title, + id: id, + }; + await supabase.from("page_content").upsert( + { + page_id: id, + content: page, + }, + { onConflict: "page_id" } + ); + return; + }, +}; +const blockFamily = atomFamily< + Pick & PartialPick, + Block, + Block +>( + ({ id, pageId, content }) => { + const defaultValue = blockDefault(id, pageId); + if (content) defaultValue.content = content; + return atomWithDebouncedStorage( + id, + defaultValue, + isStaleAtom, + DEBOUNCE_WAIT, + { maxWait: DEBOUNCE_MAX_WAIT }, + blockStoarge as Storage, + true + ); + }, + (a, b) => a.id === b.id && a.pageId === b.pageId +); + +const pageFamily = atomFamily< + Pick & PartialPick, + Page, + Page +>( + ({ id, children, title }) => { + const defaultValue = pageDefault(id); + if (!children) { + const shallow: ShallowBlock = { id, children: [] }; + defaultValue.children = [shallow]; + } + if (title) { + defaultValue.title = title; + } + + return atomWithDebouncedStorage( + id, + defaultValue, + isStaleAtom, + DEBOUNCE_WAIT, + { maxWait: DEBOUNCE_MAX_WAIT }, + pageStorage as Storage, + true + ); + }, + (a, b) => a.id === b.id +); + +const starsAtom = atomWithDebouncedStorage( + "plastic@stars", + [], + isStaleAtom, + DEBOUNCE_WAIT, + { maxWait: DEBOUNCE_MAX_WAIT }, + { + async getItem(key) { + const resp = await supabase.from("stars").select(); + const dbValue = resp.data?.[0]?.content ?? []; + return dbValue; + }, + async setItem(key, newVal) { + const resp = await supabase.from("stars").upsert({ + content: newVal, + }); + }, + } +); + +const newBlockAtom = atom< + null, + { + newBlockId: string; + pageId: string; + path: number[]; + content?: string; + op: "append" | "prepend" | "prependChild"; + } +>(null, (get, set, update) => { + const { newBlockId, pageId, path, content } = update; + const newBlockAtom = blockFamily({ id: newBlockId, pageId, content }); + const shallow: ShallowBlock = { id: newBlockId, children: [] }; + const pageAtom = pageFamily({ id: pageId }); + const pageEngine = new PageEngine(get(pageAtom)); + switch (update.op) { + case "append": + pageEngine.apendBlockAt(path, shallow); + break; + case "prepend": + pageEngine.prependBlockAt(path, shallow); + break; + case "prependChild": + pageEngine.prependChild(path, shallow); + } + set(pageAtom, pageEngine.page); + set(editingBlockIdAtom, newBlockId); + if (content) { + set(newBlockAtom, get(newBlockAtom)); + } +}); +const newPageAtom = atom< + null, + { + newPageId: string; + title?: string; + children?: ShallowBlock[]; + goto?: boolean; + } +>(null, async (get, set, update) => { + const { newPageId, title, children, goto } = update; + pageFamily({ id: newPageId, title, children }); + if (goto) { + set(pageIdAtom, newPageId); + } +}); + +const moveBlockAtom = atom( + null, + (get, set, update) => { + const { from, to } = update; + console.debug(update); + const pageId = get(pageIdAtom); + const page = get(pageFamily({ id: pageId })); + const pageEngine = new PageEngine(page); + console.debug(pageEngine.page); + const shallowBlock = pageEngine.access(from); + const [toParent] = pageEngine.accessParent(to); + pageEngine.remove(from); + toParent.children.splice(to[to.length - 1], 0, shallowBlock); + set(pageFamily({ id: pageId }), pageEngine.page); + console.debug(pageEngine.page); + } +); + +const deleteBlockAtom = atom( + null, + (get, set, update) => { + const { path, blockId } = update; + const pageAtom = pageFamily({ id: get(pageIdAtom) }); + const page = get(pageAtom); + const pageEngine = new PageEngine(page); + const [closest, closetPos] = pageEngine.upClosest(path); + pageEngine.remove(path); + const writeDb = async () => { + await supabase.from("page_content").upsert( + { + page_id: page.id, + content: pageEngine.page, + }, + { onConflict: "page_id" } + ); + }; + writeDb() + .then(() => { + set(isStaleAtom, false); + set(pageAtom, pageEngine.page); + set(anchorOffsetAtom, Infinity); + set(editingBlockIdAtom, closest.id); + }) + .finally(() => { + supabase + .from("blocks") + .delete() + .eq("block_id", blockId); + }); + } +); + +const usePage = () => { + return useAtom(pageFamily({ id: useAtomValue(pageIdAtom) })); +}; +const useBlock = (id: string) => { + const pageId = useAtomValue(pageIdAtom); + return useAtom(blockFamily({ id, pageId })); +}; + +const pageValuesAtom = atom(async (get) => { + const resp = await supabase + .from("page_content") + .select("content"); + return resp.data?.map((item) => item.content) ?? []; +}); + +const loadNotesAtom = atom(null, (get, set, update) => { + const loadNote = async () => { + await supabase.from("page_metas").upsert( + update.pages.map((p) => ({ page_id: p.id })), + { onConflict: "page_id" } + ); + const promises = [ + supabase.from("page_content").upsert( + update.pages.map((p) => ({ + page_id: p.id, + content: p, + })), + { onConflict: "page_id" } + ), + supabase.from("blocks").upsert( + Object.values(update.blocks).map((b) => ({ + block_id: b.id, + content: b, + })), + { onConflict: "block_id" } + ), + ]; + return await Promise.all(promises as any); + }; + + loadNote().then((res) => { + const currPageId = get(pageIdAtom); + const [pageContentResp, blockResp] = res; + const pageContents = (pageContentResp as any) + .data as definitions["page_content"][]; + let currPage = pageContents.find((p) => p.page_id === currPageId); + if (currPage) { + set(pageFamily({ id: currPageId }), currPage.content); + } + set(starsAtom, update.stars); + ((blockResp as any).data as definitions["blocks"][]) + .filter((b) => b.content.pageId === currPageId) + .forEach((b) => { + set(blockFamily({ id: b.block_id!, pageId: currPageId }), b.content); + }); + set(starsAtom, update.stars); + }); +}); + +const saveNotesAtom = atom(null, (get) => { + const getNotes = async () => { + let pagesResp = await supabase + .from("page_content") + .select(); + let starsResp = await supabase.from("stars").select(); + let blocksResp = await supabase + .from("blocks") + .select(); + const pages = (pagesResp.data ?? []).map((p) => p.content); + const blocks = (blocksResp.data ?? []) + .map((b) => b.content) + .reduce((acc, cur) => { + acc[cur.id] = cur; + return acc; + }, {}); + const stars = starsResp.data?.[0]?.content ?? []; + + const note: Note = { + pages, + blocks, + stars, + }; + return note; + }; + getNotes().then((note) => { + const jsonStr = JSON.stringify(note); + const blob = new Blob([jsonStr], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, "note.json"); + }); +}); + +export const supbaseAdapter: TSupabaseAdapter = { + isStaleAtom, + newPageAtom, + newBlockAtom, + gotoPageAtom, + pageIdAtom, + blockFamily, + pageFamily, + starsAtom, + usePage, + useBlock, + moveBlockAtom, + deleteBlockAtom, + pageValuesAtom, + loadNotesAtom, + saveNotesAtom, +}; diff --git a/components/Editor/adapters/types.d.ts b/components/Editor/adapters/types.d.ts index db3b411..d0aa1d1 100644 --- a/components/Editor/adapters/types.d.ts +++ b/components/Editor/adapters/types.d.ts @@ -4,6 +4,10 @@ import type { ShallowBlock, } from "@plastic-editor/protocol/lib/protocol"; +export type PartialPick = { + [P in K]?: T[P]; +}; + export type Note = { pages: Page[]; blocks: { [key: string]: Block }; diff --git a/components/LeftAside/ToolButton.tsx b/components/LeftAside/ToolButton.tsx index c6fefe6..162595e 100644 --- a/components/LeftAside/ToolButton.tsx +++ b/components/LeftAside/ToolButton.tsx @@ -19,7 +19,12 @@ export const ToolButton: React.FC = ({ style, }) => { return ( - diff --git a/components/LeftAside/index.tsx b/components/LeftAside/index.tsx index 9608732..521c9c6 100644 --- a/components/LeftAside/index.tsx +++ b/components/LeftAside/index.tsx @@ -2,10 +2,12 @@ import { useAtom } from "jotai"; import { useAtomValue, useUpdateAtom } from "jotai/utils"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useCallback } from "react"; +import { useCallback, Suspense } from "react"; import { useAdapter } from "../Editor/adapters/AdapterContext"; import { Note } from "../Editor/adapters/types"; import { ToolButton } from "./ToolButton"; +import { DotFlashing } from "../Loading"; +import { hasSupabase, supabase } from "../../db"; export const LeftAside: React.FC = () => { const router = useRouter(); const { gotoPageAtom, loadNotesAtom, saveNotesAtom } = useAdapter(); @@ -28,7 +30,7 @@ export const LeftAside: React.FC = () => { imgWidth={24} imgHeight={24} src="/icons/download-2-line.svg" - alt="Save button" + alt="Save" onClick={saveNote} /> { id="import" type="file" /> -