initial commit
This commit is contained in:
339
src/app/$username/bookmarks.tsx
Normal file
339
src/app/$username/bookmarks.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
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";
|
||||
|
||||
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,
|
||||
});
|
Reference in New Issue
Block a user