chore: add shortcut space routes

This commit is contained in:
Steven
2023-12-17 23:27:01 +08:00
parent 43cda4e2fb
commit fb7fc2443f
21 changed files with 647 additions and 493 deletions

View File

@ -6,18 +6,18 @@ import { GetShortcutAnalyticsResponse } from "@/types/proto/api/v2/shortcut_serv
import Icon from "./Icon";
interface Props {
shortcutId: number;
shortcutName: string;
className?: string;
}
const AnalyticsView: React.FC<Props> = (props: Props) => {
const { shortcutId, className } = props;
const { shortcutName, className } = props;
const { t } = useTranslation();
const [analytics, setAnalytics] = useState<GetShortcutAnalyticsResponse | null>(null);
const [selectedDeviceTab, setSelectedDeviceTab] = useState<"os" | "browser">("browser");
useEffect(() => {
shortcutServiceClient.getShortcutAnalytics({ id: shortcutId }).then((response) => {
shortcutServiceClient.getShortcutAnalytics({ name: shortcutName }).then((response) => {
setAnalytics(response);
});
}, []);

View File

@ -25,7 +25,7 @@ import Icon from "./Icon";
import ResourceNameInput from "./ResourceNameInput";
interface Props {
shortcutId?: number;
shortcutName?: string;
initialShortcut?: Partial<Shortcut>;
onClose: () => void;
onConfirm?: () => void;
@ -36,7 +36,7 @@ interface State {
}
const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, shortcutId, initialShortcut } = props;
const { onClose, onConfirm, shortcutName, initialShortcut } = props;
const { t } = useTranslation();
const [state, setState] = useState<State>({
shortcutCreate: Shortcut.fromPartial({
@ -54,13 +54,13 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const shortcutList = shortcutStore.getShortcutList();
const [tag, setTag] = useState<string>("");
const tagSuggestions = uniq(shortcutList.map((shortcut) => shortcut.tags).flat());
const isCreating = isUndefined(shortcutId);
const isCreating = isUndefined(shortcutName);
const loadingState = useLoading(!isCreating);
const requestState = useLoading(false);
useEffect(() => {
if (shortcutId) {
const shortcut = shortcutStore.getShortcutById(shortcutId);
if (shortcutName) {
const shortcut = shortcutStore.getShortcutByName(shortcutName);
if (shortcut) {
setState({
...state,
@ -77,7 +77,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
loadingState.setFinish();
}
}
}, [shortcutId]);
}, [shortcutName]);
if (loadingState.isLoading) {
return null;
@ -183,15 +183,15 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
}
try {
if (shortcutId) {
if (shortcutName) {
const updatingShortcut = {
...state.shortcutCreate,
id: shortcutId,
name: shortcutName,
tags: tag.split(" ").filter(Boolean),
};
await shortcutStore.updateShortcut(
updatingShortcut,
getShortcutUpdateMask(shortcutStore.getShortcutById(updatingShortcut.id), updatingShortcut)
getShortcutUpdateMask(shortcutStore.getShortcutByName(updatingShortcut.name), updatingShortcut)
);
} else {
await shortcutStore.createShortcut({

View File

@ -31,7 +31,7 @@ const ShortcutActionsDropdown = (props: Props) => {
content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`,
style: "danger",
onConfirm: async () => {
await shortcutStore.deleteShortcut(shortcut.id);
await shortcutStore.deleteShortcut(shortcut.name);
},
});
};
@ -82,7 +82,7 @@ const ShortcutActionsDropdown = (props: Props) => {
{showEditDrawer && (
<CreateShortcutDrawer
shortcutId={shortcut.id}
shortcutName={shortcut.name}
onClose={() => setShowEditDrawer(false)}
onConfirm={() => setShowEditDrawer(false)}
/>

View File

@ -37,7 +37,10 @@ const ShortcutCard = (props: Props) => {
>
<div className="w-full flex flex-row justify-between items-center">
<div className="w-[calc(100%-16px)] flex flex-row justify-start items-center mr-1 shrink-0">
<Link to={`/shortcut/${shortcut.id}`} className={classNames("w-8 h-8 flex justify-center items-center overflow-clip shrink-0")}>
<Link
to={`/shortcut/${shortcut.name}`}
className={classNames("w-8 h-8 flex justify-center items-center overflow-clip shrink-0")}
>
{favicon ? (
<img className="w-full h-auto rounded" src={favicon} decoding="async" loading="lazy" />
) : (
@ -120,7 +123,7 @@ const ShortcutCard = (props: Props) => {
</Tooltip>
<Tooltip title="View count" variant="solid" placement="top" arrow>
<Link
to={`/shortcut/${shortcut.id}#analytics`}
to={`/shortcut/${shortcut.name}#analytics`}
className="w-auto leading-5 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap cursor-pointer text-gray-400 text-sm"
>
<Icon.BarChart2 className="w-4 h-auto mr-1 opacity-70" />

View File

@ -10,6 +10,11 @@ export const absolutifyLink = (rel: string): string => {
return anchor.href;
};
export const isURL = (str: string): boolean => {
const urlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i;
return urlRegex.test(str);
};
export const releaseGuard = () => {
return import.meta.env.MODE === "development";
};

View File

@ -14,7 +14,8 @@ import { Collection } from "@/types/proto/api/v2/collection_service";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
const CollectionSpace = () => {
const { collectionName } = useParams();
const params = useParams();
const collectionName = params["*"];
const { sm } = useResponsiveWidth();
const userStore = useUserStore();
const collectionStore = useCollectionStore();
@ -36,7 +37,7 @@ const CollectionSpace = () => {
setShortcuts([]);
for (const shortcutId of collection.shortcutIds) {
try {
const shortcut = await shortcutStore.getOrFetchShortcutById(shortcutId);
const shortcut = await shortcutStore.fetchShortcutById(shortcutId);
setShortcuts((shortcuts) => {
return [...shortcuts, shortcut];
});

View File

@ -28,11 +28,11 @@ interface State {
const ShortcutDetail = () => {
const { t } = useTranslation();
const params = useParams();
const shortcutName = params["*"] || "";
const navigateTo = useNavigateTo();
const shortcutId = Number(params.shortcutId);
const shortcutStore = useShortcutStore();
const userStore = useUserStore();
const shortcut = shortcutStore.getShortcutById(shortcutId);
const shortcut = shortcutStore.getShortcutByName(shortcutName);
const currentUser = useUserStore().getCurrentUser();
const [state, setState] = useState<State>({
showEditDrawer: false,
@ -46,11 +46,11 @@ const ShortcutDetail = () => {
useEffect(() => {
(async () => {
const shortcut = await shortcutStore.getOrFetchShortcutById(shortcutId);
const shortcut = await shortcutStore.getOrFetchShortcutByName(shortcutName);
await userStore.getOrFetchUserById(shortcut.creatorId);
loadingState.setFinish();
})();
}, [shortcutId]);
}, [shortcutName]);
if (loadingState.isLoading) {
return null;
@ -67,7 +67,7 @@ const ShortcutDetail = () => {
content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`,
style: "danger",
onConfirm: async () => {
await shortcutStore.deleteShortcut(shortcut.id);
await shortcutStore.deleteShortcut(shortcut.name);
navigateTo("/", {
replace: true,
});
@ -198,7 +198,7 @@ const ShortcutDetail = () => {
<Icon.BarChart2 className="w-6 h-auto mr-1" />
{t("analytics.self")}
</h3>
<AnalyticsView className="mt-4 w-full grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-4" shortcutId={shortcut.id} />
<AnalyticsView className="mt-4 w-full grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-4" shortcutName={shortcut.name} />
</div>
</div>
@ -206,7 +206,7 @@ const ShortcutDetail = () => {
{state.showEditDrawer && (
<CreateShortcutDrawer
shortcutId={shortcut.id}
shortcutName={shortcut.name}
onClose={() =>
setState({
...state,

View File

@ -0,0 +1,36 @@
import { useEffect } from "react";
import toast from "react-hot-toast";
import { useParams } from "react-router-dom";
import { isURL } from "@/helpers/utils";
import useShortcutStore from "@/stores/v1/shortcut";
const ShortcutSpace = () => {
const params = useParams();
const shortcutName = params["*"] || "";
const shortcutStore = useShortcutStore();
const shortcut = shortcutStore.getShortcutByName(shortcutName);
useEffect(() => {
(async () => {
try {
await shortcutStore.getOrFetchShortcutByName(shortcutName);
} catch (error: any) {
console.error(error);
toast.error(error.details);
}
})();
}, [shortcutName]);
if (!shortcut) {
return null;
}
if (isURL(shortcut.link)) {
window.location.href = shortcut.link;
return null;
}
return <div>{shortcut.link}</div>;
};
export default ShortcutSpace;

View File

@ -2,6 +2,7 @@ import { createBrowserRouter } from "react-router-dom";
import CollectionDashboard from "@/pages/CollectionDashboard";
import CollectionSpace from "@/pages/CollectionSpace";
import NotFound from "@/pages/NotFound";
import ShortcutSpace from "@/pages/ShortcutSpace";
import SignIn from "@/pages/SignIn";
import SignUp from "@/pages/SignUp";
import SubscriptionSetting from "@/pages/SubscriptionSetting";
@ -38,7 +39,7 @@ const router = createBrowserRouter([
element: <CollectionDashboard />,
},
{
path: "/shortcut/:shortcutId",
path: "/shortcut/*",
element: <ShortcutDetail />,
},
{
@ -56,7 +57,11 @@ const router = createBrowserRouter([
],
},
{
path: "c/:collectionName",
path: "s/*",
element: <ShortcutSpace />,
},
{
path: "c/*",
element: <CollectionSpace />,
},
{

View File

@ -4,50 +4,61 @@ import { shortcutServiceClient } from "@/grpcweb";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
interface ShortcutState {
shortcutMapById: Record<number, Shortcut>;
shortcutMapByName: Record<string, Shortcut>;
fetchShortcutList: () => Promise<Shortcut[]>;
getOrFetchShortcutById: (id: number) => Promise<Shortcut>;
getShortcutById: (id: number) => Shortcut;
fetchShortcutById: (id: number) => Promise<Shortcut>;
getOrFetchShortcutByName: (name: string) => Promise<Shortcut>;
getShortcutByName: (name: string) => Shortcut;
getShortcutList: () => Shortcut[];
createShortcut: (shortcut: Shortcut) => Promise<Shortcut>;
updateShortcut: (shortcut: Partial<Shortcut>, updateMask: string[]) => Promise<Shortcut>;
deleteShortcut: (id: number) => Promise<void>;
deleteShortcut: (name: string) => Promise<void>;
}
const useShortcutStore = create<ShortcutState>()((set, get) => ({
shortcutMapById: {},
shortcutMapByName: {},
fetchShortcutList: async () => {
const { shortcuts } = await shortcutServiceClient.listShortcuts({});
const shortcutMap = get().shortcutMapById;
const shortcutMap = get().shortcutMapByName;
shortcuts.forEach((shortcut) => {
shortcutMap[shortcut.id] = shortcut;
shortcutMap[shortcut.name] = shortcut;
});
set(shortcutMap);
return shortcuts;
},
getOrFetchShortcutById: async (id: number) => {
const shortcutMap = get().shortcutMapById;
if (shortcutMap[id]) {
return shortcutMap[id] as Shortcut;
}
const { shortcut } = await shortcutServiceClient.getShortcut({
fetchShortcutById: async (id: number) => {
const { shortcut } = await shortcutServiceClient.getShortcutById({
id: id,
});
if (!shortcut) {
throw new Error(`Shortcut with id ${id} not found`);
}
return shortcut;
},
getOrFetchShortcutByName: async (name: string) => {
const shortcutMap = get().shortcutMapByName;
if (shortcutMap[name]) {
return shortcutMap[name] as Shortcut;
}
shortcutMap[id] = shortcut;
const { shortcut } = await shortcutServiceClient.getShortcut({
name,
});
if (!shortcut) {
throw new Error(`Shortcut with name ${name} not found`);
}
shortcutMap[name] = shortcut;
set(shortcutMap);
return shortcut;
},
getShortcutById: (id: number) => {
const shortcutMap = get().shortcutMapById;
return shortcutMap[id] || unknownShortcut;
getShortcutByName: (name: string) => {
const shortcutMap = get().shortcutMapByName;
return shortcutMap[name] || unknownShortcut;
},
getShortcutList: () => {
return Object.values(get().shortcutMapById);
return Object.values(get().shortcutMapByName);
},
createShortcut: async (shortcut: Shortcut) => {
const { shortcut: createdShortcut } = await shortcutServiceClient.createShortcut({
@ -56,8 +67,8 @@ const useShortcutStore = create<ShortcutState>()((set, get) => ({
if (!createdShortcut) {
throw new Error(`Failed to create shortcut`);
}
const shortcutMap = get().shortcutMapById;
shortcutMap[createdShortcut.id] = createdShortcut;
const shortcutMap = get().shortcutMapByName;
shortcutMap[createdShortcut.name] = createdShortcut;
set(shortcutMap);
return createdShortcut;
},
@ -69,17 +80,17 @@ const useShortcutStore = create<ShortcutState>()((set, get) => ({
if (!updatedShortcut) {
throw new Error(`Failed to update shortcut`);
}
const shortcutMap = get().shortcutMapById;
shortcutMap[updatedShortcut.id] = updatedShortcut;
const shortcutMap = get().shortcutMapByName;
shortcutMap[updatedShortcut.name] = updatedShortcut;
set(shortcutMap);
return updatedShortcut;
},
deleteShortcut: async (id: number) => {
deleteShortcut: async (name: string) => {
await shortcutServiceClient.deleteShortcut({
id: id,
name,
});
const shortcutMap = get().shortcutMapById;
delete shortcutMap[id];
const shortcutMap = get().shortcutMapByName;
delete shortcutMap[name];
set(shortcutMap);
},
}));