340 lines
8.4 KiB
TypeScript
340 lines
8.4 KiB
TypeScript
import { useEffect } from "react";
|
|
import { createFileRoute } from "@tanstack/react-router";
|
|
import { create } from "zustand";
|
|
import clsx from "clsx";
|
|
import { Button } from "~/components/button";
|
|
import type { LinkBookmark } from "~/bookmark/bookmark";
|
|
|
|
const testBookmarks: LinkBookmark[] = [
|
|
{
|
|
kind: "link",
|
|
id: "1",
|
|
title: "Running a Docker container as a non-root user",
|
|
url: "https://test.website.com/article/123",
|
|
},
|
|
{
|
|
kind: "link",
|
|
id: "2",
|
|
title: "Running a Docker container as a non-root user",
|
|
url: "https://test.website.com/article/123",
|
|
},
|
|
];
|
|
|
|
const LAYOUT_MODE = {
|
|
popup: "popup",
|
|
sideBySide: "side-by-side",
|
|
} as const;
|
|
type LayoutMode = (typeof LAYOUT_MODE)[keyof typeof LAYOUT_MODE];
|
|
|
|
interface BookmarkPageState {
|
|
bookmarks: LinkBookmark[];
|
|
selectedBookmarkIndex: number;
|
|
isBookmarkItemExpanded: boolean;
|
|
isBookmarkPreviewOpened: boolean;
|
|
layoutMode: LayoutMode;
|
|
|
|
setBookmarkItemExpanded: (isExpanded: boolean) => void;
|
|
setBookmarkPreviewOpened: (isOpened: boolean) => void;
|
|
setLayoutMode: (mode: LayoutMode) => void;
|
|
selectBookmarkAt: (index: number) => void;
|
|
}
|
|
|
|
const useBookmarkPageStore = create<BookmarkPageState>()((set, get) => ({
|
|
bookmarks: testBookmarks,
|
|
selectedBookmarkIndex: 0,
|
|
isBookmarkItemExpanded: false,
|
|
isBookmarkPreviewOpened: false,
|
|
layoutMode: LAYOUT_MODE.popup,
|
|
|
|
setBookmarkItemExpanded(isExpanded: boolean) {
|
|
set({ isBookmarkItemExpanded: isExpanded });
|
|
},
|
|
|
|
setBookmarkPreviewOpened(isOpened: boolean) {
|
|
set({ isBookmarkPreviewOpened: isOpened });
|
|
},
|
|
|
|
setLayoutMode(mode: LayoutMode) {
|
|
set({ layoutMode: mode });
|
|
},
|
|
|
|
selectBookmarkAt(index: number) {
|
|
const bookmarks = get().bookmarks;
|
|
if (index >= 0 && index < bookmarks.length) {
|
|
set({ selectedBookmarkIndex: index });
|
|
}
|
|
},
|
|
}));
|
|
|
|
function Page() {
|
|
const setLayoutMode = useBookmarkPageStore((state) => state.setLayoutMode);
|
|
|
|
useEffect(() => {
|
|
function onKeyDown(event: KeyboardEvent) {
|
|
const state = useBookmarkPageStore.getState();
|
|
|
|
switch (event.key) {
|
|
case "ArrowDown":
|
|
state.selectBookmarkAt(state.selectedBookmarkIndex + 1);
|
|
break;
|
|
case "ArrowUp":
|
|
state.selectBookmarkAt(state.selectedBookmarkIndex - 1);
|
|
break;
|
|
case "ArrowLeft":
|
|
state.setBookmarkItemExpanded(false);
|
|
break;
|
|
case "ArrowRight":
|
|
state.setBookmarkItemExpanded(true);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
|
|
return () => {
|
|
window.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
}, [useBookmarkPageStore]);
|
|
|
|
useEffect(() => {
|
|
function mediaQueryListener(this: MediaQueryList) {
|
|
if (this.matches) {
|
|
setLayoutMode(LAYOUT_MODE.sideBySide);
|
|
} else {
|
|
setLayoutMode(LAYOUT_MODE.popup);
|
|
}
|
|
}
|
|
|
|
const q = window.matchMedia("(width >= 64rem)");
|
|
q.addEventListener("change", mediaQueryListener);
|
|
|
|
mediaQueryListener.call(q);
|
|
|
|
return () => {
|
|
q.removeEventListener("change", mediaQueryListener);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="flex justify-center h-full">
|
|
<Main>
|
|
<div className="flex flex-col md:flex-row justify-center py-16 lg:py-32 ">
|
|
<header className="mb-4 md:mb-0 md:mr-16 text-start">
|
|
<h1 className="font-bold text-start">
|
|
<span className="invisible md:hidden">> </span>
|
|
YOUR BOOKMARKS
|
|
</h1>
|
|
</header>
|
|
<div className="flex flex-col container max-w-2xl -mt-2">
|
|
{testBookmarks.map((bookmark, i) => (
|
|
<BookmarkListItem
|
|
key={bookmark.id}
|
|
index={i}
|
|
bookmark={bookmark}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<BookmarkPreview />
|
|
</Main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Main({ children }: React.PropsWithChildren) {
|
|
const isPreviewOpened = useBookmarkPageStore(
|
|
(state) => state.isBookmarkPreviewOpened,
|
|
);
|
|
const layoutMode = useBookmarkPageStore((state) => state.layoutMode);
|
|
|
|
return (
|
|
<main
|
|
className={clsx(
|
|
"px-4 lg:px-8 2xl:px-0 grid flex justify-center relative w-full",
|
|
isPreviewOpened && layoutMode === LAYOUT_MODE.sideBySide
|
|
? "grid-cols-2"
|
|
: "grid-cols-1",
|
|
)}
|
|
>
|
|
{children}
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function BookmarkPreview() {
|
|
const isVisible = useBookmarkPageStore(
|
|
(state) => state.isBookmarkPreviewOpened,
|
|
);
|
|
const layoutMode = useBookmarkPageStore((state) => state.layoutMode);
|
|
|
|
if (!isVisible) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"h-screen flex justify-center items-center border-l border-stone-700 dark:border-stone-300 flex dark:bg-stone-900",
|
|
{
|
|
"absolute inset-0 border-l-0": layoutMode === LAYOUT_MODE.popup,
|
|
},
|
|
)}
|
|
>
|
|
<p>Content here</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BookmarkListItem({
|
|
bookmark,
|
|
index,
|
|
}: { bookmark: LinkBookmark; index: number }) {
|
|
const url = new URL(bookmark.url);
|
|
const selectedBookmark = useBookmarkPageStore(
|
|
(state) => state.bookmarks[state.selectedBookmarkIndex],
|
|
);
|
|
const isSelected = selectedBookmark.id === bookmark.id;
|
|
const isBookmarkItemExpanded = useBookmarkPageStore(
|
|
(state) => state.isBookmarkItemExpanded,
|
|
);
|
|
const setBookmarkItemExpanded = useBookmarkPageStore(
|
|
(state) => state.setBookmarkItemExpanded,
|
|
);
|
|
const selectBookmarkAt = useBookmarkPageStore(
|
|
(state) => state.selectBookmarkAt,
|
|
);
|
|
const setBookmarkPreviewOpened = useBookmarkPageStore(
|
|
(state) => state.setBookmarkPreviewOpened,
|
|
);
|
|
|
|
function expandOrOpenPreview() {
|
|
setBookmarkItemExpanded(true);
|
|
if (useBookmarkPageStore.getState().layoutMode === LAYOUT_MODE.sideBySide) {
|
|
console.log(useBookmarkPageStore.getState().layoutMode);
|
|
setBookmarkPreviewOpened(true);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={clsx("group flex flex-row justify-start py-2", {
|
|
"bg-teal-600 text-stone-100": isBookmarkItemExpanded && isSelected,
|
|
"text-teal-600": isSelected && !isBookmarkItemExpanded,
|
|
})}
|
|
onMouseEnter={() => {
|
|
if (!isBookmarkItemExpanded) {
|
|
selectBookmarkAt(index);
|
|
}
|
|
}}
|
|
>
|
|
<button
|
|
disabled={!isSelected}
|
|
className={clsx(
|
|
"select-none flex items-start font-bold hover:bg-teal-600 hover:text-stone-100",
|
|
{
|
|
invisible: !isSelected,
|
|
},
|
|
)}
|
|
onClick={() => {
|
|
setBookmarkItemExpanded(!isBookmarkItemExpanded);
|
|
setBookmarkPreviewOpened(false);
|
|
}}
|
|
>
|
|
<span className="sr-only">Options for this bookmark</span>
|
|
<span> </span>
|
|
<span className={isBookmarkItemExpanded ? "rotate-90" : ""}>></span>
|
|
<span> </span>
|
|
</button>
|
|
<div className="flex flex-col w-full">
|
|
<button className="text-start font-bold" onClick={expandOrOpenPreview}>
|
|
{bookmark.title}
|
|
</button>
|
|
<p className="opacity-80 text-sm">{url.host}</p>
|
|
{isBookmarkItemExpanded && isSelected ? (
|
|
<div className="flex flex-col space-y-1 md:flex-row md:space-y-0 md:space-x-2 items-end justify-between pt-2">
|
|
<p className="text-sm">
|
|
#dev #devops #devops #devops #devops #devops #devops
|
|
</p>
|
|
<div className="flex space-x-2">
|
|
<OpenBookmarkPreviewButton />
|
|
<Button className="text-sm">
|
|
<span className="underline">E</span>dit
|
|
</Button>
|
|
<Button className="text-sm">
|
|
<span className="underline">D</span>elete
|
|
</Button>
|
|
<span className="-ml-2"> </span>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OpenBookmarkPreviewButton() {
|
|
const isBookmarkPreviewOpened = useBookmarkPageStore(
|
|
(state) => state.isBookmarkPreviewOpened,
|
|
);
|
|
const setBookmarkPreviewOpened = useBookmarkPageStore(
|
|
(state) => state.setBookmarkPreviewOpened,
|
|
);
|
|
const setBookmarkItemExpanded = useBookmarkPageStore(
|
|
(state) => state.setBookmarkItemExpanded,
|
|
);
|
|
|
|
useEffect(() => {
|
|
function onKeyDown(event: KeyboardEvent) {
|
|
if (isBookmarkPreviewOpened && event.key === "c") {
|
|
closePreview();
|
|
} else if (!isBookmarkPreviewOpened && event.key === "o") {
|
|
openPreview();
|
|
}
|
|
}
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
|
|
return () => {
|
|
window.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
}, [isBookmarkPreviewOpened]);
|
|
|
|
function closePreview() {
|
|
setBookmarkPreviewOpened(false);
|
|
setBookmarkItemExpanded(false);
|
|
}
|
|
|
|
function openPreview() {
|
|
setBookmarkPreviewOpened(true);
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
className="text-sm"
|
|
onClick={() => {
|
|
if (isBookmarkPreviewOpened) {
|
|
closePreview();
|
|
} else {
|
|
openPreview();
|
|
}
|
|
}}
|
|
>
|
|
{isBookmarkPreviewOpened ? (
|
|
<>
|
|
<span className="underline">C</span>lose
|
|
</>
|
|
) : (
|
|
<>
|
|
<span className="underline">O</span>pen
|
|
</>
|
|
)}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
export const Route = createFileRoute("/$username/bookmarks")({
|
|
component: Page,
|
|
});
|