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
8 changes: 2 additions & 6 deletions apps/web/src/components/a4-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,8 @@ export function A4Preview() {

return (
<section className="flex flex-col gap-3">
<h2 className="text-center text-sm font-medium">
Print sheet ({paper.label})
{pageCount > 1 ? ` — ${pageCount} pages` : ''}
</h2>
{/* Floats over the page so the selected frame's tools follow you. */}
<div className="sticky top-2 z-20 mx-auto w-full max-w-xl empty:hidden">
{/* Pinned to the screen, so the selected frame's tools never shift the page. */}
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-30 flex justify-center px-4">
<SheetInspector />
</div>
<div
Expand Down
62 changes: 47 additions & 15 deletions apps/web/src/components/options-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type ReactNode, useState } from 'react'
import { Minus, Plus } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { DATE_FORMATS, type DateFormat } from '@/lib/date'
Expand Down Expand Up @@ -143,21 +144,12 @@ export function OptionsPanel() {
</select>
</Field>
<Field label="Per row" htmlFor="opt-perrow">
<span className="flex items-center gap-2">
<input
id="opt-perrow"
type="range"
min={MIN_PER_ROW}
max={MAX_PER_ROW}
step={1}
value={perRow}
onChange={(event) => setPerRow(Number(event.target.value))}
className="accent-primary w-24"
/>
<span className="text-muted-foreground w-3 text-sm tabular-nums">
{perRow}
</span>
</span>
<Stepper
value={perRow}
min={MIN_PER_ROW}
max={MAX_PER_ROW}
onChange={setPerRow}
/>
</Field>
<Toggle
label="Cut marks"
Expand All @@ -177,6 +169,46 @@ export function OptionsPanel() {
)
}

function Stepper({
value,
min,
max,
onChange,
}: {
value: number
min: number
max: number
onChange: (value: number) => void
}) {
return (
<span className="flex items-center gap-1">
<Button
type="button"
variant="outline"
size="icon"
className="size-7"
aria-label="Fewer per row"
disabled={value <= min}
onClick={() => onChange(Math.max(min, value - 1))}
>
<Minus className="size-3.5" />
</Button>
<span className="w-4 text-center text-sm tabular-nums">{value}</span>
<Button
type="button"
variant="outline"
size="icon"
className="size-7"
aria-label="More per row"
disabled={value >= max}
onClick={() => onChange(Math.min(max, value + 1))}
>
<Plus className="size-3.5" />
</Button>
</span>
)
}

function Section({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="flex flex-col gap-2.5">
Expand Down
69 changes: 0 additions & 69 deletions apps/web/src/components/photo-dropzone.tsx

This file was deleted.

69 changes: 69 additions & 0 deletions apps/web/src/components/photo-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useCallback, useState } from 'react'
import { type FileRejection, useDropzone } from 'react-dropzone'
import { ImagePlus } from 'lucide-react'

import { PhotoStrip } from '@/components/photo-strip'
import { Button } from '@/components/ui/button'
import { usePhotoStore } from '@/stores/photo-store'

/**
* Left rail: an "Add photos" button on top, the reorderable strip below.
* The whole rail is a drop target — but stays a clean button until you drag,
* when a square drop pad fades in over a blurred strip.
*/
export function PhotoSidebar() {
const addFiles = usePhotoStore((state) => state.addFiles)
const [rejectedCount, setRejectedCount] = useState(0)

const onDrop = useCallback(
(accepted: File[], rejections: FileRejection[]) => {
if (accepted.length > 0) addFiles(accepted)
setRejectedCount(rejections.length)
},
[addFiles],
)

const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
onDrop,
noClick: true,
noKeyboard: true,
accept: {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/webp': ['.webp'],
'image/heic': ['.heic'],
'image/heif': ['.heif'],
},
})

return (
<div {...getRootProps()} className="relative flex flex-1 flex-col gap-3">
<input {...getInputProps()} />

<Button type="button" onClick={open} className="w-full gap-2">
<ImagePlus className="size-4" />
Add photos
</Button>

{rejectedCount > 0 && (
<p className="text-destructive text-xs">
{rejectedCount} file{rejectedCount > 1 ? 's' : ''} skipped — only
images are supported.
</p>
)}

<PhotoStrip />

{isDragActive && (
<div className="bg-background/70 absolute inset-0 z-30 flex items-start justify-center rounded-lg backdrop-blur-sm">
<div className="border-ring text-muted-foreground flex aspect-square w-full max-w-[220px] flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed text-center">
<ImagePlus className="size-8" />
<p className="text-foreground text-sm font-medium">
Drop your photos…
</p>
</div>
</div>
)}
</div>
)
}
18 changes: 10 additions & 8 deletions apps/web/src/components/sheet-inspector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Check,
RectangleHorizontal,
RectangleVertical,
Square,
Expand Down Expand Up @@ -35,9 +36,9 @@ export function SheetInspector() {
if (!photo) return null

return (
<div className="border-input bg-card flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border p-2">
<span className="text-muted-foreground text-xs font-medium">
Drag the photo to reposition
<div className="border-input bg-card pointer-events-auto flex items-center gap-x-4 rounded-full border py-1.5 pr-1.5 pl-3 shadow-lg">
<span className="text-muted-foreground hidden text-xs font-medium sm:inline">
Drag to reposition
</span>
<div className="flex gap-0.5">
{ORIENTATIONS.map((orientation) => {
Expand Down Expand Up @@ -80,21 +81,22 @@ export function SheetInspector() {
</div>
<button
type="button"
aria-label="Remove photo"
onClick={() => {
select(null)
remove(photo.id)
}}
className="text-muted-foreground hover:text-destructive ml-auto flex items-center gap-1 text-xs"
className="text-muted-foreground hover:text-destructive hover:bg-neutral-100 ml-auto flex size-7 items-center justify-center rounded"
>
<Trash2 className="size-3.5" />
Remove
<Trash2 className="size-4" />
</button>
<button
type="button"
aria-label="Done"
onClick={() => select(null)}
className="text-xs font-medium hover:underline"
className="text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 flex size-7 items-center justify-center rounded"
>
Done
<Check className="size-4" />
</button>
</div>
)
Expand Down
14 changes: 4 additions & 10 deletions apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,29 @@ import { createFileRoute } from '@tanstack/react-router'

import { A4Preview } from '@/components/a4-preview'
import { OptionsPanel } from '@/components/options-panel'
import { PhotoDropzone } from '@/components/photo-dropzone'
import { PhotoStrip } from '@/components/photo-strip'
import { PhotoSidebar } from '@/components/photo-sidebar'
import { ProjectControls } from '@/components/project-controls'
import { usePhotoStore } from '@/stores/photo-store'

export const Route = createFileRoute('/')({
component: Home,
})

function Home() {
const hasPhotos = usePhotoStore((state) => state.photos.length > 0)

return (
<main className="mx-auto flex min-h-svh w-full max-w-7xl flex-col gap-5 p-4 sm:p-6">
<header className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold tracking-tight">Polaroid</h1>
<p className="text-muted-foreground text-sm">
Build polaroid-style print sheets — edit each frame on the page.
Everything stays on your device.
Edit each frame on the page — everything stays on your device.
</p>
</div>
<ProjectControls />
</header>

<div className="grid gap-6 lg:grid-cols-[240px_minmax(0,1fr)_300px] lg:items-start">
<aside className="flex flex-col gap-3">
<PhotoDropzone compact={hasPhotos} />
<PhotoStrip />
<aside className="flex flex-col lg:self-stretch">
<PhotoSidebar />
</aside>
<div className="min-w-0">
<A4Preview />
Expand Down
Loading