mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
feat: journey gallery 1-to-N model with M:N entry-photo junction table
Replaces the old model where journey_photos was keyed per-entry with a per-journey gallery table (one row per unique photo per journey) and a new junction table journey_entry_photos that links gallery photos to entries. Key changes: - Migration 121: renames old journey_photos to journey_photos_old, creates the new gallery table + junction table, backfills both from existing data, drops the backup, removes synthetic 'Gallery' / '[Trip Photos]' wrapper entries - journeyService: rewrites photo helpers (JP_SELECT/JOIN now joins via journey_entry_photos → journey_photos → trek_photos); adds uploadGalleryPhotos, addProviderPhotoToGallery, unlinkPhotoFromEntry, deleteGalleryPhoto; simplifies deletePhoto and linkPhotoToEntry against the new schema; syncTripPhotos inserts directly into the gallery instead of a wrapper entry - journeyShareService: updates public photo and asset validation queries to join through the gallery table instead of entry_id; getPublicJourney now returns a dedicated gallery array alongside per-entry photos - journey routes: adds gallery upload, provider-photo, and delete endpoints (POST/DELETE /:id/gallery/*); adds unlink-from-entry route (DELETE /entries/:entryId/photos/:journeyPhotoId); updates link-photo to accept journey_photo_id with a backwards-compat photo_id alias - types: adds GalleryPhoto interface - client api: adds uploadGalleryPhotos, addProviderPhotosToGallery, unlinkPhoto, deleteGalleryPhoto; updates linkPhoto param name to journeyPhotoId - journeyStore: adds GalleryPhoto type, gallery field on JourneyDetail, uploadGalleryPhotos / unlinkPhoto / deleteGalleryPhoto store actions - JourneyDetailPage + tests: updated to work with the new gallery model
This commit is contained in:
@@ -57,6 +57,24 @@ export interface JourneyPhoto {
|
||||
height?: number | null
|
||||
}
|
||||
|
||||
export interface GalleryPhoto {
|
||||
id: number
|
||||
journey_id: number
|
||||
photo_id: number
|
||||
caption?: string | null
|
||||
shared: number
|
||||
sort_order: number
|
||||
created_at: number
|
||||
// Joined from trek_photos for display
|
||||
provider?: string
|
||||
asset_id?: string | null
|
||||
owner_id?: number | null
|
||||
file_path?: string | null
|
||||
thumbnail_path?: string | null
|
||||
width?: number | null
|
||||
height?: number | null
|
||||
}
|
||||
|
||||
export interface JourneyTrip {
|
||||
trip_id: number
|
||||
added_at: number
|
||||
@@ -79,6 +97,7 @@ export interface JourneyContributor {
|
||||
|
||||
export interface JourneyDetail extends Journey {
|
||||
entries: JourneyEntry[]
|
||||
gallery: GalleryPhoto[]
|
||||
trips: JourneyTrip[]
|
||||
contributors: JourneyContributor[]
|
||||
stats: { entries: number; photos: number; places: number }
|
||||
@@ -103,6 +122,9 @@ interface JourneyState {
|
||||
reorderEntries: (journeyId: number, orderedIds: number[]) => Promise<void>
|
||||
|
||||
uploadPhotos: (entryId: number, formData: FormData) => Promise<JourneyPhoto[]>
|
||||
uploadGalleryPhotos: (journeyId: number, formData: FormData) => Promise<GalleryPhoto[]>
|
||||
unlinkPhoto: (entryId: number, journeyPhotoId: number) => Promise<void>
|
||||
deleteGalleryPhoto: (journeyId: number, journeyPhotoId: number) => Promise<void>
|
||||
deletePhoto: (photoId: number) => Promise<void>
|
||||
|
||||
clear: () => void
|
||||
@@ -228,12 +250,55 @@ export const useJourneyStore = create<JourneyState>((set, get) => ({
|
||||
entries: s.current.entries.map(e =>
|
||||
e.id === entryId ? { ...e, photos: [...(e.photos || []), ...photos] } : e
|
||||
),
|
||||
gallery: [...(s.current.gallery || []), ...(data.gallery || [])],
|
||||
},
|
||||
}
|
||||
})
|
||||
return photos
|
||||
},
|
||||
|
||||
uploadGalleryPhotos: async (journeyId, formData) => {
|
||||
const data = await journeyApi.uploadGalleryPhotos(journeyId, formData)
|
||||
const photos: GalleryPhoto[] = data.photos || []
|
||||
set(s => {
|
||||
if (!s.current || s.current.id !== journeyId) return s
|
||||
return { current: { ...s.current, gallery: [...(s.current.gallery || []), ...photos] } }
|
||||
})
|
||||
return photos
|
||||
},
|
||||
|
||||
unlinkPhoto: async (entryId, journeyPhotoId) => {
|
||||
await journeyApi.unlinkPhoto(entryId, journeyPhotoId)
|
||||
set(s => {
|
||||
if (!s.current) return s
|
||||
return {
|
||||
current: {
|
||||
...s.current,
|
||||
entries: s.current.entries.map(e =>
|
||||
e.id === entryId ? { ...e, photos: (e.photos || []).filter(p => p.id !== journeyPhotoId) } : e
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
deleteGalleryPhoto: async (journeyId, journeyPhotoId) => {
|
||||
await journeyApi.deleteGalleryPhoto(journeyId, journeyPhotoId)
|
||||
set(s => {
|
||||
if (!s.current) return s
|
||||
return {
|
||||
current: {
|
||||
...s.current,
|
||||
gallery: (s.current.gallery || []).filter(p => p.id !== journeyPhotoId),
|
||||
entries: s.current.entries.map(e => ({
|
||||
...e,
|
||||
photos: (e.photos || []).filter(p => p.id !== journeyPhotoId),
|
||||
})),
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
deletePhoto: async (photoId) => {
|
||||
await journeyApi.deletePhoto(photoId)
|
||||
set(s => {
|
||||
|
||||
Reference in New Issue
Block a user