diff --git a/src/components/books/book-item.tsx b/src/components/books/book-item.tsx index f5e1dc9..ee2bc72 100644 --- a/src/components/books/book-item.tsx +++ b/src/components/books/book-item.tsx @@ -8,6 +8,7 @@ import { AspectRatio } from "../ui/aspect-ratio"; import { Skeleton } from "../ui/skeleton"; import { BookOpen, DownloadIcon } from "lucide-react"; import { EpubReader } from "./epub-reader"; +import { BookmarkButton } from "./bookmark"; type BookItemProps = BookItemResponse; @@ -18,7 +19,11 @@ export function BookItem(props: BookItemProps) { return ( - + +
+ +
+
@@ -35,7 +40,7 @@ export function BookItem(props: BookItemProps) {
-

{props.title}

+

{props.title}

By {props.authors}

{props.book_content}

Language: {props.book_lang}

diff --git a/src/components/books/bookmark.tsx b/src/components/books/bookmark.tsx new file mode 100644 index 0000000..04fc8b7 --- /dev/null +++ b/src/components/books/bookmark.tsx @@ -0,0 +1,27 @@ +import { BookItemResponse } from "@/api/backend/types"; +import { useBookmarksStore } from "@/stores/bookmarks"; +import { Button } from "../ui/button"; +import { useMemo } from "react"; +import { BookmarkMinus, BookmarkPlus } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; + +export function BookmarkButton({ book }: { book: BookItemResponse }) { + const bookmarks = useBookmarksStore((state) => state.bookmarks); + const addBookmark = useBookmarksStore((state) => state.addBookmark); + const removeBookmark = useBookmarksStore((state) => state.removeBookmark); + + const bookMarkedBook = useMemo(() => bookmarks.find((b) => b.md5 === book.md5), [bookmarks, book.md5]); + + return ( + + + + + + {bookMarkedBook ? "Remove bookmark" : "Add bookmark"} + + + ); +} diff --git a/src/components/layout/menu.tsx b/src/components/layout/menu.tsx index f937d91..a2861a9 100644 --- a/src/components/layout/menu.tsx +++ b/src/components/layout/menu.tsx @@ -38,22 +38,34 @@ export function Menu({ isOpen }: MenuProps) { ) : null} - {menus.map(({ href, label, icon: Icon, active, submenus }, index) => + {menus.map(({ href, label, icon: Icon, active, submenus, disabled }, index) => submenus.length === 0 ? (
- + - +

+ {label} +

+ +
{isOpen === false && {label}} + {disabled && Coming soon}
diff --git a/src/constants.ts b/src/constants.ts index 8196972..8a61022 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,7 @@ export const PAGE_TITLES: Partial, string>> "/": "Home", "/about": "About", "/account": "Account", + "/lists": "Lists", "/contact": "Contact", "/featured": "Featured", "/library": "Library", diff --git a/src/lib/layout.ts b/src/lib/layout.ts index c14709e..8f0fb5d 100644 --- a/src/lib/layout.ts +++ b/src/lib/layout.ts @@ -1,6 +1,6 @@ import { routeTree } from "@/routeTree.gen"; import { RoutePaths } from "@tanstack/react-router"; -import { Blocks, House, LucideIcon, BookMarked, Star, Upload } from "lucide-react"; +import { Blocks, House, LucideIcon, BookMarked, Star, Upload, BookOpenText } from "lucide-react"; export type Submenu = { href: RoutePaths; @@ -14,6 +14,7 @@ type Menu = { active: boolean; icon: LucideIcon; submenus: Submenu[]; + disabled?: boolean; }; type Group = { @@ -39,6 +40,7 @@ export function getMenuList(pathname: RoutePaths | string): Gr active: pathname === "/library", icon: BookMarked, submenus: [], + disabled: import.meta.env.PROD, }, { href: "/upload", @@ -46,6 +48,7 @@ export function getMenuList(pathname: RoutePaths | string): Gr active: pathname === "/upload", icon: Upload, submenus: [], + disabled: import.meta.env.PROD, }, ], }, @@ -58,6 +61,14 @@ export function getMenuList(pathname: RoutePaths | string): Gr active: pathname === "/account", icon: House, submenus: [], + disabled: import.meta.env.PROD, + }, + { + href: "/lists", + label: "Lists", + active: pathname === "/lists", + icon: BookOpenText, + submenus: [], }, { href: "/settings", diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 866b28a..61fde57 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as UploadImport } from './routes/upload' import { Route as SettingsImport } from './routes/settings' import { Route as RegisterImport } from './routes/register' import { Route as LoginImport } from './routes/login' +import { Route as ListsImport } from './routes/lists' import { Route as LibraryImport } from './routes/library' import { Route as FeaturedImport } from './routes/featured' import { Route as ContactImport } from './routes/contact' @@ -44,6 +45,11 @@ const LoginRoute = LoginImport.update({ getParentRoute: () => rootRoute, } as any) +const ListsRoute = ListsImport.update({ + path: '/lists', + getParentRoute: () => rootRoute, +} as any) + const LibraryRoute = LibraryImport.update({ path: '/library', getParentRoute: () => rootRoute, @@ -120,6 +126,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibraryImport parentRoute: typeof rootRoute } + '/lists': { + id: '/lists' + path: '/lists' + fullPath: '/lists' + preLoaderRoute: typeof ListsImport + parentRoute: typeof rootRoute + } '/login': { id: '/login' path: '/login' @@ -160,6 +173,7 @@ export const routeTree = rootRoute.addChildren({ ContactRoute, FeaturedRoute, LibraryRoute, + ListsRoute, LoginRoute, RegisterRoute, SettingsRoute, @@ -180,6 +194,7 @@ export const routeTree = rootRoute.addChildren({ "/contact", "/featured", "/library", + "/lists", "/login", "/register", "/settings", @@ -204,6 +219,9 @@ export const routeTree = rootRoute.addChildren({ "/library": { "filePath": "library.tsx" }, + "/lists": { + "filePath": "lists.tsx" + }, "/login": { "filePath": "login.tsx" }, diff --git a/src/routes/account.tsx b/src/routes/account.tsx index b4bf857..5c791e1 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -12,6 +12,7 @@ import { Trash2 } from "lucide-react"; export const Route = createFileRoute("/account")({ component: Account, beforeLoad: (ctx) => { + if (import.meta.env.PROD) throw redirect({ to: "/", search: { q: "" } }); if (!ctx.context.auth.isLoggedIn) throw redirect({ to: "/login" }); }, }); diff --git a/src/routes/library.tsx b/src/routes/library.tsx index 24d38dc..f66ac8e 100644 --- a/src/routes/library.tsx +++ b/src/routes/library.tsx @@ -1,10 +1,12 @@ -import * as React from "react"; import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/library")({ component: Library, + beforeLoad: () => { + if (import.meta.env.PROD) throw redirect({ to: "/", search: { q: "" } }); + }, }); function Library() { diff --git a/src/routes/lists.tsx b/src/routes/lists.tsx new file mode 100644 index 0000000..340158f --- /dev/null +++ b/src/routes/lists.tsx @@ -0,0 +1,25 @@ +import { BookItem } from "@/components/books/book-item"; +import { useBookmarksStore } from "@/stores/bookmarks"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/lists")({ + component: Lists, +}); + +export function Lists() { + const bookmarks = useBookmarksStore((state) => state.bookmarks); + + return ( +
+
+ {bookmarks.length > 0 ?

Your Bookmarks

: null} + +
+ {bookmarks.map((bookmark) => ( + + ))} +
+
+
+ ); +} diff --git a/src/routes/login.tsx b/src/routes/login.tsx index de1a01e..05973d2 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/componen import { Button } from "@/components/ui/button"; import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/components/ui/input-otp"; import { useIsMobile } from "@/hooks/use-ismobile"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, redirect } from "@tanstack/react-router"; import { useRouter } from "@tanstack/react-router"; import { z } from "zod"; import { useForm } from "react-hook-form"; @@ -16,6 +16,9 @@ import { TurnstileWidget } from "@/components/layout/turnstile"; export const Route = createFileRoute("/login")({ component: Login, + beforeLoad: () => { + if (import.meta.env.PROD) throw redirect({ to: "/", search: { q: "" } }); + }, }); const loginFormSchema = z.object({ diff --git a/src/routes/register.tsx b/src/routes/register.tsx index 5e16c82..95554c6 100644 --- a/src/routes/register.tsx +++ b/src/routes/register.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useNavigate } from "@tanstack/react-router"; +import { redirect, useNavigate } from "@tanstack/react-router"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; @@ -18,6 +18,9 @@ import { TurnstileWidget } from "@/components/layout/turnstile"; export const Route = createFileRoute("/register")({ component: Register, + beforeLoad: () => { + if (import.meta.env.PROD) throw redirect({ to: "/", search: { q: "" } }); + }, loader: async (opts) => { await opts.context.queryClient.ensureQueryData(randomWordsWithNumberQueryOptions); }, diff --git a/src/routes/upload.tsx b/src/routes/upload.tsx index 43c1b0f..4c7c01c 100644 --- a/src/routes/upload.tsx +++ b/src/routes/upload.tsx @@ -1,10 +1,13 @@ import * as React from "react"; import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/upload")({ component: Upload, + beforeLoad: () => { + if (import.meta.env.PROD) throw redirect({ to: "/", search: { q: "" } }); + }, }); function Upload() { diff --git a/src/stores/bookmarks.ts b/src/stores/bookmarks.ts new file mode 100644 index 0000000..a355243 --- /dev/null +++ b/src/stores/bookmarks.ts @@ -0,0 +1,23 @@ +import { BookItemResponse } from "@/api/backend/types"; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface BookmarksStoreState { + bookmarks: BookItemResponse[]; + addBookmark: (bookmark: BookItemResponse) => void; + removeBookmark: (md5: string) => void; +} + +export const useBookmarksStore = create()( + persist( + (set) => ({ + bookmarks: [], + addBookmark: (bookmark) => set((state) => ({ bookmarks: [...state.bookmarks, bookmark] })), + removeBookmark: (md5) => set((state) => ({ bookmarks: state.bookmarks.filter((b) => b.md5 !== md5) })), + }), + { + name: "BR::bookmarks", + getStorage: () => localStorage, + }, + ), +);