From 8ff1bb118a95bac4d69faef538df82751d19c901 Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:29:16 +0530 Subject: [PATCH 1/7] feat(web): add caption fields to photo model --- apps/web/src/lib/photos.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/src/lib/photos.ts b/apps/web/src/lib/photos.ts index e8be143..a0aae43 100644 --- a/apps/web/src/lib/photos.ts +++ b/apps/web/src/lib/photos.ts @@ -5,8 +5,13 @@ export interface Photo { url: string name: string size: number + /** Polaroid caption lines (auto-filled from EXIF later; editable). */ + captionTop: string + captionBottom: string } +export type CaptionField = 'captionTop' | 'captionBottom' + const IMAGE_EXTENSIONS = /\.(jpe?g|png|webp|heic|heif|avif|gif|bmp|tiff?)$/i export function isImageFile(file: File): boolean { @@ -20,6 +25,8 @@ export function createPhoto(file: File): Photo { url: URL.createObjectURL(file), name: file.name, size: file.size, + captionTop: '', + captionBottom: '', } } From 5bfd13e8a0e061f58fb7493b0d2eff491ca168d5 Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:29:39 +0530 Subject: [PATCH 2/7] feat(web): add setCaption action to photo store --- apps/web/src/stores/photo-store.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/src/stores/photo-store.ts b/apps/web/src/stores/photo-store.ts index ad31b4d..c43a6db 100644 --- a/apps/web/src/stores/photo-store.ts +++ b/apps/web/src/stores/photo-store.ts @@ -1,11 +1,17 @@ import { create } from 'zustand' -import { type Photo, createPhoto, isImageFile } from '@/lib/photos' +import { + type CaptionField, + type Photo, + createPhoto, + isImageFile, +} from '@/lib/photos' interface PhotoState { photos: Photo[] /** Adds image files to the collection. Returns how many were accepted. */ addFiles: (files: File[]) => number + setCaption: (id: string, field: CaptionField, value: string) => void remove: (id: string) => void clear: () => void } @@ -19,6 +25,12 @@ export const usePhotoStore = create((set) => ({ } return accepted.length }, + setCaption: (id, field, value) => + set((state) => ({ + photos: state.photos.map((photo) => + photo.id === id ? { ...photo, [field]: value } : photo, + ), + })), remove: (id) => set((state) => { const target = state.photos.find((photo) => photo.id === id) From 365ff1dfd6a410d0e98a190b9be43821818e26a0 Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:29:56 +0530 Subject: [PATCH 3/7] feat(web): add settings store for frame size --- apps/web/src/stores/settings-store.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/web/src/stores/settings-store.ts diff --git a/apps/web/src/stores/settings-store.ts b/apps/web/src/stores/settings-store.ts new file mode 100644 index 0000000..23d3290 --- /dev/null +++ b/apps/web/src/stores/settings-store.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand' + +export const MIN_FRAME_PADDING = 6 +export const MAX_FRAME_PADDING = 28 + +interface SettingsState { + /** White polaroid border width in px (sides/top); the bottom is thicker. */ + framePadding: number + setFramePadding: (px: number) => void +} + +export const useSettingsStore = create((set) => ({ + framePadding: 14, + setFramePadding: (px) => set({ framePadding: px }), +})) From 870763d29ce209b463b50f0fde7331f833fc403e Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:30:10 +0530 Subject: [PATCH 4/7] feat(web): add handwriting font token for captions --- apps/web/src/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 269edc5..4f93859 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -42,6 +42,7 @@ } @theme inline { + --font-handwriting: "Caveat", "Segoe Print", "Bradley Hand", cursive; --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); From b686106efbe215b02b05f240cd2e8ad842440c7f Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:30:50 +0530 Subject: [PATCH 5/7] feat(web): add Polaroid frame component with editable captions --- apps/web/src/components/polaroid.tsx | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 apps/web/src/components/polaroid.tsx diff --git a/apps/web/src/components/polaroid.tsx b/apps/web/src/components/polaroid.tsx new file mode 100644 index 0000000..03f7518 --- /dev/null +++ b/apps/web/src/components/polaroid.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react' +import { ImageOff } from 'lucide-react' + +import { cn } from '@/lib/utils' +import { type Photo } from '@/lib/photos' +import { usePhotoStore } from '@/stores/photo-store' +import { useSettingsStore } from '@/stores/settings-store' + +export function Polaroid({ photo }: { photo: Photo }) { + const setCaption = usePhotoStore((state) => state.setCaption) + const framePadding = useSettingsStore((state) => state.framePadding) + const [failed, setFailed] = useState(false) + + return ( +
+
+ {failed ? ( +
+ + Preview unavailable +
+ ) : ( + {photo.name} setFailed(true)} + className="h-full w-full object-cover" + /> + )} +
+
+ setCaption(photo.id, 'captionTop', value)} + placeholder="Add a caption" + className="text-lg leading-tight" + /> + setCaption(photo.id, 'captionBottom', value)} + className="text-sm text-neutral-500" + /> +
+
+ ) +} + +function CaptionInput({ + value, + onChange, + placeholder, + className, +}: { + value: string + onChange: (value: string) => void + placeholder?: string + className?: string +}) { + return ( + onChange(event.target.value)} + placeholder={placeholder} + aria-label="Polaroid caption" + className={cn( + 'font-handwriting w-full border-0 bg-transparent text-center text-neutral-800 placeholder:text-neutral-300 focus:outline-none', + className, + )} + /> + ) +} From cfcb68b9b1b93383b21a701e2bb0461377513a0f Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:31:09 +0530 Subject: [PATCH 6/7] feat(web): add frame-size slider control --- apps/web/src/components/frame-controls.tsx | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 apps/web/src/components/frame-controls.tsx diff --git a/apps/web/src/components/frame-controls.tsx b/apps/web/src/components/frame-controls.tsx new file mode 100644 index 0000000..7b44640 --- /dev/null +++ b/apps/web/src/components/frame-controls.tsx @@ -0,0 +1,31 @@ +import { + MAX_FRAME_PADDING, + MIN_FRAME_PADDING, + useSettingsStore, +} from '@/stores/settings-store' + +export function FrameControls() { + const framePadding = useSettingsStore((state) => state.framePadding) + const setFramePadding = useSettingsStore((state) => state.setFramePadding) + + return ( +
+ + setFramePadding(Number(event.target.value))} + className="accent-primary w-32" + /> +
+ ) +} From a5b1acc8427815153385e517f88e79421350cfc2 Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 17:31:47 +0530 Subject: [PATCH 7/7] feat(web): render photos as polaroids in the grid --- apps/web/src/components/photo-grid.tsx | 46 +++++++++----------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/apps/web/src/components/photo-grid.tsx b/apps/web/src/components/photo-grid.tsx index 64ef2ec..f5bd4e8 100644 --- a/apps/web/src/components/photo-grid.tsx +++ b/apps/web/src/components/photo-grid.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' -import { ImageOff, X } from 'lucide-react' +import { X } from 'lucide-react' +import { FrameControls } from '@/components/frame-controls' +import { Polaroid } from '@/components/polaroid' import { Button } from '@/components/ui/button' -import { type Photo, formatBytes } from '@/lib/photos' +import { type Photo } from '@/lib/photos' import { usePhotoStore } from '@/stores/photo-store' export function PhotoGrid() { @@ -12,16 +13,19 @@ export function PhotoGrid() { if (photos.length === 0) return null return ( -
-
+
+

{photos.length} {photos.length === 1 ? 'photo' : 'photos'}

- +
+ + +
-
    +
      {photos.map((photo) => ( ))} @@ -32,38 +36,20 @@ export function PhotoGrid() { function PhotoTile({ photo }: { photo: Photo }) { const remove = usePhotoStore((state) => state.remove) - const [failed, setFailed] = useState(false) return ( -
    • - {failed ? ( -
      - - Preview unavailable -
      - ) : ( - {photo.name} setFailed(true)} - className="h-full w-full object-cover" - /> - )} +
    • + -
      -

      {photo.name}

      -

      {formatBytes(photo.size)}

      -
    • ) }