Skip to content
Merged
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
201 changes: 117 additions & 84 deletions apps/web/src/components/a4-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,76 @@
import { useLayoutEffect, useRef, useState } from 'react'
import {
RectangleHorizontal,
RectangleVertical,
Square,
} from 'lucide-react'
import { useLayoutEffect, useRef, useState } from "react";
import { RectangleHorizontal, RectangleVertical, Square } from "lucide-react";

import { SheetInspector } from '@/components/sheet-inspector'
import { SheetPage } from '@/components/sheet-page'
import { SheetInspector } from "@/components/sheet-inspector";
import { SheetPage } from "@/components/sheet-page";
import { StripPage } from "@/components/strip-page";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { type Orientation } from '@/lib/crop'
import { captionFontStack } from '@/lib/fonts'
import { FRAME_SHAPES, paperSize } from '@/lib/layout'
import { paginate } from '@/lib/pages'
import { cn } from '@/lib/utils'
import { usePhotoStore } from '@/stores/photo-store'
import { useSettingsStore } from '@/stores/settings-store'
} from "@/components/ui/tooltip";
import { type Orientation } from "@/lib/crop";
import { captionFontStack } from "@/lib/fonts";
import { FRAME_SHAPES, paperSize } from "@/lib/layout";
import { paginate } from "@/lib/pages";
import { paginateStrips } from "@/lib/strip";
import { cn } from "@/lib/utils";
import { usePhotoStore } from "@/stores/photo-store";
import { useSettingsStore } from "@/stores/settings-store";

const SHAPE_ICONS: Record<Orientation, typeof Square> = {
square: Square,
portrait: RectangleVertical,
landscape: RectangleHorizontal,
}
};

/** The centre column: the print sheet itself, with a floating frame inspector. */
export function A4Preview() {
const photos = usePhotoStore((state) => state.photos)
const captionFontId = useSettingsStore((state) => state.captionFontId)
const paperSizeId = useSettingsStore((state) => state.paperSizeId)
const frameShape = useSettingsStore((state) => state.frameShape)
const pageShapes = useSettingsStore((state) => state.pageShapes)
const setPageShape = useSettingsStore((state) => state.setPageShape)
const borderColor = useSettingsStore((state) => state.borderColor)
const borderWidth = useSettingsStore((state) => state.borderWidth)
const perRow = useSettingsStore((state) => state.polaroidsPerRow)
const rows = useSettingsStore((state) => state.rowsPerPage)
const showCutMarks = useSettingsStore((state) => state.showCutMarks)
const showCaptions = useSettingsStore((state) => state.showCaptions)
const showCameraLine = useSettingsStore((state) => state.showCameraLine)
const fontStack = captionFontStack(captionFontId)
const paper = paperSize(paperSizeId)
const photos = usePhotoStore((state) => state.photos);
const sheetFormat = useSettingsStore((state) => state.sheetFormat);
const stripsPerRow = useSettingsStore((state) => state.stripsPerRow);
const captionFontId = useSettingsStore((state) => state.captionFontId);
const paperSizeId = useSettingsStore((state) => state.paperSizeId);
const frameShape = useSettingsStore((state) => state.frameShape);
const pageShapes = useSettingsStore((state) => state.pageShapes);
const setPageShape = useSettingsStore((state) => state.setPageShape);
const borderColor = useSettingsStore((state) => state.borderColor);
const borderWidth = useSettingsStore((state) => state.borderWidth);
const perRow = useSettingsStore((state) => state.polaroidsPerRow);
const rows = useSettingsStore((state) => state.rowsPerPage);
const showCutMarks = useSettingsStore((state) => state.showCutMarks);
const showCaptions = useSettingsStore((state) => state.showCaptions);
const showCameraLine = useSettingsStore((state) => state.showCameraLine);
const fontStack = captionFontStack(captionFontId);
const paper = paperSize(paperSizeId);

const containerRef = useRef<HTMLDivElement>(null)
const [width, setWidth] = useState(0)
const containerRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);

