From fb78c9660bcc0ca001269b1296033b8023b25b3f Mon Sep 17 00:00:00 2001 From: EGOIST Date: Fri, 13 May 2022 15:21:49 +0800 Subject: [PATCH] allow to sort navigation items --- package.json | 4 + pnpm-lock.yaml | 41 ++++- src/lib/db.server.ts | 44 +++--- src/lib/helpers.ts | 11 +- src/lib/singleton.server.ts | 8 +- .../dashboard/[subdomain]/account/profile.tsx | 7 +- src/pages/dashboard/[subdomain]/editor.tsx | 8 +- .../[subdomain]/settings/domains.tsx | 2 + .../[subdomain]/settings/general.tsx | 2 + .../[subdomain]/settings/navigation.tsx | 141 +++++++++++------- 10 files changed, 182 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 79a5199d..8e08f252 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "cookie": "^0.5.0", "dayjs": "^1.11.1", "dotenv": "^16.0.0", + "fast-deep-equal": "^3.1.3", "form-data": "^4.0.0", "mailgun.js": "^5.2.2", "markdown-it": "^13.0.0", @@ -42,6 +43,8 @@ "react-hook-form": "^7.31.1", "react-hot-toast": "^2.2.0", "react-query": "^3.39.0", + "react-sortablejs": "^6.1.1", + "sortablejs": "^1.15.0", "ua-parser-js": "^1.0.2", "uuid": "^8.3.2", "zod": "^3.15.1", @@ -57,6 +60,7 @@ "@types/react": "^18.0.9", "@types/react-avatar-editor": "^12.0.0", "@types/react-dom": "^18.0.3", + "@types/sortablejs": "^1.13.0", "@types/ua-parser-js": "^0.7.36", "@types/uuid": "^8.3.4", "eslint": "^8.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 737abb25..adcfa879 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,7 @@ specifiers: '@types/react': ^18.0.9 '@types/react-avatar-editor': ^12.0.0 '@types/react-dom': ^18.0.3 + '@types/sortablejs': ^1.13.0 '@types/ua-parser-js': ^0.7.36 '@types/uuid': ^8.3.4 '@web-std/form-data': ^3.0.2 @@ -34,6 +35,7 @@ specifiers: dotenv: ^16.0.0 eslint: ^8.15.0 eslint-config-next: ^12.1.6 + fast-deep-equal: ^3.1.3 form-data: ^4.0.0 mailgun.js: ^5.2.2 markdown-it: ^13.0.0 @@ -48,6 +50,8 @@ specifiers: react-hook-form: ^7.31.1 react-hot-toast: ^2.2.0 react-query: ^3.39.0 + react-sortablejs: ^6.1.1 + sortablejs: ^1.15.0 tailwindcss: ^3.0.24 typescript: ^4.5.5 ua-parser-js: ^1.0.2 @@ -76,6 +80,7 @@ dependencies: cookie: 0.5.0 dayjs: 1.11.2 dotenv: 16.0.0 + fast-deep-equal: 3.1.3 form-data: 4.0.0 mailgun.js: 5.2.2 markdown-it: 13.0.1 @@ -88,6 +93,8 @@ dependencies: react-hook-form: 7.31.1_react@18.1.0 react-hot-toast: 2.2.0_ef5jwxihqo6n7gxfmzogljlgcm react-query: 3.39.0_ef5jwxihqo6n7gxfmzogljlgcm + react-sortablejs: 6.1.1_kjz72dk5lh6hp4ogspefhm7r2u + sortablejs: 1.15.0 ua-parser-js: 1.0.2 uuid: 8.3.2 zod: 3.15.1 @@ -103,6 +110,7 @@ devDependencies: '@types/react': 18.0.9 '@types/react-avatar-editor': 12.0.0 '@types/react-dom': 18.0.3 + '@types/sortablejs': 1.13.0 '@types/ua-parser-js': 0.7.36 '@types/uuid': 8.3.4 eslint: 8.15.0 @@ -1677,6 +1685,10 @@ packages: resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} dev: true + /@types/sortablejs/1.13.0: + resolution: {integrity: sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ==} + dev: true + /@types/ua-parser-js/0.7.36: resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==} dev: true @@ -2076,6 +2088,10 @@ packages: fsevents: 2.3.2 dev: true + /classnames/2.3.1: + resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} + dev: false + /clone-deep/4.0.1: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} @@ -2633,7 +2649,6 @@ packages: /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob/3.2.11: resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} @@ -3662,6 +3677,22 @@ packages: react-dom: 18.1.0_react@18.1.0 dev: false + /react-sortablejs/6.1.1_kjz72dk5lh6hp4ogspefhm7r2u: + resolution: {integrity: sha512-VXTCHiitk1QT1uxt+hFGSihG1phk/HAw/17a65diTT3NVm/5dui2O99X5L/tOFLgRT5ifxwZtnloTTDsT5EH2w==} + peerDependencies: + '@types/sortablejs': '1' + react: '>=16.9.0' + react-dom: '>=16.9.0' + sortablejs: '1' + dependencies: + '@types/sortablejs': 1.13.0 + classnames: 2.3.1 + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + sortablejs: 1.15.0 + tiny-invariant: 1.2.0 + dev: false + /react-ssr-prepass/1.5.0_react@18.1.0: resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==} peerDependencies: @@ -3805,6 +3836,10 @@ packages: engines: {node: '>=8'} dev: true + /sortablejs/1.15.0: + resolution: {integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==} + dev: false + /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -3935,6 +3970,10 @@ packages: resolution: {integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=} dev: true + /tiny-invariant/1.2.0: + resolution: {integrity: sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==} + dev: false + /to-fast-properties/2.0.0: resolution: {integrity: sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=} engines: {node: '>=4'} diff --git a/src/lib/db.server.ts b/src/lib/db.server.ts index 6d884646..c0627b70 100644 --- a/src/lib/db.server.ts +++ b/src/lib/db.server.ts @@ -6,34 +6,30 @@ const logLevel: Prisma.LogLevel[] = IS_PROD ? ["info", "error", "warn"] : ["info", "warn", "error", "query"] -export const prisma = /* @__PURE__ */ singleton( - "prisma", - () => { - if (!IS_PROD && !IS_BROWSER) { - for (const k in require.cache) { - if (k.includes("prisma")) { - delete require.cache[k] - } +export const prisma = /* @__PURE__ */ singleton("prisma", () => { + if (!IS_PROD && !IS_BROWSER) { + for (const k in require.cache) { + if (k.includes("prisma")) { + delete require.cache[k] } } + } - const client = new PrismaClient({ - log: logLevel, - }) + const client = new PrismaClient({ + log: logLevel, + }) - client.$use(async (params, next) => { - const now = Date.now() - const result = await next(params) - const end = Date.now() - const duration = Math.floor(end - now) - console.log("query took", duration, "ms") - // See results here - return result - }) + client.$use(async (params, next) => { + const now = Date.now() + const result = await next(params) + const end = Date.now() + const duration = Math.floor(end - now) + console.log("query took", duration, "ms") + // See results here + return result + }) - return client - }, - IS_PROD -) + return client +}) export type { Prisma } diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index e94b111a..ff8fbc6f 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,6 +1,15 @@ import { IS_PROD } from "./constants" import { OUR_DOMAIN } from "./env" -export const getSiteLink = ({ subdomain }: { subdomain: string }) => { +export const getSiteLink = ({ + subdomain, + noProtocol, +}: { + subdomain: string + noProtocol?: boolean +}) => { + if (noProtocol) { + return `${subdomain}.${OUR_DOMAIN}` + } return `${IS_PROD ? "https" : "http"}://${subdomain}.${OUR_DOMAIN}` } diff --git a/src/lib/singleton.server.ts b/src/lib/singleton.server.ts index 48616dcd..5e7fb46e 100644 --- a/src/lib/singleton.server.ts +++ b/src/lib/singleton.server.ts @@ -1,12 +1,8 @@ // @ts-nocheck globalThis._singletons = globalThis._singletons || {} -export const singleton = ( - id: string, - factory: () => T, - enableSingleton = true -): T => { - if (!globalThis._singletons[id] || !enableSingleton) { +export const singleton = (id: string, factory: () => T): T => { + if (!globalThis._singletons[id]) { globalThis._singletons[id] = factory() } return globalThis._singletons[id] diff --git a/src/pages/dashboard/[subdomain]/account/profile.tsx b/src/pages/dashboard/[subdomain]/account/profile.tsx index 2c138cd4..1ef66515 100644 --- a/src/pages/dashboard/[subdomain]/account/profile.tsx +++ b/src/pages/dashboard/[subdomain]/account/profile.tsx @@ -9,7 +9,10 @@ import { Input } from "~/components/ui/Input" import { trpc } from "~/lib/trpc" export default function AccountProfilePage() { - const viewer = trpc.useQuery(["auth.viewer"]) + const viewer = trpc.useQuery(["auth.viewer"], { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }) const updateProfile = trpc.useMutation("user.updateProfile") const form = useForm({ @@ -27,8 +30,8 @@ export default function AccountProfilePage() { useEffect(() => { if (updateProfile.isSuccess) { - updateProfile.reset() toast.success("Saved!") + updateProfile.reset() } }, [updateProfile]) diff --git a/src/pages/dashboard/[subdomain]/editor.tsx b/src/pages/dashboard/[subdomain]/editor.tsx index c3cd38e4..89f08f7d 100644 --- a/src/pages/dashboard/[subdomain]/editor.tsx +++ b/src/pages/dashboard/[subdomain]/editor.tsx @@ -14,6 +14,7 @@ import { type EditorView } from "@codemirror/view" import { trpc } from "~/lib/trpc" import { useRouter } from "next/router" import { DashboardLayout } from "~/components/dashboard/DashboardLayout" +import { UniLink } from "~/components/ui/UniLink" const getInputDatetimeValue = (date: Date | string) => { const str = dayjs(date).format() @@ -246,7 +247,12 @@ export default function SubdomainEditor() { {values.slug && (
This {isPost ? "post" : "page"} will be accessible at{" "} - {getSiteLink({ subdomain })}/{values.slug} + + {getSiteLink({ subdomain, noProtocol: true })}/{values.slug} +
)} diff --git a/src/pages/dashboard/[subdomain]/settings/domains.tsx b/src/pages/dashboard/[subdomain]/settings/domains.tsx index 43d65303..0a9aed3c 100644 --- a/src/pages/dashboard/[subdomain]/settings/domains.tsx +++ b/src/pages/dashboard/[subdomain]/settings/domains.tsx @@ -14,6 +14,8 @@ export default function SettingsDomainsPage() { const subdomain = router.query.subdomain as string const siteResult = trpc.useQuery(["site", { site: subdomain }], { enabled: !!subdomain, + refetchOnWindowFocus: false, + refetchOnReconnect: false, }) const ctx = trpc.useContext() const updateSite = trpc.useMutation("site.updateSite") diff --git a/src/pages/dashboard/[subdomain]/settings/general.tsx b/src/pages/dashboard/[subdomain]/settings/general.tsx index e3d01a43..e68a4049 100644 --- a/src/pages/dashboard/[subdomain]/settings/general.tsx +++ b/src/pages/dashboard/[subdomain]/settings/general.tsx @@ -16,6 +16,8 @@ export default function SiteSettingsGeneralPage() { const siteResult = trpc.useQuery(["site", { site: subdomain }], { enabled: !!subdomain, + refetchOnWindowFocus: false, + refetchOnReconnect: false, }) const site = siteResult.data const updateSite = trpc.useMutation("site.updateSite") diff --git a/src/pages/dashboard/[subdomain]/settings/navigation.tsx b/src/pages/dashboard/[subdomain]/settings/navigation.tsx index a96408c6..5bc5d992 100644 --- a/src/pages/dashboard/[subdomain]/settings/navigation.tsx +++ b/src/pages/dashboard/[subdomain]/settings/navigation.tsx @@ -1,29 +1,86 @@ import { Button } from "~/components/ui/Button" import { Input } from "~/components/ui/Input" -import { FormEvent, useEffect, useState } from "react" +import { FormEvent, useEffect, useMemo, useState } from "react" import toast from "react-hot-toast" import { SettingsLayout } from "~/components/dashboard/SettingsLayout" import { useRouter } from "next/router" import { DashboardLayout } from "~/components/dashboard/DashboardLayout" import { trpc } from "~/lib/trpc" -import { useForm } from "react-hook-form" import { SiteNavigationItem } from "~/lib/types" import { nanoid } from "nanoid" +import { ReactSortable } from "react-sortablejs" +import equal from "fast-deep-equal" + +type UpdateItem = (id: string, newItem: Partial) => void + +type RemoveItem = (id: string) => void + +const SortableNavigationItem: React.FC<{ + item: SiteNavigationItem + updateItem: UpdateItem + removeItem: RemoveItem +}> = ({ item, updateItem, removeItem }) => { + return ( +
+
+ +
+ updateItem(item.id, { label: e.target.value })} + /> + updateItem(item.id, { url: e.target.value })} + /> +
+ +
+
+ ) +} export default function SiteSettingsNavigationPage() { const router = useRouter() - const [itemsModified, setItemsModified] = useState(false) const subdomain = router.query.subdomain as string - + const trpcContext = trpc.useContext() const siteResult = trpc.useQuery(["site", { site: subdomain }], { enabled: !!subdomain, + refetchOnWindowFocus: false, }) + const updateSite = trpc.useMutation("site.updateSite") const [items, setItems] = useState([]) - const updateItem = (id: string, newItem: Partial) => { + const itemsModified = useMemo(() => { + if (!siteResult.data) return false + return !equal(items, siteResult.data.navigation) + }, [items, siteResult.data]) + + const updateItem: UpdateItem = (id, newItem) => { setItems((items) => { return items.map((item) => { if (item.id === id) { @@ -32,7 +89,6 @@ export default function SiteSettingsNavigationPage() { return item }) }) - setItemsModified(true) } const newEmptyItem = () => { @@ -47,73 +103,56 @@ export default function SiteSettingsNavigationPage() { }) } - const removeItem = (id: string) => { + const removeItem: RemoveItem = (id) => { setItems((items) => items.filter((item) => item.id !== id)) - setItemsModified(true) } useEffect(() => { - if (!siteResult.isPreviousData && siteResult.data?.navigation) { + if (siteResult.data?.navigation) { setItems(siteResult.data.navigation) } - }, [siteResult.isPreviousData, siteResult.data]) + }, [siteResult.data?.navigation]) useEffect(() => { if (updateSite.isSuccess) { updateSite.reset() toast.success("Saved") - setItemsModified(true) + trpcContext.invalidateQueries(["site", { site: subdomain }]) } - }, [updateSite]) + }, [updateSite, trpcContext, subdomain]) return (
-
+
{items.length === 0 && ( -
+
No navigation items yet
)} - {items.map((item) => { - return ( -
- - updateItem(item.id, { label: e.target.value }) - } + {/** @ts-expect-error */} + + {items.map((item) => { + return ( + - - updateItem(item.id, { url: e.target.value }) - } - /> -
- -
-
- ) - })} + ) + })} + +