Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ const Card = ({ paper, onSelect, isSelected, isShow=true }: CardProps) => {
window.open(paperLink, "_blank");
}}
className={cn(
"cursor-pointer overflow-hidden rounded-sm border-2 border-[#734DFF] bg-[#FFFFFF] font-play transition-all duration-150 hover:bg-[#EFEAFF] dark:border-[#36266D] dark:bg-[#171720] hover:dark:bg-[#262635]",
isSelected && "ring-2 ring-[#7480FF] bg-[#EFEAFF]"
"cursor-pointer overflow-hidden rounded-sm border-2 border-[#734DFF] bg-[#FFFFFF] font-play shadow-sm transition-all duration-150 hover:-translate-y-0.5 hover:bg-[#EFEAFF] hover:shadow-md dark:border-[#36266D] dark:bg-[#171720] dark:shadow-none hover:dark:bg-[#262635]",
isSelected && "-translate-y-0.5 bg-[#EFEAFF] shadow-md ring-2 ring-[#7480FF]"
)}
>
<Image
Expand Down Expand Up @@ -168,7 +168,7 @@ const Card = ({ paper, onSelect, isSelected, isShow=true }: CardProps) => {
name={generateFileName(paper).replace(/\.[^.]+$/, "")}
className="h-full overflow-hidden"
height="100%"
hideControls={true}
hideControls={false}
backgroundColor="transparent"
hideScrollbar={true}
/>
Expand Down
23 changes: 11 additions & 12 deletions src/components/CatalogueContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Link from "next/link";
import { useCourses } from "@/context/courseContext";
import { FilterProvider, useFilters } from "@/context/filterContext";
import EmptyState from "./ui/EmptyState";
import SidebarButton from "./SidebarButton";
import SelectionToolbar from "./SelectionToolbar";
import SortComponent from "./ui/sorting";

const CatalogueContentInner = ({ subject }: { subject: string | null }) => {
Expand All @@ -39,6 +39,7 @@ const CatalogueContentInner = ({ subject }: { subject: string | null }) => {
selectedCampuses,
selectedAnswerKeyIncluded,
papers,
filteredPapers,
appliedFilters,
filtersPulled,
currentPage,
Expand All @@ -60,6 +61,7 @@ const CatalogueContentInner = ({ subject }: { subject: string | null }) => {
handleSelectAll,
handleDeselectAll,
handleDownloadSelected,
isDownloading,
} = useFilters();

useEffect(() => {
Expand Down Expand Up @@ -316,17 +318,14 @@ const CatalogueContentInner = ({ subject }: { subject: string | null }) => {
currentSort={sortOption}
/>

<SidebarButton onClick={handleSelectAll} className="order-2">
Select All
</SidebarButton>

<SidebarButton onClick={handleDeselectAll} className="order-2">
Deselect All
</SidebarButton>

<SidebarButton onClick={handleDownloadSelected} className="order-2">
Download Selected
</SidebarButton>
<SelectionToolbar
selectedCount={selectedPapers.length}
totalCount={(appliedFilters ? filteredPapers : papers).length}
onSelectAll={handleSelectAll}
onDeselectAll={handleDeselectAll}
onDownload={() => void handleDownloadSelected()}
isDownloading={isDownloading}
/>
</div>

{relatedSubjects.length > 0 && (
Expand Down
89 changes: 89 additions & 0 deletions src/components/SelectionToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client";

import React from "react";
import { CheckSquare, Square, Download, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";

interface SelectionToolbarProps {
selectedCount: number;
totalCount: number;
onSelectAll: () => void;
onDeselectAll: () => void;
onDownload: () => void;
isDownloading: boolean;
}

const SelectionToolbar: React.FC<SelectionToolbarProps> = ({
selectedCount,
totalCount,
onSelectAll,
onDeselectAll,
onDownload,
isDownloading,
}) => {
const allSelected = totalCount > 0 && selectedCount === totalCount;
const hasSelection = selectedCount > 0;

return (
<div className="flex items-center gap-2 rounded-full border-2 border-black bg-white p-1 pl-3 font-play text-xs font-semibold shadow-[2px_2px_0_0_#000] dark:border-[#434dba] dark:bg-[#0a0118] dark:shadow-[2px_2px_0_0_#434dba]">
<span
className={cn(
"min-w-[64px] select-none text-gray-500 transition-colors dark:text-gray-400",
hasSelection && "text-[#5B4CDB] dark:text-[#B2B8FF]",
)}
>
{selectedCount} / {totalCount} selected
</span>

<div className="h-5 w-px bg-black/10 dark:bg-white/10" />

<button
type="button"
onClick={allSelected ? onDeselectAll : onSelectAll}
className="flex items-center gap-1.5 rounded-full px-3 py-1.5 transition-colors hover:bg-[#B2B8FF] hover:text-black dark:hover:bg-[#434dba] dark:hover:text-white"
aria-pressed={allSelected}
>
{allSelected ? (
<CheckSquare size={16} className="text-[#7480FF]" />
) : (
<Square size={16} />
)}
<span className="hidden sm:inline">
{allSelected ? "Deselect All" : "Select All"}
</span>
<span className="sm:hidden">{allSelected ? "None" : "All"}</span>
</button>

<button
type="button"
onClick={onDownload}
disabled={!hasSelection || isDownloading}
aria-busy={isDownloading}
className={cn(
"flex items-center gap-1.5 rounded-full bg-black px-3 py-1.5 text-white transition-colors dark:bg-[#7480FF]",
hasSelection && !isDownloading
? "hover:bg-[#7480FF] dark:hover:bg-[#B2B8FF] dark:hover:text-black"
: "cursor-not-allowed opacity-40",
)}
>
{isDownloading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Download size={16} />
)}
<span className="hidden sm:inline">
{isDownloading
? "Zipping…"
: hasSelection
? `Download (${selectedCount})`
: "Download Selected"}
</span>
<span className="sm:hidden">
{isDownloading ? "Zipping…" : `Download${hasSelection ? ` (${selectedCount})` : ""}`}
</span>
</button>
</div>
);
};

export default SelectionToolbar;
132 changes: 127 additions & 5 deletions src/components/newPdfViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useZoom, ZoomPluginPackage, ZoomMode } from '@embedpdf/plugin-
import { RenderLayer, RenderPluginPackage } from '@embedpdf/plugin-render/react';
import { ExportPluginPackage } from '@embedpdf/plugin-export/react';

import { Download, ZoomIn, ZoomOut, Maximize2, Minimize2 } from "lucide-react";
import { Download, ZoomIn, ZoomOut, Maximize2, Minimize2, BookOpenText, X } from "lucide-react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { downloadFile } from "../lib/utils/download";
import { Button } from "./ui/button";
Expand All @@ -26,6 +26,10 @@ interface ControlProps {
isMobile: boolean;
isSmall: boolean;
viewerRef: React.RefObject<HTMLDivElement>;
isReadingMode: boolean;
toggleReadingMode: () => void;
showReadingCoachmark: boolean;
dismissReadingCoachmark: () => void;
}

interface PdfViewerProps {
Expand All @@ -37,6 +41,7 @@ interface PdfViewerProps {
backgroundColor?: string;
hideScrollbar?: boolean;
isolateFromTheme?: boolean;
onReadingModeChange?: (isReadingMode: boolean) => void;
}

interface WheelZoomProps {
Expand All @@ -58,7 +63,8 @@ function useBreakpoint() {
}

const Controls = memo(function Controls({documentId, toggleFullscreen, isFullscreen, onDownload,
forceMobile, isMobile, isSmall, viewerRef}: ControlProps) {
forceMobile, isMobile, isSmall, viewerRef, isReadingMode, toggleReadingMode,
showReadingCoachmark, dismissReadingCoachmark}: ControlProps) {

const { provides: zoomProv, state: zoomState } = useZoom(documentId);
const { provides: scrollProv, state: scrollState } = useScroll(documentId);
Expand Down Expand Up @@ -110,6 +116,23 @@ const pageChange = useCallback(

if (!zoomProv || !scrollProv) return null;

if (isReadingMode) {
return (
<button
onClick={toggleReadingMode}
title="Exit Reading Mode (R)"
className="group fixed bottom-5 left-1/2 z-[2] flex -translate-x-1/2 items-center gap-2 rounded-full bg-[#262635]/70 py-1.5 pl-3.5 pr-2 text-xs font-medium text-white/70 shadow-lg backdrop-blur transition-all hover:bg-[#262635] hover:text-white hover:opacity-100"
style={{ opacity: 0.55 }}
>
<BookOpenText size={13} />
Reading Mode
<span className="ml-1 flex h-5 w-5 items-center justify-center rounded-full bg-white/10 group-hover:bg-[#6536c1]">
<X size={11} />
</span>
</button>
);
}

const zoomIn = () => zoomProv.zoomIn();
const zoomOut = () => zoomProv.zoomOut();
const { zoomLevel } = zoomState;
Expand Down Expand Up @@ -146,6 +169,35 @@ const pageChange = useCallback(

const toolSet = (
<>
<div className="relative">
{showReadingCoachmark && (
<div className="absolute bottom-full right-0 z-[2] mb-3 w-52 animate-in fade-in slide-in-from-bottom-1 duration-300">
<div className="rounded-lg bg-[#6536c1] px-3 py-2.5 text-[11px] leading-snug text-white shadow-xl">
<span className="font-semibold">New: Reading Mode.</span> Hides
everything but the paper — lighter than Fullscreen, your tabs
stay put.
<button
onClick={dismissReadingCoachmark}
className="mt-1.5 block text-[10px] font-semibold underline underline-offset-2"
>
Got it
</button>
</div>
<div className="ml-auto mr-3.5 h-2 w-2 -translate-y-1 rotate-45 bg-[#6536c1]" />
</div>
)}
{showReadingCoachmark && (
<span className="absolute inset-0 animate-ping rounded bg-[#6536c1] opacity-40" />
)}
<Button
onClick={toggleReadingMode}
className="relative h-10 w-10 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]"
title="Reading Mode (R) — show just the paper"
>
<BookOpenText size={24} />
</Button>
</div>

<Button
onClick={toggleFullscreen}
className="h-10 w-10 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]"
Expand Down Expand Up @@ -406,11 +458,14 @@ export default function PDFViewer({
backgroundColor,
hideScrollbar = false,
isolateFromTheme = true,
onReadingModeChange,
}: PdfViewerProps) {
const { engine, isLoading } = usePdfiumEngine();
const { isMobile, isSmall } = useBreakpoint();
const { resolvedTheme } = useTheme();
const [isFullscreen, setIsFullscreen] = useState(false);
const [isReadingMode, setIsReadingMode] = useState(false);
const [showReadingCoachmark, setShowReadingCoachmark] = useState(false);
const viewerRef = useRef<HTMLDivElement>(null);
const effectiveBackgroundColor =
backgroundColor ?? (resolvedTheme === "light" ? "#F3F5FF" : "#070114");
Expand Down Expand Up @@ -441,6 +496,59 @@ export default function PDFViewer({
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
}, []);

const dismissReadingCoachmark = useCallback(() => {
setShowReadingCoachmark(false);
try {
window.localStorage.setItem("pdfReadingModeSeen", "1");
} catch {
// localStorage unavailable (private mode, etc) — coachmark just won't persist
}
}, []);

const toggleReadingMode = useCallback(() => {
setIsReadingMode((r) => {
const next = !r;
onReadingModeChange?.(next);
return next;
});
dismissReadingCoachmark();
}, [dismissReadingCoachmark, onReadingModeChange]);

useEffect(() => {
try {
if (!window.localStorage.getItem("pdfReadingModeSeen")) {
setShowReadingCoachmark(true);
}
} catch {
// ignore
}
}, []);

useEffect(() => {
if (!isReadingMode) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = prevOverflow;
};
}, [isReadingMode]);

useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null;
if (target && ["INPUT", "TEXTAREA"].includes(target.tagName)) return;

if (e.key === "r" || e.key === "R") {
toggleReadingMode();
} else if (e.key === "Escape" && isReadingMode) {
setIsReadingMode(false);
onReadingModeChange?.(false);
}
};
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
}, [isReadingMode, toggleReadingMode, onReadingModeChange]);

const plugins = useMemo(() => [
createPluginRegistration(DocumentManagerPluginPackage, {
initialDocuments: [{ url }],
Expand Down Expand Up @@ -503,14 +611,20 @@ if (isLoading || !engine) {
data-pdf-viewer-scrollbars={hideScrollbar ? "hidden" : "visible"}
className={className}
style={{
height,
height: isReadingMode ? "100dvh" : height,
width: "100%",
position: "relative",
position: isReadingMode ? "fixed" : "relative",
top: isReadingMode ? 0 : undefined,
left: isReadingMode ? 0 : undefined,
right: isReadingMode ? 0 : undefined,
bottom: isReadingMode ? 0 : undefined,
zIndex: isReadingMode ? 999 : undefined,
backgroundColor: effectiveBackgroundColor,
display: "flex",
flexDirection: "column",
colorScheme: isolateFromTheme ? "light" : undefined,
forcedColorAdjust: isolateFromTheme ? "none" : undefined,
transition: "height 0.2s ease",
}}
>
<EmbedPDF engine={engine} plugins={plugins}>
Expand All @@ -528,6 +642,10 @@ if (isLoading || !engine) {
isMobile={isMobile}
isSmall={isSmall}
viewerRef={viewerRef}
isReadingMode={isReadingMode}
toggleReadingMode={toggleReadingMode}
showReadingCoachmark={showReadingCoachmark}
dismissReadingCoachmark={dismissReadingCoachmark}
/>}
<DocumentContent documentId={activeDocumentId}>
{({ isLoaded }) => (
Expand Down Expand Up @@ -579,6 +697,10 @@ if (isLoading || !engine) {
isMobile={isMobile}
isSmall={isSmall}
viewerRef={viewerRef}
isReadingMode={isReadingMode}
toggleReadingMode={toggleReadingMode}
showReadingCoachmark={showReadingCoachmark}
dismissReadingCoachmark={dismissReadingCoachmark}
/>
)}
</>
Expand All @@ -588,4 +710,4 @@ if (isLoading || !engine) {
</div>
</>
);
}
}
2 changes: 1 addition & 1 deletion src/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-xl px-2 py-1.5 text-sm outline-none transition-colors hover:bg-[#1F2A3D] focus:bg-[#1F2A3D] text-white data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
"relative flex cursor-default select-none items-center gap-2 rounded-xl px-2 py-1.5 text-sm text-black outline-none transition-colors hover:bg-black/[0.06] focus:bg-black/[0.06] dark:text-white dark:hover:bg-[#1F2A3D] dark:focus:bg-[#1F2A3D] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className,
)}
Expand Down
Loading
Loading