From 74200f468cf759728607d252383d201f41b4da05 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 1 Aug 2023 23:19:17 +0800 Subject: [PATCH] feat: add shortcut detail page --- web/src/components/ShortcutView.tsx | 82 ++++++------ web/src/helpers/api.ts | 4 + web/src/pages/ShortcutDetail.tsx | 191 ++++++++++++++++++++++++++++ web/src/routers/index.tsx | 10 ++ web/src/services/shortcutService.ts | 20 ++- web/src/stores/v1/shortcut.ts | 38 ++++++ 6 files changed, 296 insertions(+), 49 deletions(-) create mode 100644 web/src/pages/ShortcutDetail.tsx create mode 100644 web/src/stores/v1/shortcut.ts diff --git a/web/src/components/ShortcutView.tsx b/web/src/components/ShortcutView.tsx index 9ad47d7..eae9b26 100644 --- a/web/src/components/ShortcutView.tsx +++ b/web/src/components/ShortcutView.tsx @@ -3,6 +3,7 @@ import copy from "copy-to-clipboard"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; import { absolutifyLink } from "../helpers/utils"; import { shortcutService } from "../services"; import useFaviconStore from "../stores/v1/favicon"; @@ -31,7 +32,6 @@ const ShortcutView = (props: Props) => { const [showAnalyticsDialog, setShowAnalyticsDialog] = useState(false); const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id; const shortcutLink = absolutifyLink(`/s/${shortcut.name}`); - const compactStyle = viewStore.layout === "grid"; useEffect(() => { faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => { @@ -62,40 +62,47 @@ const ShortcutView = (props: Props) => {
-
+ {favicon ? ( ) : ( - + )} + +
+
+ + s/ + {shortcut.name} + + + + + + + + + + +
+ + {shortcut.link} +
- - s/ - {shortcut.name} - - - - - - - - - -
{havePermission && ( @@ -129,21 +136,6 @@ const ShortcutView = (props: Props) => { )}
- {shortcut.description && !compactStyle &&

{shortcut.description}

} -
- {shortcut.tags.map((tag) => { - return ( - viewStore.setFilter({ tag: tag })} - > - #{tag} - - ); - })} - {shortcut.tags.length === 0 && No tags} -
diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 11ed9d7..db5fae9 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -55,6 +55,10 @@ export function getShortcutList(shortcutFind?: ShortcutFind) { return axios.get(`/api/v1/shortcut?${queryList.join("&")}`); } +export function getShortcutById(id: number) { + return axios.get(`/api/v1/shortcut/${id}`); +} + export function createShortcut(shortcutCreate: ShortcutCreate) { return axios.post("/api/v1/shortcut", shortcutCreate); } diff --git a/web/src/pages/ShortcutDetail.tsx b/web/src/pages/ShortcutDetail.tsx new file mode 100644 index 0000000..7d55ca0 --- /dev/null +++ b/web/src/pages/ShortcutDetail.tsx @@ -0,0 +1,191 @@ +import { Tooltip } from "@mui/joy"; +import copy from "copy-to-clipboard"; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { useLoaderData } from "react-router-dom"; +import { showCommonDialog } from "../components/Alert"; +import AnalyticsDialog from "../components/AnalyticsDialog"; +import Dropdown from "../components/common/Dropdown"; +import CreateShortcutDialog from "../components/CreateShortcutDialog"; +import GenerateQRCodeDialog from "../components/GenerateQRCodeDialog"; +import Icon from "../components/Icon"; +import VisibilityIcon from "../components/VisibilityIcon"; +import { absolutifyLink } from "../helpers/utils"; +import { shortcutService } from "../services"; +import useFaviconStore from "../stores/v1/favicon"; +import useUserStore from "../stores/v1/user"; + +interface State { + showEditModal: boolean; +} + +const ShortcutDetail = () => { + const { t } = useTranslation(); + const shortcutId = (useLoaderData() as Shortcut).id; + const shortcut = shortcutService.getShortcutById(shortcutId) as Shortcut; + const currentUser = useUserStore().getCurrentUser(); + const faviconStore = useFaviconStore(); + const [state, setState] = useState({ + showEditModal: false, + }); + const [favicon, setFavicon] = useState(undefined); + const [showQRCodeDialog, setShowQRCodeDialog] = useState(false); + const [showAnalyticsDialog, setShowAnalyticsDialog] = useState(false); + const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id; + const shortcutLink = absolutifyLink(`/s/${shortcut.name}`); + + useEffect(() => { + faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => { + if (url) { + setFavicon(url); + } + }); + }, [shortcut.link]); + + const handleCopyButtonClick = () => { + copy(shortcutLink); + toast.success("Shortcut link copied to clipboard."); + }; + + const handleDeleteShortcutButtonClick = (shortcut: Shortcut) => { + showCommonDialog({ + title: "Delete Shortcut", + content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`, + style: "danger", + onConfirm: async () => { + await shortcutService.deleteShortcutById(shortcut.id); + }, + }); + }; + + return ( + <> +
+
+ {favicon ? ( + + ) : ( + + )} +
+ + s/ + {shortcut.name} + + + + +
+ + + + + + + {havePermission && ( + + + + + + } + > + )} +
+ {shortcut.description &&

{shortcut.description}

} +
+ {shortcut.tags.map((tag) => { + return ( + + #{tag} + + ); + })} + {shortcut.tags.length === 0 && No tags} +
+
+ +
+ + {shortcut.creator.nickname} +
+
+ +
+ + {t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)} +
+
+ +
setShowAnalyticsDialog(true)} + > + + {shortcut.view} visits +
+
+
+
+ + {showQRCodeDialog && setShowQRCodeDialog(false)} />} + + {state.showEditModal && ( + + setState({ + ...state, + showEditModal: false, + }) + } + /> + )} + + {showAnalyticsDialog && setShowAnalyticsDialog(false)} />} + + ); +}; + +export default ShortcutDetail; diff --git a/web/src/routers/index.tsx b/web/src/routers/index.tsx index f673a72..d918b6a 100644 --- a/web/src/routers/index.tsx +++ b/web/src/routers/index.tsx @@ -3,8 +3,10 @@ import App from "../App"; import Root from "../layouts/Root"; import Home from "../pages/Home"; import Setting from "../pages/Setting"; +import ShortcutDetail from "../pages/ShortcutDetail"; import SignIn from "../pages/SignIn"; import SignUp from "../pages/SignUp"; +import { shortcutService } from "../services"; const router = createBrowserRouter([ { @@ -27,6 +29,14 @@ const router = createBrowserRouter([ path: "", element: , }, + { + path: "/shortcut/:shortcutId", + element: , + loader: async ({ params }) => { + const shortcut = await shortcutService.getOrFetchShortcutById(Number(params.shortcutId)); + return shortcut; + }, + }, { path: "/setting", element: , diff --git a/web/src/services/shortcutService.ts b/web/src/services/shortcutService.ts index 9d5e7bb..8c189a8 100644 --- a/web/src/services/shortcutService.ts +++ b/web/src/services/shortcutService.ts @@ -29,13 +29,25 @@ const shortcutService = { }, getShortcutById: (id: ShortcutId) => { - for (const s of shortcutService.getState().shortcutList) { - if (s.id === id) { - return s; + for (const shortcut of shortcutService.getState().shortcutList) { + if (shortcut.id === id) { + return shortcut; + } + } + return null; + }, + + getOrFetchShortcutById: async (id: ShortcutId) => { + for (const shortcut of shortcutService.getState().shortcutList) { + if (shortcut.id === id) { + return shortcut; } } - return null; + const data = (await api.getShortcutById(id)).data; + const shortcut = convertResponseModelShortcut(data); + store.dispatch(createShortcut(shortcut)); + return shortcut; }, createShortcut: async (shortcutCreate: ShortcutCreate) => { diff --git a/web/src/stores/v1/shortcut.ts b/web/src/stores/v1/shortcut.ts new file mode 100644 index 0000000..bb5928b --- /dev/null +++ b/web/src/stores/v1/shortcut.ts @@ -0,0 +1,38 @@ +import { create } from "zustand"; +import * as api from "../../helpers/api"; + +const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { + return { + ...shortcut, + createdTs: shortcut.createdTs * 1000, + updatedTs: shortcut.updatedTs * 1000, + }; +}; + +interface ShortcutState { + shortcutMapById: Record; + getOrFetchShortcutById: (id: ShortcutId) => Promise; + getShortcutById: (id: ShortcutId) => Shortcut; +} + +const useShortcutStore = create()((set, get) => ({ + shortcutMapById: {}, + getOrFetchShortcutById: async (id: ShortcutId) => { + const shortcutMap = get().shortcutMapById; + if (shortcutMap[id]) { + return shortcutMap[id] as Shortcut; + } + + const { data } = await api.getShortcutById(id); + const shortcut = convertResponseModelShortcut(data); + shortcutMap[id] = shortcut; + set(shortcutMap); + return shortcut; + }, + getShortcutById: (id: ShortcutId) => { + const shortcutMap = get().shortcutMapById; + return shortcutMap[id] as Shortcut; + }, +})); + +export default useShortcutStore;