diff --git a/api/share.ts b/api/share.ts new file mode 100644 index 000000000..936144ed1 --- /dev/null +++ b/api/share.ts @@ -0,0 +1,33 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { confirmActiveSubscriptionFromToken } from "./_lib/_helpers"; +import { supabase } from "./_lib/_supabase"; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + // make sure user is logged in + const token = req.headers.authorization; + if (!token) { + return res.status(401).send("Unauthorized"); + } + if (!confirmActiveSubscriptionFromToken(token)) { + return res.status(402).send("Unauthorized"); + } + + // get chartId and userEmail from request body + const { chartId, userEmail } = req.body; + if (!chartId || !userEmail) { + return res.status(400).send("Missing chartId or userEmail"); + } + + const result = await supabase.from("shared_charts").insert([ + { + flowchart_id: chartId, + user_email: userEmail, + }, + ]); + + if (result.error) { + return res.status(400).send(result.error.message); + } + + return res.status(200).send("Chart shared successfully"); +} diff --git a/app/package.json b/app/package.json index 9f25eb1f6..e7eefa878 100644 --- a/app/package.json +++ b/app/package.json @@ -54,8 +54,9 @@ "@sentry/tracing": "^7.38.0", "@stripe/react-stripe-js": "^1.16.4", "@stripe/stripe-js": "^1.46.0", - "@supabase/gotrue-js": "^2", - "@supabase/supabase-js": "^2", + "@supabase/gotrue-js": "^2.12.1", + "@supabase/realtime-js": "^2.6.0", + "@supabase/supabase-js": "^2.8.0", "@svgr/webpack": "^6.3.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", @@ -107,6 +108,10 @@ "svgo": "^2.8.0", "use-debounce": "^5.2.1", "web-vitals": "^1.0.1", + "y-monaco": "^0.1.4", + "y-protocols": "^1.0.5", + "y-websocket": "^1.4.5", + "yjs": "^13.5.47", "zustand": "^4.3.3" }, "eslintConfig": { @@ -189,6 +194,9 @@ ], "transformIgnorePatterns": [ "node_modules/(?!(react-use-localstorage)).*\\.js$" - ] + ], + "moduleNameMapper": { + "monaco-editor": "/node_modules/@monaco-editor/react" + } } } diff --git a/app/src/components/AppContext.tsx b/app/src/components/AppContext.tsx index fbe7a11b4..60ee333ca 100644 --- a/app/src/components/AppContext.tsx +++ b/app/src/components/AppContext.tsx @@ -26,7 +26,7 @@ import { colors, darkTheme } from "../slang/config"; type Theme = typeof colors; // Stored in localStorage -export type UserSettings = { +type UserSettings = { mode: "light" | "dark"; language?: string; }; diff --git a/app/src/components/CloneButton.tsx b/app/src/components/CloneButton.tsx index 497ac8de8..3b0ca84ea 100644 --- a/app/src/components/CloneButton.tsx +++ b/app/src/components/CloneButton.tsx @@ -2,14 +2,15 @@ import { Trans } from "@lingui/macro"; import { FaCopy } from "react-icons/fa"; import { useHistory } from "react-router-dom"; +import { getDoc } from "../lib/docHelpers"; +import { docToString } from "../lib/docToString"; import { randomChartName, titleToLocalStorageKey } from "../lib/helpers"; -import { docToString, useDoc } from "../lib/useDoc"; import { Type } from "../slang"; import styles from "./EditorWrapper.module.css"; export function CloneButton() { const { push } = useHistory(); - const fullText = useDoc((s) => docToString(s)); + const fullText = docToString(getDoc()); return ( + + ) : null} */} ); } @@ -285,11 +314,11 @@ function Mermaid() { } function HostedOptions() { - const id = useDocDetails("id"); + const id = useDetails("id"); if (typeof id !== "number") throw new Error("id is not a number"); - const isPublic = useDocDetails("isPublic"); - const publicId = useDocDetails("publicId"); + const isPublic = useDetails("isPublic"); + const publicId = useDetails("publicId"); const makePublic = useMutation( "makeChartPublic", @@ -297,11 +326,11 @@ function HostedOptions() { { onSuccess: (result) => { if (!result) return; - useDoc.setState( + useDetailsStore.setState( (state) => { return produce(state, (draft) => { - draft.details.isPublic = result.isPublic; - draft.details.publicId = result.publicId; + draft.isPublic = result.isPublic; + draft.publicId = result.publicId; }); }, false, diff --git a/app/src/components/SignUpForm.tsx b/app/src/components/SignUpForm.tsx index 8c266ad9a..20b1bf45c 100644 --- a/app/src/components/SignUpForm.tsx +++ b/app/src/components/SignUpForm.tsx @@ -1,11 +1,10 @@ import { t, Trans } from "@lingui/macro"; import * as RadioGroup from "@radix-ui/react-radio-group"; import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js"; -import { useContext, useState } from "react"; +import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useMutation } from "react-query"; -import { AppContext } from "../components/AppContext"; import { Button, Input, Notice } from "../components/Shared"; import Spinner from "../components/Spinner"; import { isError } from "../lib/helpers"; @@ -31,7 +30,6 @@ export function SignUpForm() { email: "", }, }); - const { theme } = useContext(AppContext); const [success, setSuccess] = useState(false); const create = useMutation( "createCustomer", diff --git a/app/src/components/Tabs/EditLayoutTab.tsx b/app/src/components/Tabs/EditLayoutTab.tsx index 28b24d10e..d60676281 100644 --- a/app/src/components/Tabs/EditLayoutTab.tsx +++ b/app/src/components/Tabs/EditLayoutTab.tsx @@ -1,13 +1,12 @@ import { t, Trans } from "@lingui/macro"; -import produce from "immer"; import { FaRegSnowflake } from "react-icons/fa"; import { GraphOptionsObject } from "../../lib/constants"; +import { setMetaImmer, useSafeDoc } from "../../lib/docHelpers"; import { defaultLayout, getLayout } from "../../lib/getLayout"; import { directions, layouts } from "../../lib/graphOptions"; import { hasOwnProperty } from "../../lib/helpers"; import { useIsValidSponsor } from "../../lib/hooks"; -import { useDoc } from "../../lib/useDoc"; import { unfreezeDoc, useIsFrozen } from "../../lib/useIsFrozen"; import styles from "./EditLayoutTab.module.css"; import { @@ -21,7 +20,7 @@ import { export function EditLayoutTab() { const isValidSponsor = useIsValidSponsor(); - const doc = useDoc(); + const doc = useSafeDoc(); const layout = ( hasOwnProperty(doc.meta, "layout") ? doc.meta.layout : {} ) as GraphOptionsObject["layout"]; @@ -76,18 +75,12 @@ export function EditLayoutTab() { options={layouts} value={layoutName} onValueChange={(name) => { - useDoc.setState( - (state) => { - return produce(state, (draft) => { - if (!draft.meta.layout) draft.meta.layout = {}; - // This any is because typing the layout object is too restrictive - (draft.meta.layout as any).name = name; - delete draft.meta.nodePositions; - }); - }, - false, - "EditLayoutTab/layout" - ); + setMetaImmer((draft) => { + if (!draft.layout) draft.layout = {}; + // This any is because typing the layout object is too restrictive + (draft.layout as any).name = name; + delete draft.nodePositions; + }, "EditLayoutTab/layout"); }} /> @@ -98,18 +91,12 @@ export function EditLayoutTab() { options={directions} value={direction} onValueChange={(direction) => { - useDoc.setState( - (state) => { - return produce(state, (draft) => { - if (!draft.meta.layout) draft.meta.layout = {}; - // This any is because typing the layout object is too restrictive - (draft.meta.layout as any).rankDir = direction; - delete draft.meta.nodePositions; - }); - }, - false, - "EditLayoutTab/direction" - ); + setMetaImmer((draft) => { + if (!draft.layout) draft.layout = {}; + // This any is because typing the layout object is too restrictive + (draft.layout as any).rankDir = direction; + delete draft.nodePositions; + }, "EditLayoutTab/direction"); }} /> @@ -142,20 +129,14 @@ export function EditLayoutTab() { min={0.25} className={styles.numberInput} onChange={(e) => { - useDoc.setState( - (state) => { - return produce(state, (draft) => { - if (!draft.meta.layout) draft.meta.layout = {}; - // This any is because typing the layout object is too restrictive + setMetaImmer((draft) => { + if (!draft.layout) draft.layout = {}; + // This any is because typing the layout object is too restrictive - (draft.meta.layout as any).spacingFactor = parseFloat( - e.target.value - ); - }); - }, - false, - "EditLayoutTab/spacing-number" - ); + (draft.layout as any).spacingFactor = parseFloat( + e.target.value + ); + }, "EditLayoutTab/spacing-number"); }} /> { - useDoc.setState( - (state) => { - return produce(state, (draft) => { - if (!draft.meta.layout) draft.meta.layout = {}; - // This any is because typing the layout object is too restrictive - (draft.meta.layout as any).spacingFactor = value; - }); - }, - false, - "EditLayoutTab/spacing" - ); + setMetaImmer((draft) => { + if (!draft.layout) draft.layout = {}; + // This any is because typing the layout object is too restrictive + (draft.layout as any).spacingFactor = value; + }, "EditLayoutTab/spacing"); }} /> diff --git a/app/src/components/Tabs/EditMetaTab.tsx b/app/src/components/Tabs/EditMetaTab.tsx index 85bc1b574..8665a0732 100644 --- a/app/src/components/Tabs/EditMetaTab.tsx +++ b/app/src/components/Tabs/EditMetaTab.tsx @@ -3,15 +3,18 @@ import Editor from "@monaco-editor/react"; import { useEffect, useState } from "react"; import { editorOptions } from "../../lib/constants"; +import { setDoc, useDocMeta } from "../../lib/docHelpers"; import { useLightOrDarkMode } from "../../lib/hooks"; -import { useDoc } from "../../lib/useDoc"; import { Button } from "../Shared"; export function EditMetaTab() { - const meta = useDoc((s) => s.meta); + const meta = useDocMeta(); const [localMeta, setLocalMeta] = useState(JSON.stringify(meta, null, 2)); // try to parse when changed and only allow saving if valid const [parsed, setParsed] = useState>(meta); + useEffect(() => { + setLocalMeta(JSON.stringify(meta, null, 2)); + }, [meta]); useEffect(() => { try { const parsed = JSON.parse(localMeta); @@ -62,7 +65,7 @@ export function EditMetaTab() { onClick={() => { try { if (!parsed) return; - useDoc.setState({ meta: parsed }, false, "EditMetaTab/meta"); + setDoc({ meta: parsed }, "EditMetaTab/meta"); } catch (e) { console.error(e); } diff --git a/app/src/components/Tabs/EditStyleTab.tsx b/app/src/components/Tabs/EditStyleTab.tsx index dc6812cbc..65fa69117 100644 --- a/app/src/components/Tabs/EditStyleTab.tsx +++ b/app/src/components/Tabs/EditStyleTab.tsx @@ -1,7 +1,7 @@ import { t, Trans } from "@lingui/macro"; -import produce from "immer"; import throttle from "lodash.throttle"; +import { setMetaImmer, useDocMeta } from "../../lib/docHelpers"; import { themes } from "../../lib/graphOptions"; import { useBackgroundColor, @@ -9,7 +9,6 @@ import { useThemeKey, } from "../../lib/graphThemes"; import { useIsValidSponsor } from "../../lib/hooks"; -import { useDoc } from "../../lib/useDoc"; import { Button } from "../Shared"; import { CustomSelect, @@ -25,7 +24,7 @@ export function EditStyleTab() { const themeNiceName = themes.find((t) => t.value === themeKey)?.label() ?? "???"; const theme = useCurrentTheme(themeKey); - const meta = useDoc((s) => s.meta); + const meta = useDocMeta(); const bg = useBackgroundColor(theme); return ( @@ -36,15 +35,9 @@ export function EditStyleTab() { options={themes} value={themeKey} onValueChange={(themeKey) => { - useDoc.setState( - (s) => { - return produce(s, (draft) => { - draft.meta.theme = themeKey; - }); - }, - false, - "EditStyleTab/theme" - ); + setMetaImmer((draft) => { + draft.theme = themeKey; + }, "EditStyleTab/theme"); }} /> @@ -68,15 +61,9 @@ export function EditStyleTab() { {meta.background && (