useLayoutEffect(() => {
const el = containerRef.current
if (!el) return
const update = () => setWidth(el.clientWidth)
update()
const observer = new ResizeObserver(update)
observer.observe(el)
return () => observer.disconnect()
}, [])
const el = containerRef.current;
if (!el) return;
const update = () => setWidth(el.clientWidth);
update();
const observer = new ResizeObserver(update);
observer.observe(el);
return () => observer.disconnect();
}, []);

const pages = paginate(photos, perRow, rows, paper, frameShape, pageShapes, borderWidth)
const pageCount = pages.length
const gridPages = paginate(
photos,
perRow,
rows,
paper,
frameShape,
pageShapes,
borderWidth,
);
const stripPages = paginateStrips(photos, stripsPerRow, paper, borderWidth);
const pageCount =
sheetFormat === "strip" ? stripPages.length : gridPages.length;
const pageLabel = (page: number) =>
pageCount > 1 ? `Page ${page + 1} of ${pageCount}` : "";

return (
<section className="flex flex-col gap-3">
Expand All @@ -70,54 +82,75 @@ export function A4Preview() {
ref={containerRef}
className="mx-auto flex w-full max-w-xl flex-col gap-5"
>
{pages.map((slice, page) => (
<div
key={`page-${slice.photos[0]?.id ?? page}`}
className="flex flex-col gap-1"
>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">
{pageCount > 1 ? `Page ${page + 1} of ${pageCount}` : ''}
</span>
<PageShapeToggle
value={slice.shape}
onChange={(shape) => setPageShape(page, shape)}
/>
</div>
<SheetPage
photos={slice.photos}
width={width}
perRow={perRow}
rows={rows}
paper={paper}
shape={slice.shape}
borderColor={borderColor}
borderWidth={borderWidth}
fontStack={fontStack}
showCutMarks={showCutMarks}
showCaptions={showCaptions}
showCameraLine={showCameraLine}
editable
/>
</div>
))}
{sheetFormat === "strip"
? stripPages.map((stripPhotos, page) => (
<div
key={`strip-${stripPhotos[0]?.id ?? page}`}
className="flex flex-col gap-1"
>
<span className="text-muted-foreground text-xs">
{pageLabel(page)}
</span>
<StripPage
photos={stripPhotos}
width={width}
stripsPerRow={stripsPerRow}
paper={paper}
borderColor={borderColor}
borderWidth={borderWidth}
showCutMarks={showCutMarks}
editable
/>
</div>
))
: gridPages.map((slice, page) => (
<div
key={`page-${slice.photos[0]?.id ?? page}`}
className="flex flex-col gap-1"
>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">
{pageLabel(page)}
</span>
<PageShapeToggle
value={slice.shape}
onChange={(shape) => setPageShape(page, shape)}
/>
</div>
<SheetPage
photos={slice.photos}
width={width}
perRow={perRow}
rows={rows}
paper={paper}
shape={slice.shape}
borderColor={borderColor}
borderWidth={borderWidth}
fontStack={fontStack}
showCutMarks={showCutMarks}
showCaptions={showCaptions}
showCameraLine={showCameraLine}
editable
/>
</div>
))}
</div>
</section>
)
);
}

function PageShapeToggle({
value,
onChange,
}: {
value: Orientation
onChange: (shape: Orientation) => void
value: Orientation;
onChange: (shape: Orientation) => void;
}) {
return (
<div className="flex gap-0.5">
{FRAME_SHAPES.map(({ id, label }) => {
const Icon = SHAPE_ICONS[id]
const active = value === id
const Icon = SHAPE_ICONS[id];
const active = value === id;
return (
<Tooltip key={id}>
<TooltipTrigger asChild>
Expand All @@ -127,17 +160,17 @@ function PageShapeToggle({
aria-pressed={active}
onClick={() => onChange(id)}
className={cn(
'text-muted-foreground hover:bg-accent hover:text-foreground flex size-6 items-center justify-center rounded',
active && 'bg-accent text-foreground',
"text-muted-foreground hover:bg-accent hover:text-foreground flex size-6 items-center justify-center rounded",
active && "bg-accent text-foreground",
)}
>
<Icon className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent>{label} frames on this page</TooltipContent>
</Tooltip>
)
);
})}
</div>
)
);
}
Loading
Loading