Files
markone/src/app/$username/bookmarks.tsx

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">&gt;&nbsp;</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>&nbsp;</span>
<span className={isBookmarkItemExpanded ? "rotate-90" : ""}>&gt;</span>
<span>&nbsp;</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">&nbsp;</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,
});