Skip to content

Commit 7aa91f3

Browse files
committed
galleries have upload dates and fize sizes added
1 parent 9f255f6 commit 7aa91f3

File tree

4 files changed

+124
-14
lines changed

4 files changed

+124
-14
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""add file_size and uploaded_at to photo table
2+
3+
Revision ID: 2025011501
4+
Revises: 2025111201
5+
Create Date: 2025-01-15
6+
"""
7+
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "2025011501"
15+
down_revision = "2025111201"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# Add file_size column (in bytes, default 0 for existing photos)
22+
op.add_column("photo", sa.Column("file_size", sa.Integer(), nullable=False, server_default="0"))
23+
24+
# Add uploaded_at column (use created_at as default for existing photos)
25+
# First add as nullable, then update existing rows, then make non-nullable
26+
op.add_column("photo", sa.Column("uploaded_at", sa.DateTime(), nullable=True))
27+
op.execute("UPDATE photo SET uploaded_at = created_at WHERE uploaded_at IS NULL")
28+
op.alter_column("photo", "uploaded_at", nullable=False)
29+
30+
31+
def downgrade():
32+
op.drop_column("photo", "uploaded_at")
33+
op.drop_column("photo", "file_size")
34+

backend/app/api/routes/galleries.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ async def upload_gallery_photos(
225225
target_path = storage_dir / f"{stem}-{counter}{ext}"
226226
counter += 1
227227
content = await uf.read()
228+
file_size = len(content) # Get file size in bytes
228229
with open(target_path, "wb") as out:
229230
out.write(content)
230231

@@ -236,6 +237,7 @@ async def upload_gallery_photos(
236237
gallery_id=id,
237238
filename=rel_filename,
238239
url=f"/api/v1/galleries/{id}/photos/files/{rel_filename}",
240+
file_size=file_size,
239241
),
240242
)
241243
saved.append(photo)

backend/app/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,14 @@ class PhotoCreate(SQLModel):
251251
gallery_id: uuid.UUID
252252
filename: str = Field(min_length=1, max_length=255)
253253
url: str = Field(min_length=1, max_length=500)
254+
file_size: int = Field(ge=0) # File size in bytes
254255

255256

256257
class Photo(PhotoBase, table=True):
257258
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
258259
created_at: datetime = Field(default_factory=datetime.utcnow)
260+
uploaded_at: datetime = Field(default_factory=datetime.utcnow) # When photo was uploaded
261+
file_size: int = Field(default=0, ge=0) # File size in bytes
259262
gallery_id: uuid.UUID = Field(
260263
foreign_key="gallery.id", nullable=False, ondelete="CASCADE"
261264
)
@@ -264,6 +267,8 @@ class Photo(PhotoBase, table=True):
264267
class PhotoPublic(PhotoBase):
265268
id: uuid.UUID
266269
created_at: datetime
270+
uploaded_at: datetime
271+
file_size: int
267272
gallery_id: uuid.UUID
268273

269274

frontend/src/routes/_layout/galleries.$galleryId.tsx

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,18 @@ function GalleryDetail() {
9292
},
9393
})
9494
if (!res.ok) throw new Error("Failed to load photos")
95-
const data = await res.json() as { data: { id: string; filename: string; url: string }[]; count: number }
95+
const data = await res.json() as {
96+
data: {
97+
id: string;
98+
filename: string;
99+
url: string;
100+
file_size: number;
101+
uploaded_at: string;
102+
}[];
103+
count: number
104+
}
96105
// Convert relative URLs to absolute URLs
97-
data.data = data.data.map((photo: { id: string; filename: string; url: string }) => ({
106+
data.data = data.data.map((photo) => ({
98107
...photo,
99108
url: photo.url.startsWith("http") ? photo.url : `${OpenAPI.BASE}${photo.url}`,
100109
}))
@@ -204,6 +213,32 @@ function GalleryDetail() {
204213
setSelected((prev: Record<string, boolean>) => ({ ...prev, [id]: !prev[id] }))
205214
}
206215

216+
// Format file size for display
217+
const formatFileSize = (bytes: number): string => {
218+
if (bytes === 0) return "0 Bytes"
219+
const k = 1024
220+
const sizes = ["Bytes", "KB", "MB", "GB"]
221+
const i = Math.floor(Math.log(bytes) / Math.log(k))
222+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]
223+
}
224+
225+
// Format date for display
226+
const formatDate = (dateString: string | undefined): string => {
227+
if (!dateString) return "Unknown"
228+
try {
229+
const date = new Date(dateString)
230+
return date.toLocaleDateString("en-US", {
231+
year: "numeric",
232+
month: "long",
233+
day: "numeric",
234+
hour: "2-digit",
235+
minute: "2-digit",
236+
})
237+
} catch {
238+
return "Unknown"
239+
}
240+
}
241+
207242
const onDownloadAll = () => {
208243
if (!hasPhotos) {
209244
showErrorToast("There are no photos in this gallery to download")
@@ -445,14 +480,28 @@ function GalleryDetail() {
445480
446481
</Button>
447482
</Box>
448-
<Box> {/* TODO: Add file size and date added */ }
449-
<Text fontWeight="bold">Filename: {photos[currentPhotoIndex].filename}</Text>
450-
<Text>
451-
Size: unknown (file size is not tracked in the database)
452-
</Text>
453-
<Text>
454-
Date added: {gallery.date ?? "Unknown"}
455-
</Text>
483+
<Box>
484+
<Stack gap={2}>
485+
<Text fontWeight="bold">Filename: {photos[currentPhotoIndex].filename}</Text>
486+
<Text>
487+
Size: {photos[currentPhotoIndex].file_size
488+
? formatFileSize(photos[currentPhotoIndex].file_size)
489+
: "Unknown"}
490+
</Text>
491+
<Text>
492+
Date uploaded: {formatDate(photos[currentPhotoIndex].uploaded_at)}
493+
</Text>
494+
<Flex alignItems="center" gap={2} mt={2}>
495+
<Checkbox
496+
checked={!!selected[photos[currentPhotoIndex].id]}
497+
onCheckedChange={() => {
498+
onSelectToggle(photos[currentPhotoIndex].id)
499+
}}
500+
>
501+
Select this photo
502+
</Checkbox>
503+
</Flex>
504+
</Stack>
456505
</Box>
457506
</Stack>
458507
)}
@@ -478,7 +527,7 @@ function GalleryDetail() {
478527
}}
479528
gap={4}
480529
>
481-
{photos.map((p: { id: string; filename: string; url: string }, index: number) => (
530+
{photos.map((p: { id: string; filename: string; url: string; file_size?: number; uploaded_at?: string }, index: number) => (
482531
<Box
483532
key={p.id}
484533
position="relative"
@@ -489,18 +538,38 @@ function GalleryDetail() {
489538
alignItems="center"
490539
justifyContent="center"
491540
overflow="hidden"
492-
onClick={() => openLightboxAt(index)}
541+
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
542+
// Don't open dialog if clicking on checkbox
543+
const target = e.target as HTMLElement
544+
if (target.closest('[role="checkbox"]') || target.closest('input[type="checkbox"]')) {
545+
return
546+
}
547+
openLightboxAt(index)
548+
}}
493549
cursor="pointer"
494550
>
495551
<img
496552
src={p.url}
497553
alt={p.filename}
498554
style={{ width: "100%", height: "100%", objectFit: "cover" }}
499555
/>
500-
<Box position="absolute" top="8px" left="8px" bg="whiteAlpha.800" borderRadius="md" p={1}>
556+
<Box
557+
position="absolute"
558+
top="8px"
559+
left="8px"
560+
bg="whiteAlpha.800"
561+
borderRadius="md"
562+
p={1}
563+
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
564+
// Stop propagation so clicking checkbox doesn't open dialog
565+
e.stopPropagation()
566+
}}
567+
>
501568
<Checkbox
502569
checked={!!selected[p.id]}
503-
onCheckedChange={() => onSelectToggle(p.id)}
570+
onCheckedChange={() => {
571+
onSelectToggle(p.id)
572+
}}
504573
>
505574
Select
506575
</Checkbox>

0 commit comments

Comments
 (0)