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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,9 @@ vite.config.ts.timestamp-*

# Cloudflare Wrangler local state
.wrangler/

# Local agent handoff / scratch docs (never tracked)
.claude/

# TanStack Router generated temp dir
.tanstack/
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/fraunces": "^5.2.9",
"@fontsource/indie-flower": "^5.2.7",
"@fontsource/kalam": "^5.2.8",
"@fontsource/montserrat": "^5.2.8",
Expand Down
22 changes: 14 additions & 8 deletions apps/web/src/components/a4-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useLayoutEffect, useRef, useState } from "react";
import { useCallback, useRef, useState } from "react";
import { RectangleHorizontal, RectangleVertical, Square } from "lucide-react";

import { EmptyHero } from "@/components/empty-hero";
import { SheetInspector } from "@/components/sheet-inspector";
import { SheetPage } from "@/components/sheet-page";
import { StripPage } from "@/components/strip-page";
Expand Down Expand Up @@ -44,19 +45,24 @@ export function A4Preview() {
const fontStack = captionFontStack(captionFontId);
const paper = paperSize(paperSizeId);

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

useLayoutEffect(() => {
const el = containerRef.current;
// Callback ref so the observer re-attaches whenever the sheet container
// mounts — e.g. after the empty-state hero gives way to the first page.
const setContainer = useCallback((el: HTMLDivElement | null) => {
observerRef.current?.disconnect();
if (!el) return;
const update = () => setWidth(el.clientWidth);
update();
const observer = new ResizeObserver(update);
observer.observe(el);
return () => observer.disconnect();
observerRef.current = new ResizeObserver(update);
observerRef.current.observe(el);
}, []);

if (photos.length === 0) {
return <EmptyHero />;
}

const gridPages = paginate(
photos,
perRow,
Expand All @@ -79,7 +85,7 @@ export function A4Preview() {
<SheetInspector />
</div>
<div
ref={containerRef}
ref={setContainer}
className="mx-auto flex w-full max-w-xl flex-col gap-5"
>
{sheetFormat === "strip"
Expand Down
83 changes: 83 additions & 0 deletions apps/web/src/components/empty-hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useRef } from "react";
import { ImagePlus } from "lucide-react";

import { Button } from "@/components/ui/button";
import { usePhotoStore } from "@/stores/photo-store";

const ACCEPT =
"image/jpeg,image/png,image/webp,image/heic,image/heif,.jpg,.jpeg,.png,.webp,.heic,.heif";

/** A few illustrative polaroids — pure CSS, no image assets, so it loads instantly. */
const SAMPLES = [
{ tint: "linear-gradient(150deg, #f7c59f, #ee6c4d)", caption: "Lisbon · '24", rotate: -6 },
{ tint: "linear-gradient(150deg, #bcd4e6, #3d5a80)", caption: "Sunday market", rotate: 3 },
{ tint: "linear-gradient(150deg, #f6e7b4, #e3924f)", caption: "Golden hour", rotate: -2 },
];

/** Shown in the centre column before any photos are added. */
export function EmptyHero() {
const addFiles = usePhotoStore((state) => state.addFiles);
const inputRef = useRef<HTMLInputElement>(null);

return (
<div className="flex min-h-[60svh] flex-col items-center justify-center gap-9 px-4 text-center">
<div aria-hidden className="flex items-end justify-center pt-2">
{SAMPLES.map((sample, i) => (
<figure
key={sample.caption}
className="-mx-2.5 w-28 rounded-sm bg-white p-2 pb-7 shadow-xl ring-1 ring-black/5 sm:w-36"
style={{ transform: `rotate(${sample.rotate}deg)`, zIndex: i === 1 ? 2 : 1 }}
>
<div
className="aspect-square w-full rounded-[2px]"
style={{ background: sample.tint }}
/>
<figcaption
className="mt-2 text-center text-sm text-[#1f1b16]"
style={{ fontFamily: '"Kalam", cursive' }}
>
{sample.caption}
</figcaption>
</figure>
))}
</div>

<div className="flex max-w-md flex-col items-center gap-3">
<h2 className="font-display text-3xl leading-tight tracking-tight [font-variation-settings:'opsz'_144] sm:text-4xl">
Turn your photos into polaroids
</h2>
<p className="text-muted-foreground text-sm leading-relaxed sm:text-base">
Drop in a few shots — captions fill themselves in from each photo's
date and place — and download a print-ready sheet to cut out at home.
</p>
</div>

<div className="flex flex-col items-center gap-2">
<input
ref={inputRef}
type="file"
accept={ACCEPT}
multiple
hidden
onChange={(event) => {
const files = Array.from(event.target.files ?? []);
if (files.length > 0) addFiles(files);
event.target.value = "";
}}
/>
<Button
type="button"
size="lg"
className="gap-2"
onClick={() => inputRef.current?.click()}
>
<ImagePlus className="size-4" />
Add photos
</Button>
<span className="text-muted-foreground text-xs">
or drag photos anywhere
</span>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions apps/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '@fontsource/montserrat/400.css'
import '@fontsource/montserrat/500.css'
import '@fontsource/montserrat/600.css'
import '@fontsource/montserrat/700.css'
import '@fontsource-variable/fraunces/opsz.css'
import '@fontsource/indie-flower'
import '@fontsource/patrick-hand'
import '@fontsource/shadows-into-light'
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ function Home() {
<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>
<h1 className="font-display text-3xl font-semibold tracking-tight [font-variation-settings:'opsz'_144]">
Polaroid
</h1>
<p className="text-muted-foreground text-sm">
Edit each frame on the page — everything stays on your device.
</p>
Expand Down
72 changes: 40 additions & 32 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,56 @@
:root {
color-scheme: light;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
/* Warm cream canvas + coral accent (#58 visual identity). */
--coral: oklch(0.726 0.166 24);
--coral-foreground: oklch(0.225 0.009 67);
--background: oklch(0.968 0.012 84);
--foreground: oklch(0.225 0.009 67);
--card: oklch(0.985 0.008 84);
--card-foreground: oklch(0.225 0.009 67);
--popover: oklch(0.99 0.006 84);
--popover-foreground: oklch(0.225 0.009 67);
--primary: var(--coral);
--primary-foreground: var(--coral-foreground);
--secondary: oklch(0.94 0.012 84);
--secondary-foreground: oklch(0.225 0.009 67);
--muted: oklch(0.94 0.012 84);
--muted-foreground: oklch(0.585 0.012 78);
--accent: oklch(0.93 0.014 80);
--accent-foreground: oklch(0.225 0.009 67);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--border: oklch(0.9 0.012 84);
--input: oklch(0.9 0.012 84);
--ring: var(--coral);
}

.dark {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--coral: oklch(0.74 0.16 24);
--coral-foreground: oklch(0.18 0.008 67);
--background: oklch(0.165 0.006 60);
--foreground: oklch(0.97 0.006 85);
--card: oklch(0.215 0.006 60);
--card-foreground: oklch(0.97 0.006 85);
--popover: oklch(0.215 0.006 60);
--popover-foreground: oklch(0.97 0.006 85);
--primary: var(--coral);
--primary-foreground: var(--coral-foreground);
--secondary: oklch(0.28 0.006 60);
--secondary-foreground: oklch(0.97 0.006 85);
--muted: oklch(0.28 0.006 60);
--muted-foreground: oklch(0.72 0.01 78);
--accent: oklch(0.3 0.008 60);
--accent-foreground: oklch(0.97 0.006 85);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--ring: var(--coral);
}

@theme inline {
--font-sans: 'Montserrat', ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-display: 'Fraunces Variable', Georgia, 'Times New Roman', serif;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
Expand All @@ -61,6 +67,8 @@
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-coral: var(--coral);
--color-coral-foreground: var(--coral-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading