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
4 changes: 2 additions & 2 deletions apps/web/app/(dashboard)/w/[slug]/tracked-links/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
Mail,
} from 'lucide-react';
import { trpc } from '@/lib/trpc/client';
import { formatDate, getRelativeTime } from '@/lib/utils';
import { formatDate, formatDuration, getRelativeTime } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { FileIcon } from '@/components/file-icon';
Expand Down Expand Up @@ -314,7 +314,7 @@ export default function TrackedLinkDetailPage({
label="Avg. Duration"
value={
analytics?.avgDurationSeconds != null
? `${analytics.avgDurationSeconds}s`
? formatDuration(analytics.avgDurationSeconds)
: '--'
}
sub={
Expand Down
102 changes: 88 additions & 14 deletions apps/web/app/shared/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
AlertCircle,
Folder,
ChevronRight,
ArrowLeft,
} from "lucide-react";
import { Logo } from "@/assets/logo";
import { trpc } from "@/lib/trpc/client";
import { formatBytes } from "@/lib/utils";
import { cn, formatBytes } from "@/lib/utils";
import { SharedFilePreview } from "@/features/files/shared-preview";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { FileIcon } from "@/components/file-icon";
Expand All @@ -25,6 +27,12 @@ export default function SharedPage({
const [password, setPassword] = useState("");
const [enteredPassword, setEnteredPassword] = useState<string | undefined>();
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
const [previewFile, setPreviewFile] = useState<{
fileId?: string;
name: string;
mimeType: string;
size: number;
} | null>(null);

const { data, isLoading } = trpc.shares.access.useQuery({
token,
Expand Down Expand Up @@ -54,6 +62,7 @@ export default function SharedPage({
);

const getDownloadUrl = trpc.shares.getDownloadUrl.useMutation();
const getPreviewUrl = trpc.shares.getPreviewUrl.useMutation();

const handleDownload = async (fileId?: string) => {
try {
Expand Down Expand Up @@ -172,7 +181,12 @@ export default function SharedPage({

return (
<div className="min-h-screen flex items-center justify-center bg-background p-6">
<div className="w-full max-w-md rounded-lg border bg-card p-6">
<div
className={cn(
"w-full rounded-lg border bg-card p-6",
previewFile ? "max-w-3xl" : "max-w-md",
)}
>
<div className="flex items-center gap-2 mb-6">
<Logo className="size-5 text-primary" />
<span className="title text-base">Locker</span>
Expand All @@ -181,9 +195,56 @@ export default function SharedPage({
</span>
</div>

{item.type === "file" ? (
{previewFile ? (
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-background rounded-sm border">
<div className="flex items-center gap-3">
<button
onClick={() => setPreviewFile(null)}
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground cursor-pointer shrink-0"
>
<ArrowLeft className="size-4" />
Back
</button>
<p className="text-sm font-medium text-foreground truncate flex-1">
{previewFile.name}
</p>
<Button
size="sm"
variant="outline"
onClick={() => handleDownload(previewFile.fileId)}
>
<Download className="size-3.5" />
Download
</Button>
</div>

<SharedFilePreview
key={previewFile.fileId ?? "root"}
file={previewFile}
fetchUrl={() =>
getPreviewUrl
.mutateAsync({
token,
fileId: previewFile.fileId,
password: enteredPassword,
})
.then((r) => r.url)
}
onDownload={() => handleDownload(previewFile.fileId)}
/>
</div>
) : item.type === "file" ? (
<div className="space-y-4">
<button
onClick={() =>
setPreviewFile({
name: item.name,
mimeType: item.mimeType ?? "",
size: item.size ?? 0,
})
}
className="flex items-center gap-3 p-3 w-full text-left bg-background rounded-sm border hover:bg-accent/50 cursor-pointer"
>
<FileIcon
name={item.name}
mimeType={item.mimeType}
Expand All @@ -197,7 +258,8 @@ export default function SharedPage({
{formatBytes(item.size ?? 0)}
</p>
</div>
</div>
<ChevronRight className="size-3.5 text-muted-foreground shrink-0" />
</button>

<Button className="w-full" onClick={() => handleDownload()}>
<Download className="size-3.5" />
Expand Down Expand Up @@ -265,16 +327,28 @@ export default function SharedPage({
{displayFiles?.map((file) => (
<div
key={file.id}
className="flex items-center gap-2.5 px-3 py-2"
className="flex items-center gap-2.5 px-3 py-2 hover:bg-accent/50"
>
<FileIcon
name={file.name}
mimeType={file.mimeType}
className="h-4 w-4 shrink-0"
/>
<span className="text-sm text-foreground truncate flex-1">
{file.name}
</span>
<button
onClick={() =>
setPreviewFile({
fileId: file.id,
name: file.name,
mimeType: file.mimeType,
size: file.size,
})
}
className="flex items-center gap-2.5 flex-1 min-w-0 text-left cursor-pointer"
>
<FileIcon
name={file.name}
mimeType={file.mimeType}
className="h-4 w-4 shrink-0"
/>
<span className="text-sm text-foreground truncate flex-1">
{file.name}
</span>
</button>
<span className="text-xs font-medium text-muted-foreground">
{formatBytes(file.size)}
</span>
Expand Down
109 changes: 95 additions & 14 deletions apps/web/app/t/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
Folder,
Mail,
ChevronRight,
ArrowLeft,
} from "lucide-react";
import { Logo } from "@/assets/logo";
import { trpc } from "@/lib/trpc/client";
import { formatBytes } from "@/lib/utils";
import { cn, formatBytes } from "@/lib/utils";
import { SharedFilePreview } from "@/features/files/shared-preview";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { FileIcon } from "@/components/file-icon";
Expand Down Expand Up @@ -39,6 +41,12 @@ export default function TrackedLinkPage({
const eventIdRef = useRef<string | null>(null);
const startTimeRef = useRef<number>(Date.now());
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
const [previewFile, setPreviewFile] = useState<{
fileId?: string;
name: string;
mimeType: string;
size: number;
} | null>(null);

const { data, isLoading } = trpc.trackedLinks.access.useQuery({
token,
Expand All @@ -57,6 +65,7 @@ export default function TrackedLinkPage({
);

const getDownloadUrl = trpc.trackedLinks.getDownloadUrl.useMutation();
const getPreviewUrl = trpc.trackedLinks.getPreviewUrl.useMutation();

// Send tracking beacon when access succeeds
const sendTrackingBeacon = useCallback(async () => {
Expand Down Expand Up @@ -279,7 +288,12 @@ export default function TrackedLinkPage({

return (
<div className="min-h-screen flex items-center justify-center bg-background p-6">
<div className="w-full max-w-md rounded-lg border bg-card p-6">
<div
className={cn(
"w-full rounded-lg border bg-card p-6",
previewFile ? "max-w-3xl" : "max-w-md",
)}
>
<div className="flex items-center gap-2 mb-6">
<Logo className="size-5 text-primary" />
<span className="title text-base">Locker</span>
Expand All @@ -288,9 +302,63 @@ export default function TrackedLinkPage({
</span>
</div>

{item.type === "file" ? (
{previewFile ? (
<div className="space-y-4">
<div className="flex items-center gap-3">
<button
onClick={() => setPreviewFile(null)}
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground cursor-pointer shrink-0"
>
<ArrowLeft className="size-4" />
Back
</button>
<p className="text-sm font-medium text-foreground truncate flex-1">
{previewFile.name}
</p>
{access === "download" && (
<Button
size="sm"
variant="outline"
onClick={() => handleDownload(previewFile.fileId)}
>
<Download className="size-3.5" />
Download
</Button>
)}
</div>

<SharedFilePreview
key={previewFile.fileId ?? "root"}
file={previewFile}
fetchUrl={() =>
getPreviewUrl
.mutateAsync({
token,
fileId: previewFile.fileId,
password: enteredPassword,
email: enteredEmail,
})
.then((r) => r.url)
}
onDownload={
access === "download"
? () => handleDownload(previewFile.fileId)
: undefined
}
/>
</div>
) : item.type === "file" ? (
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-background rounded-sm border">
<button
onClick={() =>
setPreviewFile({
name: item.name,
mimeType: item.mimeType ?? "",
size: item.size ?? 0,
})
}
className="flex items-center gap-3 p-3 w-full text-left bg-background rounded-sm border hover:bg-accent/50 cursor-pointer"
>
<FileIcon
name={item.name}
mimeType={item.mimeType}
Expand All @@ -304,7 +372,8 @@ export default function TrackedLinkPage({
{formatBytes(item.size ?? 0)}
</p>
</div>
</div>
<ChevronRight className="size-3.5 text-muted-foreground shrink-0" />
</button>

{access === "download" && (
<Button className="w-full" onClick={() => handleDownload()}>
Expand Down Expand Up @@ -374,16 +443,28 @@ export default function TrackedLinkPage({
{displayFiles?.map((file) => (
<div
key={file.id}
className="flex items-center gap-2.5 px-3 py-2"
className="flex items-center gap-2.5 px-3 py-2 hover:bg-accent/50"
>
<FileIcon
name={file.name}
mimeType={file.mimeType}
className="h-4 w-4 shrink-0"
/>
<span className="text-sm text-foreground truncate flex-1">
{file.name}
</span>
<button
onClick={() =>
setPreviewFile({
fileId: file.id,
name: file.name,
mimeType: file.mimeType,
size: file.size,
})
}
className="flex items-center gap-2.5 flex-1 min-w-0 text-left cursor-pointer"
>
<FileIcon
name={file.name}
mimeType={file.mimeType}
className="h-4 w-4 shrink-0"
/>
<span className="text-sm text-foreground truncate flex-1">
{file.name}
</span>
</button>
<span className="text-xs font-medium text-muted-foreground">
{formatBytes(file.size)}
</span>
Expand Down
23 changes: 10 additions & 13 deletions apps/web/components/pdf-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ export function PDFViewer({
}, [url, onLoadSuccess, onLoadError]);

/* ---- Observe container width ---- */
// Re-run when loading finishes: the scroll container only exists once the
// loading skeleton is replaced, so a mount-only effect would never attach.
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
Expand All @@ -536,7 +538,7 @@ export function PDFViewer({
});
observer.observe(container);
return () => observer.disconnect();
}, []);
}, [isLoading]);

/* ---- Observe visible pages ---- */
useEffect(() => {
Expand Down Expand Up @@ -612,17 +614,11 @@ export function PDFViewer({
[pages.length, scrollToPage, onPageChangeProp],
);

/* ---- Auto-scale on load ---- */
useEffect(() => {
if (pages.length === 0 || !containerWidth) return;
const firstPage = pages[0];
if (!firstPage) return;
const viewport = firstPage.getViewport({ scale: 1 });
const fitScale = (containerWidth - 48) / viewport.width;
if (fitScale < 1) setScale(fitScale);
}, [pages, containerWidth]);

/* ---- Zoom handlers ---- */
// PDFPage renders at scale * min(pageFitScale, 1), so pages wider than the
// container (e.g. landscape) already fit the full width at scale 1 — no
// auto-scale on load needed, and the fit handlers below must divide that
// clamp back out to hit an exact target scale.
const handleScaleChange = useCallback((s: number) => {
setScale(Math.max(MIN_SCALE, Math.min(MAX_SCALE, s)));
}, []);
Expand All @@ -632,7 +628,8 @@ export function PDFViewer({
const firstPage = pages[0];
if (!firstPage) return;
const vp = firstPage.getViewport({ scale: 1 });
setScale((containerWidth - 48) / vp.width);
const fitScale = (containerWidth - 48) / vp.width;
setScale(Math.max(fitScale, 1));
}, [pages, containerWidth]);

const handleFitPage = useCallback(() => {
Expand All @@ -645,7 +642,7 @@ export function PDFViewer({
const containerHeight = container.clientHeight - PAGE_GAP * 2;
const widthScale = (containerWidth - 48) / vp.width;
const heightScale = containerHeight / vp.height;
setScale(Math.min(widthScale, heightScale));
setScale(Math.min(widthScale, heightScale) / Math.min(widthScale, 1));
}, [pages, containerWidth]);

/* ---- Keyboard navigation ---- */
Expand Down
Loading