mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat: unified photo provider abstraction layer (#584)
Introduce trek_photos as central photo registry. Frontend uses /api/photos/:id/:kind instead of provider-specific URLs. Adding a new photo provider is now backend-only work. - New trek_photos table (migration 98) with photo_id FK in trip_photos and journey_photos - Unified /api/photos/:id/thumbnail|original|info endpoint - photoResolverService for central resolution and streaming - ProviderPicker: add "All Photos" tab, rename tabs, fix i18n - Localize all hardcoded strings in JourneyDetailPage (14 langs) - Fix date formatting to use browser locale instead of hardcoded 'en' - Journey stats as styled tile cards
This commit is contained in:
@@ -233,8 +233,8 @@ describe('MemoriesPanel', () => {
|
||||
http.get('/api/integrations/memories/unified/trips/:tripId/photos', () =>
|
||||
HttpResponse.json({
|
||||
photos: [
|
||||
{ asset_id: 'photo1', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-01T10:00:00Z' },
|
||||
{ asset_id: 'photo2', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-10T10:00:00Z' },
|
||||
{ photo_id: 1, asset_id: 'photo1', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-01T10:00:00Z' },
|
||||
{ photo_id: 2, asset_id: 'photo2', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-10T10:00:00Z' },
|
||||
],
|
||||
})
|
||||
),
|
||||
@@ -501,8 +501,8 @@ describe('MemoriesPanel', () => {
|
||||
http.get('/api/integrations/memories/unified/trips/:tripId/photos', () =>
|
||||
HttpResponse.json({
|
||||
photos: [
|
||||
{ asset_id: 'p1', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-01T00:00:00Z', city: 'Paris' },
|
||||
{ asset_id: 'p2', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-05T00:00:00Z', city: 'Lyon' },
|
||||
{ photo_id: 10, asset_id: 'p1', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-01T00:00:00Z', city: 'Paris' },
|
||||
{ photo_id: 11, asset_id: 'p2', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-05T00:00:00Z', city: 'Lyon' },
|
||||
],
|
||||
})
|
||||
),
|
||||
@@ -676,8 +676,8 @@ describe('MemoriesPanel', () => {
|
||||
http.get('/api/integrations/memories/unified/trips/:tripId/photos', () =>
|
||||
HttpResponse.json({
|
||||
photos: [
|
||||
{ asset_id: 'p1', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-01T00:00:00Z', city: 'Paris' },
|
||||
{ asset_id: 'p2', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-05T00:00:00Z', city: 'Lyon' },
|
||||
{ photo_id: 10, asset_id: 'p1', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-01T00:00:00Z', city: 'Paris' },
|
||||
{ photo_id: 11, asset_id: 'p2', provider: 'immich', user_id: 1, username: 'me', shared: 1, added_at: '2025-03-05T00:00:00Z', city: 'Lyon' },
|
||||
],
|
||||
})
|
||||
),
|
||||
|
||||
@@ -30,6 +30,7 @@ function ProviderImg({ baseUrl, provider, style, loading }: { baseUrl: string; p
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface TripPhoto {
|
||||
photo_id: number
|
||||
asset_id: string
|
||||
provider: string
|
||||
user_id: number
|
||||
@@ -105,19 +106,12 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
}
|
||||
|
||||
function buildProviderAssetUrl(photo: TripPhoto, what: string): string {
|
||||
return `${ADDON_PREFIX}/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/${what}`
|
||||
return `/photos/${photo.photo_id}/${what}`
|
||||
}
|
||||
|
||||
function buildProviderAssetUrlFromAsset(asset: Asset, what: string, userId: number): string {
|
||||
const photo: TripPhoto = {
|
||||
asset_id: asset.id,
|
||||
provider: asset.provider,
|
||||
user_id: userId,
|
||||
username: '',
|
||||
shared: 0,
|
||||
added_at: null
|
||||
}
|
||||
return buildProviderAssetUrl(photo, what)
|
||||
// Picker photos are not yet saved — use provider-specific URL
|
||||
return `${ADDON_PREFIX}/${asset.provider}/assets/${tripId}/${asset.id}/${userId}/${what}`
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +183,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
}
|
||||
|
||||
// Lightbox
|
||||
const [lightboxId, setLightboxId] = useState<string | null>(null)
|
||||
const [lightboxId, setLightboxId] = useState<number | null>(null)
|
||||
const [lightboxUserId, setLightboxUserId] = useState<number | null>(null)
|
||||
const [lightboxInfo, setLightboxInfo] = useState<any>(null)
|
||||
const [lightboxInfoLoading, setLightboxInfoLoading] = useState(false)
|
||||
@@ -357,11 +351,10 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
try {
|
||||
await apiClient.delete(buildUnifiedUrl('photos'), {
|
||||
data: {
|
||||
asset_id: photo.asset_id,
|
||||
provider: photo.provider,
|
||||
photo_id: photo.photo_id,
|
||||
},
|
||||
})
|
||||
setTripPhotos(prev => prev.filter(p => !(p.provider === photo.provider && p.asset_id === photo.asset_id)))
|
||||
setTripPhotos(prev => prev.filter(p => p.photo_id !== photo.photo_id))
|
||||
} catch { toast.error(t('memories.error.removePhoto')) }
|
||||
}
|
||||
|
||||
@@ -371,11 +364,10 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
try {
|
||||
await apiClient.put(buildUnifiedUrl('photos', 'sharing'), {
|
||||
shared,
|
||||
asset_id: photo.asset_id,
|
||||
provider: photo.provider,
|
||||
photo_id: photo.photo_id,
|
||||
})
|
||||
setTripPhotos(prev => prev.map(p =>
|
||||
p.provider === photo.provider && p.asset_id === photo.asset_id ? { ...p, shared: shared ? 1 : 0 } : p
|
||||
p.photo_id === photo.photo_id ? { ...p, shared: shared ? 1 : 0 } : p
|
||||
))
|
||||
} catch { toast.error(t('memories.error.toggleSharing')) }
|
||||
}
|
||||
@@ -839,10 +831,10 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
{allVisible.map(photo => {
|
||||
const isOwn = photo.user_id === currentUser?.id
|
||||
return (
|
||||
<div key={`${photo.provider}:${photo.asset_id}`} className="group"
|
||||
<div key={photo.photo_id} className="group"
|
||||
style={{ position: 'relative', aspectRatio: '1', borderRadius: 10, overflow: 'visible', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setLightboxId(photo.asset_id); setLightboxUserId(photo.user_id); setLightboxInfo(null)
|
||||
setLightboxId(photo.photo_id); setLightboxUserId(photo.user_id); setLightboxInfo(null)
|
||||
if (lightboxOriginalSrc) URL.revokeObjectURL(lightboxOriginalSrc)
|
||||
setLightboxOriginalSrc('')
|
||||
fetchImageAsBlob('/api' + buildProviderAssetUrl(photo, 'original')).then(setLightboxOriginalSrc)
|
||||
@@ -961,7 +953,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
setShowMobileInfo(false)
|
||||
}
|
||||
|
||||
const currentIdx = allVisible.findIndex(p => p.asset_id === lightboxId)
|
||||
const currentIdx = allVisible.findIndex(p => p.photo_id === lightboxId)
|
||||
const hasPrev = currentIdx > 0
|
||||
const hasNext = currentIdx < allVisible.length - 1
|
||||
const navigateTo = (idx: number) => {
|
||||
@@ -969,7 +961,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
if (!photo) return
|
||||
if (lightboxOriginalSrc) URL.revokeObjectURL(lightboxOriginalSrc)
|
||||
setLightboxOriginalSrc('')
|
||||
setLightboxId(photo.asset_id)
|
||||
setLightboxId(photo.photo_id)
|
||||
setLightboxUserId(photo.user_id)
|
||||
setLightboxInfo(null)
|
||||
fetchImageAsBlob('/api' + buildProviderAssetUrl(photo, 'original')).then(setLightboxOriginalSrc)
|
||||
|
||||
@@ -19,8 +19,7 @@ function abs(url: string | null | undefined): string {
|
||||
}
|
||||
|
||||
function pSrc(p: JourneyPhoto): string {
|
||||
if (p.provider === 'local') return abs(`/uploads/${p.file_path}`)
|
||||
return abs(`/api/integrations/memories/${p.provider}/assets/0/${p.asset_id}/${p.owner_id}/original`)
|
||||
return abs(`/api/photos/${p.photo_id}/original`)
|
||||
}
|
||||
|
||||
function fmtDate(d: string): string {
|
||||
|
||||
@@ -286,7 +286,7 @@ export default function PlaceFormModal({
|
||||
onChange={e => handleChange('description', e.target.value)}
|
||||
rows={2}
|
||||
placeholder={t('places.formDescriptionPlaceholder')}
|
||||
className="form-input" style={{ resize: 'none' }}
|
||||
className="form-input" style={{ resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1539,6 +1539,40 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.entries.deleteTitle': 'حذف الإدخال',
|
||||
'journey.photosUploaded': 'تم رفع {count} صورة',
|
||||
'journey.photosAdded': 'تمت إضافة {count} صورة',
|
||||
'journey.picker.tripPeriod': 'فترة الرحلة',
|
||||
'journey.picker.dateRange': 'نطاق التاريخ',
|
||||
'journey.picker.allPhotos': 'كل الصور',
|
||||
'journey.picker.albums': 'ألبومات',
|
||||
'journey.picker.selected': 'محدد',
|
||||
'journey.picker.addTo': 'إضافة إلى',
|
||||
'journey.picker.newGallery': 'معرض جديد',
|
||||
'journey.picker.noAlbums': 'لم يتم العثور على ألبومات',
|
||||
'journey.picker.selectDate': 'اختر تاريخ',
|
||||
'journey.picker.search': 'بحث',
|
||||
|
||||
// Journey Detail
|
||||
'journey.detail.photos': 'صور',
|
||||
'journey.detail.backToJourney': 'العودة للمجلة',
|
||||
'journey.detail.day': 'اليوم {number}',
|
||||
'journey.detail.places': 'أماكن',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'الدور',
|
||||
'journey.invite.viewer': 'مشاهد',
|
||||
'journey.invite.editor': 'محرر',
|
||||
'journey.invite.invite': 'دعوة',
|
||||
'journey.invite.inviting': 'جارٍ الدعوة...',
|
||||
|
||||
// Journey Entry Editor
|
||||
'journey.editor.uploadPhotos': 'رفع صور',
|
||||
'journey.editor.fromGallery': 'من المعرض',
|
||||
'journey.editor.addAnother': 'إضافة آخر',
|
||||
'journey.editor.makeFirst': 'جعله الأول',
|
||||
'journey.editor.searching': 'جارٍ البحث...',
|
||||
|
||||
// Journey — Share
|
||||
'journey.share.copy': 'نسخ',
|
||||
'journey.share.copied': 'تم النسخ!',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'الدردشة',
|
||||
|
||||
@@ -1902,6 +1902,9 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Colaboradores',
|
||||
'journey.detail.readMore': 'Ler mais',
|
||||
'journey.detail.prosCons': 'Prós e contras',
|
||||
'journey.detail.photos': 'fotos',
|
||||
'journey.detail.day': 'Dia {number}',
|
||||
'journey.detail.places': 'lugares',
|
||||
'journey.stats.days': 'Dias',
|
||||
'journey.stats.cities': 'Cidades',
|
||||
'journey.stats.entries': 'Entradas',
|
||||
@@ -1928,6 +1931,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Clima',
|
||||
'journey.editor.photoFirst': '1º',
|
||||
'journey.editor.makeFirst': 'Tornar 1º',
|
||||
'journey.editor.searching': 'Pesquisando...',
|
||||
'journey.mood.amazing': 'Incrível',
|
||||
'journey.mood.good': 'Bom',
|
||||
'journey.mood.neutral': 'Neutro',
|
||||
@@ -1972,6 +1976,13 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.linkDeleted': 'Link de compartilhamento removido',
|
||||
'journey.share.deleteFailed': 'Não foi possível excluir',
|
||||
'journey.share.updateFailed': 'Não foi possível atualizar',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Função',
|
||||
'journey.invite.viewer': 'Visualizador',
|
||||
'journey.invite.editor': 'Editor',
|
||||
'journey.invite.invite': 'Convidar',
|
||||
'journey.invite.inviting': 'Convidando...',
|
||||
'journey.settings.title': 'Configurações da jornada',
|
||||
'journey.settings.coverImage': 'Imagem de capa',
|
||||
'journey.settings.changeCover': 'Alterar capa',
|
||||
@@ -2002,6 +2013,16 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'Fim',
|
||||
'journey.pdf.saveAsPdf': 'Salvar como PDF',
|
||||
'journey.pdf.pages': 'páginas',
|
||||
'journey.picker.tripPeriod': 'Período da viagem',
|
||||
'journey.picker.dateRange': 'Período',
|
||||
'journey.picker.allPhotos': 'Todas as fotos',
|
||||
'journey.picker.albums': 'Álbuns',
|
||||
'journey.picker.selected': 'selecionados',
|
||||
'journey.picker.addTo': 'Adicionar a',
|
||||
'journey.picker.newGallery': 'Nova galeria',
|
||||
'journey.picker.noAlbums': 'Nenhum álbum encontrado',
|
||||
'journey.picker.selectDate': 'Selecionar data',
|
||||
'journey.picker.search': 'Pesquisar',
|
||||
'dashboard.greeting.morning': 'Bom dia,',
|
||||
'dashboard.greeting.afternoon': 'Boa tarde,',
|
||||
'dashboard.greeting.evening': 'Boa noite,',
|
||||
|
||||
@@ -1904,6 +1904,9 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Přispěvatelé',
|
||||
'journey.detail.readMore': 'Číst dále',
|
||||
'journey.detail.prosCons': 'Klady a zápory',
|
||||
'journey.detail.photos': 'fotky',
|
||||
'journey.detail.day': 'Den {number}',
|
||||
'journey.detail.places': 'míst',
|
||||
'journey.stats.days': 'Dny',
|
||||
'journey.stats.cities': 'Města',
|
||||
'journey.stats.entries': 'Záznamy',
|
||||
@@ -1930,6 +1933,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Počasí',
|
||||
'journey.editor.photoFirst': '1.',
|
||||
'journey.editor.makeFirst': 'Nastavit jako 1.',
|
||||
'journey.editor.searching': 'Hledání...',
|
||||
'journey.mood.amazing': 'Úžasný',
|
||||
'journey.mood.good': 'Dobrý',
|
||||
'journey.mood.neutral': 'Neutrální',
|
||||
@@ -1974,6 +1978,13 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.linkDeleted': 'Odkaz ke sdílení smazán',
|
||||
'journey.share.deleteFailed': 'Smazání selhalo',
|
||||
'journey.share.updateFailed': 'Aktualizace selhala',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Role',
|
||||
'journey.invite.viewer': 'Čtenář',
|
||||
'journey.invite.editor': 'Editor',
|
||||
'journey.invite.invite': 'Pozvat',
|
||||
'journey.invite.inviting': 'Zveme...',
|
||||
'journey.settings.title': 'Nastavení cestovního deníku',
|
||||
'journey.settings.coverImage': 'Titulní obrázek',
|
||||
'journey.settings.changeCover': 'Změnit obal',
|
||||
@@ -2004,6 +2015,16 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'Konec',
|
||||
'journey.pdf.saveAsPdf': 'Uložit jako PDF',
|
||||
'journey.pdf.pages': 'stran',
|
||||
'journey.picker.tripPeriod': 'Období cesty',
|
||||
'journey.picker.dateRange': 'Časové období',
|
||||
'journey.picker.allPhotos': 'Všechny fotky',
|
||||
'journey.picker.albums': 'Alba',
|
||||
'journey.picker.selected': 'vybráno',
|
||||
'journey.picker.addTo': 'Přidat do',
|
||||
'journey.picker.newGallery': 'Nová galerie',
|
||||
'journey.picker.noAlbums': 'Žádná alba nenalezena',
|
||||
'journey.picker.selectDate': 'Vyberte datum',
|
||||
'journey.picker.search': 'Hledat',
|
||||
'dashboard.greeting.morning': 'Dobré ráno,',
|
||||
'dashboard.greeting.afternoon': 'Dobré odpoledne,',
|
||||
'dashboard.greeting.evening': 'Dobrý večer,',
|
||||
|
||||
@@ -1892,6 +1892,9 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Mitwirkende',
|
||||
'journey.detail.readMore': 'Mehr lesen',
|
||||
'journey.detail.prosCons': 'Pro & Contra',
|
||||
'journey.detail.photos': 'Fotos',
|
||||
'journey.detail.day': 'Tag {number}',
|
||||
'journey.detail.places': 'Orte',
|
||||
'journey.stats.days': 'Tage',
|
||||
'journey.stats.cities': 'Städte',
|
||||
'journey.stats.entries': 'Einträge',
|
||||
@@ -1918,6 +1921,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Wetter',
|
||||
'journey.editor.photoFirst': '1.',
|
||||
'journey.editor.makeFirst': 'Als 1. setzen',
|
||||
'journey.editor.searching': 'Suche...',
|
||||
'journey.mood.amazing': 'Großartig',
|
||||
'journey.mood.good': 'Gut',
|
||||
'journey.mood.neutral': 'Neutral',
|
||||
@@ -1962,6 +1966,13 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.linkDeleted': 'Link entfernt',
|
||||
'journey.share.deleteFailed': 'Entfernen fehlgeschlagen',
|
||||
'journey.share.updateFailed': 'Aktualisierung fehlgeschlagen',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Rolle',
|
||||
'journey.invite.viewer': 'Betrachter',
|
||||
'journey.invite.editor': 'Bearbeiter',
|
||||
'journey.invite.invite': 'Einladen',
|
||||
'journey.invite.inviting': 'Wird eingeladen...',
|
||||
'journey.settings.title': 'Journey-Einstellungen',
|
||||
'journey.settings.coverImage': 'Titelbild',
|
||||
'journey.settings.changeCover': 'Titelbild ändern',
|
||||
@@ -1992,6 +2003,16 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'Ende',
|
||||
'journey.pdf.saveAsPdf': 'Als PDF speichern',
|
||||
'journey.pdf.pages': 'Seiten',
|
||||
'journey.picker.tripPeriod': 'Reisezeitraum',
|
||||
'journey.picker.dateRange': 'Zeitraum',
|
||||
'journey.picker.allPhotos': 'Alle Fotos',
|
||||
'journey.picker.albums': 'Alben',
|
||||
'journey.picker.selected': 'ausgewählt',
|
||||
'journey.picker.addTo': 'Hinzufügen zu',
|
||||
'journey.picker.newGallery': 'Neue Galerie',
|
||||
'journey.picker.noAlbums': 'Keine Alben gefunden',
|
||||
'journey.picker.selectDate': 'Datum wählen',
|
||||
'journey.picker.search': 'Suchen',
|
||||
'dashboard.greeting.morning': 'Guten Morgen,',
|
||||
'dashboard.greeting.afternoon': 'Guten Tag,',
|
||||
'dashboard.greeting.evening': 'Guten Abend,',
|
||||
|
||||
@@ -1895,6 +1895,9 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Contributors',
|
||||
'journey.detail.readMore': 'Read more',
|
||||
'journey.detail.prosCons': 'Pros & Cons',
|
||||
'journey.detail.photos': 'photos',
|
||||
'journey.detail.day': 'Day {number}',
|
||||
'journey.detail.places': 'places',
|
||||
|
||||
// Journey Detail — Stats
|
||||
'journey.stats.days': 'Days',
|
||||
@@ -1929,6 +1932,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Weather',
|
||||
'journey.editor.photoFirst': '1st',
|
||||
'journey.editor.makeFirst': 'Make 1st',
|
||||
'journey.editor.searching': 'Searching...',
|
||||
|
||||
// Journey Entry — Moods
|
||||
'journey.mood.amazing': 'Amazing',
|
||||
@@ -1984,6 +1988,13 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.deleteFailed': 'Failed to delete',
|
||||
'journey.share.updateFailed': 'Failed to update',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Role',
|
||||
'journey.invite.viewer': 'Viewer',
|
||||
'journey.invite.editor': 'Editor',
|
||||
'journey.invite.invite': 'Invite',
|
||||
'journey.invite.inviting': 'Inviting...',
|
||||
|
||||
// Journey — Settings Dialog
|
||||
'journey.settings.title': 'Journey Settings',
|
||||
'journey.settings.coverImage': 'Cover Image',
|
||||
@@ -2019,6 +2030,16 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'The End',
|
||||
'journey.pdf.saveAsPdf': 'Save as PDF',
|
||||
'journey.pdf.pages': 'pages',
|
||||
'journey.picker.tripPeriod': 'Trip Period',
|
||||
'journey.picker.dateRange': 'Date Range',
|
||||
'journey.picker.allPhotos': 'All Photos',
|
||||
'journey.picker.albums': 'Albums',
|
||||
'journey.picker.selected': 'selected',
|
||||
'journey.picker.addTo': 'Add to',
|
||||
'journey.picker.newGallery': 'New Gallery',
|
||||
'journey.picker.noAlbums': 'No albums found',
|
||||
'journey.picker.selectDate': 'Select date',
|
||||
'journey.picker.search': 'Search',
|
||||
|
||||
// Dashboard Mobile
|
||||
'dashboard.greeting.morning': 'Good morning,',
|
||||
|
||||
@@ -1906,6 +1906,9 @@ const es: Record<string, string> = {
|
||||
'journey.detail.contributors': 'Colaboradores',
|
||||
'journey.detail.readMore': 'Leer más',
|
||||
'journey.detail.prosCons': 'Pros y contras',
|
||||
'journey.detail.photos': 'fotos',
|
||||
'journey.detail.day': 'Día {number}',
|
||||
'journey.detail.places': 'lugares',
|
||||
'journey.stats.days': 'Días',
|
||||
'journey.stats.cities': 'Ciudades',
|
||||
'journey.stats.entries': 'Entradas',
|
||||
@@ -1932,6 +1935,7 @@ const es: Record<string, string> = {
|
||||
'journey.editor.weather': 'Clima',
|
||||
'journey.editor.photoFirst': '1º',
|
||||
'journey.editor.makeFirst': 'Hacer 1º',
|
||||
'journey.editor.searching': 'Buscando...',
|
||||
'journey.mood.amazing': 'Increíble',
|
||||
'journey.mood.good': 'Bien',
|
||||
'journey.mood.neutral': 'Neutral',
|
||||
@@ -1976,6 +1980,13 @@ const es: Record<string, string> = {
|
||||
'journey.share.linkDeleted': 'Enlace para compartir eliminado',
|
||||
'journey.share.deleteFailed': 'No se pudo eliminar',
|
||||
'journey.share.updateFailed': 'No se pudo actualizar',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Rol',
|
||||
'journey.invite.viewer': 'Lector',
|
||||
'journey.invite.editor': 'Editor',
|
||||
'journey.invite.invite': 'Invitar',
|
||||
'journey.invite.inviting': 'Invitando...',
|
||||
'journey.settings.title': 'Ajustes de la travesía',
|
||||
'journey.settings.coverImage': 'Imagen de portada',
|
||||
'journey.settings.changeCover': 'Cambiar portada',
|
||||
@@ -2006,6 +2017,16 @@ const es: Record<string, string> = {
|
||||
'journey.pdf.theEnd': 'Fin',
|
||||
'journey.pdf.saveAsPdf': 'Guardar como PDF',
|
||||
'journey.pdf.pages': 'páginas',
|
||||
'journey.picker.tripPeriod': 'Período del viaje',
|
||||
'journey.picker.dateRange': 'Rango de fechas',
|
||||
'journey.picker.allPhotos': 'Todas las fotos',
|
||||
'journey.picker.albums': 'Álbumes',
|
||||
'journey.picker.selected': 'seleccionados',
|
||||
'journey.picker.addTo': 'Añadir a',
|
||||
'journey.picker.newGallery': 'Nueva galería',
|
||||
'journey.picker.noAlbums': 'No se encontraron álbumes',
|
||||
'journey.picker.selectDate': 'Seleccionar fecha',
|
||||
'journey.picker.search': 'Buscar',
|
||||
'dashboard.greeting.morning': 'Buenos días,',
|
||||
'dashboard.greeting.afternoon': 'Buenas tardes,',
|
||||
'dashboard.greeting.evening': 'Buenas noches,',
|
||||
|
||||
@@ -1900,6 +1900,9 @@ const fr: Record<string, string> = {
|
||||
'journey.detail.contributors': 'Contributeurs',
|
||||
'journey.detail.readMore': 'Lire la suite',
|
||||
'journey.detail.prosCons': 'Pour et contre',
|
||||
'journey.detail.photos': 'photos',
|
||||
'journey.detail.day': 'Jour {number}',
|
||||
'journey.detail.places': 'lieux',
|
||||
'journey.stats.days': 'Jours',
|
||||
'journey.stats.cities': 'Villes',
|
||||
'journey.stats.entries': 'Entrées',
|
||||
@@ -1926,6 +1929,7 @@ const fr: Record<string, string> = {
|
||||
'journey.editor.weather': 'Météo',
|
||||
'journey.editor.photoFirst': '1er',
|
||||
'journey.editor.makeFirst': 'Mettre en 1er',
|
||||
'journey.editor.searching': 'Recherche...',
|
||||
'journey.mood.amazing': 'Incroyable',
|
||||
'journey.mood.good': 'Bien',
|
||||
'journey.mood.neutral': 'Neutre',
|
||||
@@ -1970,6 +1974,13 @@ const fr: Record<string, string> = {
|
||||
'journey.share.linkDeleted': 'Lien de partage supprimé',
|
||||
'journey.share.deleteFailed': 'Échec de la suppression',
|
||||
'journey.share.updateFailed': 'Échec de la mise à jour',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Rôle',
|
||||
'journey.invite.viewer': 'Lecteur',
|
||||
'journey.invite.editor': 'Éditeur',
|
||||
'journey.invite.invite': 'Inviter',
|
||||
'journey.invite.inviting': 'Invitation...',
|
||||
'journey.settings.title': 'Paramètres du journal',
|
||||
'journey.settings.coverImage': 'Image de couverture',
|
||||
'journey.settings.changeCover': 'Changer la couverture',
|
||||
@@ -2000,6 +2011,16 @@ const fr: Record<string, string> = {
|
||||
'journey.pdf.theEnd': 'Fin',
|
||||
'journey.pdf.saveAsPdf': 'Enregistrer en PDF',
|
||||
'journey.pdf.pages': 'pages',
|
||||
'journey.picker.tripPeriod': 'Période du voyage',
|
||||
'journey.picker.dateRange': 'Plage de dates',
|
||||
'journey.picker.allPhotos': 'Toutes les photos',
|
||||
'journey.picker.albums': 'Albums',
|
||||
'journey.picker.selected': 'sélectionnés',
|
||||
'journey.picker.addTo': 'Ajouter à',
|
||||
'journey.picker.newGallery': 'Nouvelle galerie',
|
||||
'journey.picker.noAlbums': 'Aucun album trouvé',
|
||||
'journey.picker.selectDate': 'Sélectionner une date',
|
||||
'journey.picker.search': 'Rechercher',
|
||||
'dashboard.greeting.morning': 'Bonjour,',
|
||||
'dashboard.greeting.afternoon': 'Bon après-midi,',
|
||||
'dashboard.greeting.evening': 'Bonsoir,',
|
||||
|
||||
@@ -1901,6 +1901,9 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Közreműködők',
|
||||
'journey.detail.readMore': 'Tovább olvasás',
|
||||
'journey.detail.prosCons': 'Előnyök és hátrányok',
|
||||
'journey.detail.photos': 'fotók',
|
||||
'journey.detail.day': '{number}. nap',
|
||||
'journey.detail.places': 'helyek',
|
||||
'journey.stats.days': 'Napok',
|
||||
'journey.stats.cities': 'Városok',
|
||||
'journey.stats.entries': 'Bejegyzések',
|
||||
@@ -1927,6 +1930,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Időjárás',
|
||||
'journey.editor.photoFirst': '1.',
|
||||
'journey.editor.makeFirst': 'Legyen az 1.',
|
||||
'journey.editor.searching': 'Keresés...',
|
||||
'journey.mood.amazing': 'Fantasztikus',
|
||||
'journey.mood.good': 'Jó',
|
||||
'journey.mood.neutral': 'Semleges',
|
||||
@@ -1971,6 +1975,13 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.linkDeleted': 'Megosztó link törölve',
|
||||
'journey.share.deleteFailed': 'Nem sikerült törölni',
|
||||
'journey.share.updateFailed': 'Nem sikerült frissíteni',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Szerepkör',
|
||||
'journey.invite.viewer': 'Megtekintő',
|
||||
'journey.invite.editor': 'Szerkesztő',
|
||||
'journey.invite.invite': 'Meghívás',
|
||||
'journey.invite.inviting': 'Meghívás...',
|
||||
'journey.settings.title': 'Útinapló beállításai',
|
||||
'journey.settings.coverImage': 'Borítókép',
|
||||
'journey.settings.changeCover': 'Borító módosítása',
|
||||
@@ -2001,6 +2012,16 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'Vége',
|
||||
'journey.pdf.saveAsPdf': 'Mentés PDF-ként',
|
||||
'journey.pdf.pages': 'oldal',
|
||||
'journey.picker.tripPeriod': 'Utazási időszak',
|
||||
'journey.picker.dateRange': 'Időszak',
|
||||
'journey.picker.allPhotos': 'Összes fotó',
|
||||
'journey.picker.albums': 'Albumok',
|
||||
'journey.picker.selected': 'kiválasztva',
|
||||
'journey.picker.addTo': 'Hozzáadás',
|
||||
'journey.picker.newGallery': 'Új galéria',
|
||||
'journey.picker.noAlbums': 'Nem található album',
|
||||
'journey.picker.selectDate': 'Dátum választása',
|
||||
'journey.picker.search': 'Keresés',
|
||||
'dashboard.greeting.morning': 'Jó reggelt,',
|
||||
'dashboard.greeting.afternoon': 'Jó napot,',
|
||||
'dashboard.greeting.evening': 'Jó estét,',
|
||||
|
||||
@@ -1901,6 +1901,9 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Contributori',
|
||||
'journey.detail.readMore': 'Leggi di più',
|
||||
'journey.detail.prosCons': 'Pro e contro',
|
||||
'journey.detail.photos': 'foto',
|
||||
'journey.detail.day': 'Giorno {number}',
|
||||
'journey.detail.places': 'luoghi',
|
||||
'journey.stats.days': 'Giorni',
|
||||
'journey.stats.cities': 'Città',
|
||||
'journey.stats.entries': 'Voci',
|
||||
@@ -1927,6 +1930,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Meteo',
|
||||
'journey.editor.photoFirst': '1°',
|
||||
'journey.editor.makeFirst': 'Metti 1°',
|
||||
'journey.editor.searching': 'Ricerca...',
|
||||
'journey.mood.amazing': 'Fantastico',
|
||||
'journey.mood.good': 'Buono',
|
||||
'journey.mood.neutral': 'Neutro',
|
||||
@@ -1971,6 +1975,13 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.linkDeleted': 'Link di condivisione eliminato',
|
||||
'journey.share.deleteFailed': 'Eliminazione fallita',
|
||||
'journey.share.updateFailed': 'Aggiornamento fallito',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Ruolo',
|
||||
'journey.invite.viewer': 'Visualizzatore',
|
||||
'journey.invite.editor': 'Editore',
|
||||
'journey.invite.invite': 'Invita',
|
||||
'journey.invite.inviting': 'Invito in corso...',
|
||||
'journey.settings.title': 'Impostazioni del diario',
|
||||
'journey.settings.coverImage': 'Immagine di copertina',
|
||||
'journey.settings.changeCover': 'Cambia copertina',
|
||||
@@ -2001,6 +2012,16 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'Fine',
|
||||
'journey.pdf.saveAsPdf': 'Salva come PDF',
|
||||
'journey.pdf.pages': 'pagine',
|
||||
'journey.picker.tripPeriod': 'Periodo del viaggio',
|
||||
'journey.picker.dateRange': 'Intervallo di date',
|
||||
'journey.picker.allPhotos': 'Tutte le foto',
|
||||
'journey.picker.albums': 'Album',
|
||||
'journey.picker.selected': 'selezionati',
|
||||
'journey.picker.addTo': 'Aggiungi a',
|
||||
'journey.picker.newGallery': 'Nuova galleria',
|
||||
'journey.picker.noAlbums': 'Nessun album trovato',
|
||||
'journey.picker.selectDate': 'Seleziona data',
|
||||
'journey.picker.search': 'Cerca',
|
||||
'dashboard.greeting.morning': 'Buongiorno,',
|
||||
'dashboard.greeting.afternoon': 'Buon pomeriggio,',
|
||||
'dashboard.greeting.evening': 'Buonasera,',
|
||||
|
||||
@@ -1900,6 +1900,9 @@ const nl: Record<string, string> = {
|
||||
'journey.detail.contributors': 'Bijdragers',
|
||||
'journey.detail.readMore': 'Lees meer',
|
||||
'journey.detail.prosCons': 'Voor- & nadelen',
|
||||
'journey.detail.photos': 'foto\'s',
|
||||
'journey.detail.day': 'Dag {number}',
|
||||
'journey.detail.places': 'plaatsen',
|
||||
'journey.stats.days': 'Dagen',
|
||||
'journey.stats.cities': 'Steden',
|
||||
'journey.stats.entries': 'Vermeldingen',
|
||||
@@ -1926,6 +1929,7 @@ const nl: Record<string, string> = {
|
||||
'journey.editor.weather': 'Weer',
|
||||
'journey.editor.photoFirst': '1e',
|
||||
'journey.editor.makeFirst': 'Maak 1e',
|
||||
'journey.editor.searching': 'Zoeken...',
|
||||
'journey.mood.amazing': 'Fantastisch',
|
||||
'journey.mood.good': 'Goed',
|
||||
'journey.mood.neutral': 'Neutraal',
|
||||
@@ -1970,6 +1974,13 @@ const nl: Record<string, string> = {
|
||||
'journey.share.linkDeleted': 'Deellink verwijderd',
|
||||
'journey.share.deleteFailed': 'Verwijderen mislukt',
|
||||
'journey.share.updateFailed': 'Bijwerken mislukt',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Rol',
|
||||
'journey.invite.viewer': 'Kijker',
|
||||
'journey.invite.editor': 'Bewerker',
|
||||
'journey.invite.invite': 'Uitnodigen',
|
||||
'journey.invite.inviting': 'Uitnodigen...',
|
||||
'journey.settings.title': 'Reisverslaginstellingen',
|
||||
'journey.settings.coverImage': 'Omslagfoto',
|
||||
'journey.settings.changeCover': 'Omslag wijzigen',
|
||||
@@ -2000,6 +2011,16 @@ const nl: Record<string, string> = {
|
||||
'journey.pdf.theEnd': 'Einde',
|
||||
'journey.pdf.saveAsPdf': 'Opslaan als PDF',
|
||||
'journey.pdf.pages': 'pagina\'s',
|
||||
'journey.picker.tripPeriod': 'Reisperiode',
|
||||
'journey.picker.dateRange': 'Datumbereik',
|
||||
'journey.picker.allPhotos': 'Alle foto\'s',
|
||||
'journey.picker.albums': 'Albums',
|
||||
'journey.picker.selected': 'geselecteerd',
|
||||
'journey.picker.addTo': 'Toevoegen aan',
|
||||
'journey.picker.newGallery': 'Nieuwe galerij',
|
||||
'journey.picker.noAlbums': 'Geen albums gevonden',
|
||||
'journey.picker.selectDate': 'Selecteer datum',
|
||||
'journey.picker.search': 'Zoeken',
|
||||
'dashboard.greeting.morning': 'Goedemorgen,',
|
||||
'dashboard.greeting.afternoon': 'Goedemiddag,',
|
||||
'dashboard.greeting.evening': 'Goedenavond,',
|
||||
|
||||
@@ -1896,6 +1896,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.contributors': 'Współtwórcy',
|
||||
'journey.detail.readMore': 'Czytaj dalej',
|
||||
'journey.detail.prosCons': 'Zalety i wady',
|
||||
'journey.detail.photos': 'zdjęć',
|
||||
'journey.detail.day': 'Dzień {number}',
|
||||
'journey.detail.places': 'miejsc',
|
||||
'journey.stats.days': 'Dni',
|
||||
'journey.stats.cities': 'Miasta',
|
||||
'journey.stats.entries': 'Wpisy',
|
||||
@@ -1922,6 +1925,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.editor.weather': 'Pogoda',
|
||||
'journey.editor.photoFirst': '1.',
|
||||
'journey.editor.makeFirst': 'Ustaw jako 1.',
|
||||
'journey.editor.searching': 'Szukanie...',
|
||||
'journey.mood.amazing': 'Niesamowity',
|
||||
'journey.mood.good': 'Dobry',
|
||||
'journey.mood.neutral': 'Neutralny',
|
||||
@@ -1966,6 +1970,13 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.share.linkDeleted': 'Link udostępniania usunięty',
|
||||
'journey.share.deleteFailed': 'Usunięcie nie powiodło się',
|
||||
'journey.share.updateFailed': 'Aktualizacja nie powiodła się',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Rola',
|
||||
'journey.invite.viewer': 'Obserwator',
|
||||
'journey.invite.editor': 'Redaktor',
|
||||
'journey.invite.invite': 'Zaproś',
|
||||
'journey.invite.inviting': 'Zapraszanie...',
|
||||
'journey.settings.title': 'Ustawienia dziennika podróży',
|
||||
'journey.settings.coverImage': 'Zdjęcie okładkowe',
|
||||
'journey.settings.changeCover': 'Zmień okładkę',
|
||||
@@ -1996,6 +2007,16 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.pdf.theEnd': 'Koniec',
|
||||
'journey.pdf.saveAsPdf': 'Zapisz jako PDF',
|
||||
'journey.pdf.pages': 'stron',
|
||||
'journey.picker.tripPeriod': 'Okres podróży',
|
||||
'journey.picker.dateRange': 'Zakres dat',
|
||||
'journey.picker.allPhotos': 'Wszystkie zdjęcia',
|
||||
'journey.picker.albums': 'Albumy',
|
||||
'journey.picker.selected': 'wybranych',
|
||||
'journey.picker.addTo': 'Dodaj do',
|
||||
'journey.picker.newGallery': 'Nowa galeria',
|
||||
'journey.picker.noAlbums': 'Nie znaleziono albumów',
|
||||
'journey.picker.selectDate': 'Wybierz datę',
|
||||
'journey.picker.search': 'Szukaj',
|
||||
'dashboard.greeting.morning': 'Dzień dobry,',
|
||||
'dashboard.greeting.afternoon': 'Dzień dobry,',
|
||||
'dashboard.greeting.evening': 'Dobry wieczór,',
|
||||
|
||||
@@ -1900,6 +1900,9 @@ const ru: Record<string, string> = {
|
||||
'journey.detail.contributors': 'Участники',
|
||||
'journey.detail.readMore': 'Читать далее',
|
||||
'journey.detail.prosCons': 'Плюсы и минусы',
|
||||
'journey.detail.photos': 'фото',
|
||||
'journey.detail.day': 'День {number}',
|
||||
'journey.detail.places': 'мест',
|
||||
'journey.stats.days': 'Дней',
|
||||
'journey.stats.cities': 'Городов',
|
||||
'journey.stats.entries': 'Записей',
|
||||
@@ -1926,6 +1929,7 @@ const ru: Record<string, string> = {
|
||||
'journey.editor.weather': 'Погода',
|
||||
'journey.editor.photoFirst': '1-е',
|
||||
'journey.editor.makeFirst': 'Сделать 1-м',
|
||||
'journey.editor.searching': 'Поиск...',
|
||||
'journey.mood.amazing': 'Потрясающе',
|
||||
'journey.mood.good': 'Хорошо',
|
||||
'journey.mood.neutral': 'Нейтрально',
|
||||
@@ -1970,6 +1974,13 @@ const ru: Record<string, string> = {
|
||||
'journey.share.linkDeleted': 'Ссылка удалена',
|
||||
'journey.share.deleteFailed': 'Не удалось удалить',
|
||||
'journey.share.updateFailed': 'Не удалось обновить',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'Роль',
|
||||
'journey.invite.viewer': 'Наблюдатель',
|
||||
'journey.invite.editor': 'Редактор',
|
||||
'journey.invite.invite': 'Пригласить',
|
||||
'journey.invite.inviting': 'Приглашаем...',
|
||||
'journey.settings.title': 'Настройки путешествия',
|
||||
'journey.settings.coverImage': 'Обложка',
|
||||
'journey.settings.changeCover': 'Сменить обложку',
|
||||
@@ -2000,6 +2011,16 @@ const ru: Record<string, string> = {
|
||||
'journey.pdf.theEnd': 'Конец',
|
||||
'journey.pdf.saveAsPdf': 'Сохранить как PDF',
|
||||
'journey.pdf.pages': 'страниц',
|
||||
'journey.picker.tripPeriod': 'Период поездки',
|
||||
'journey.picker.dateRange': 'Диапазон дат',
|
||||
'journey.picker.allPhotos': 'Все фото',
|
||||
'journey.picker.albums': 'Альбомы',
|
||||
'journey.picker.selected': 'выбрано',
|
||||
'journey.picker.addTo': 'Добавить в',
|
||||
'journey.picker.newGallery': 'Новая галерея',
|
||||
'journey.picker.noAlbums': 'Альбомы не найдены',
|
||||
'journey.picker.selectDate': 'Выберите дату',
|
||||
'journey.picker.search': 'Поиск',
|
||||
'dashboard.greeting.morning': 'Доброе утро,',
|
||||
'dashboard.greeting.afternoon': 'Добрый день,',
|
||||
'dashboard.greeting.evening': 'Добрый вечер,',
|
||||
|
||||
@@ -1900,6 +1900,9 @@ const zh: Record<string, string> = {
|
||||
'journey.detail.contributors': '贡献者',
|
||||
'journey.detail.readMore': '阅读更多',
|
||||
'journey.detail.prosCons': '优缺点',
|
||||
'journey.detail.photos': '照片',
|
||||
'journey.detail.day': '第{number}天',
|
||||
'journey.detail.places': '个地点',
|
||||
'journey.stats.days': '天',
|
||||
'journey.stats.cities': '城市',
|
||||
'journey.stats.entries': '条目',
|
||||
@@ -1910,7 +1913,7 @@ const zh: Record<string, string> = {
|
||||
'journey.synced.places': '个地点',
|
||||
'journey.synced.synced': '已同步',
|
||||
'journey.editor.uploadPhotos': '上传照片',
|
||||
'journey.editor.fromGallery': '从相册选择',
|
||||
'journey.editor.fromGallery': '从相册',
|
||||
'journey.editor.allPhotosAdded': '所有照片已添加',
|
||||
'journey.editor.writeStory': '写下你的故事...',
|
||||
'journey.editor.prosCons': '优缺点',
|
||||
@@ -1926,6 +1929,7 @@ const zh: Record<string, string> = {
|
||||
'journey.editor.weather': '天气',
|
||||
'journey.editor.photoFirst': '第1张',
|
||||
'journey.editor.makeFirst': '设为第1张',
|
||||
'journey.editor.searching': '搜索中...',
|
||||
'journey.mood.amazing': '太棒了',
|
||||
'journey.mood.good': '不错',
|
||||
'journey.mood.neutral': '一般',
|
||||
@@ -1970,6 +1974,13 @@ const zh: Record<string, string> = {
|
||||
'journey.share.linkDeleted': '分享链接已删除',
|
||||
'journey.share.deleteFailed': '删除失败',
|
||||
'journey.share.updateFailed': '更新失败',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': '角色',
|
||||
'journey.invite.viewer': '查看者',
|
||||
'journey.invite.editor': '编辑者',
|
||||
'journey.invite.invite': '邀请',
|
||||
'journey.invite.inviting': '邀请中...',
|
||||
'journey.settings.title': '旅程设置',
|
||||
'journey.settings.coverImage': '封面图片',
|
||||
'journey.settings.changeCover': '更换封面',
|
||||
@@ -2000,6 +2011,16 @@ const zh: Record<string, string> = {
|
||||
'journey.pdf.theEnd': '终',
|
||||
'journey.pdf.saveAsPdf': '保存为 PDF',
|
||||
'journey.pdf.pages': '页',
|
||||
'journey.picker.tripPeriod': '旅行时间段',
|
||||
'journey.picker.dateRange': '日期范围',
|
||||
'journey.picker.allPhotos': '所有照片',
|
||||
'journey.picker.albums': '相册',
|
||||
'journey.picker.selected': '已选择',
|
||||
'journey.picker.addTo': '添加到',
|
||||
'journey.picker.newGallery': '新相册',
|
||||
'journey.picker.noAlbums': '未找到相册',
|
||||
'journey.picker.selectDate': '选择日期',
|
||||
'journey.picker.search': '搜索',
|
||||
'dashboard.greeting.morning': '早上好,',
|
||||
'dashboard.greeting.afternoon': '下午好,',
|
||||
'dashboard.greeting.evening': '晚上好,',
|
||||
|
||||
@@ -1861,6 +1861,9 @@ const zhTw: Record<string, string> = {
|
||||
'journey.detail.contributors': '貢獻者',
|
||||
'journey.detail.readMore': '閱讀更多',
|
||||
'journey.detail.prosCons': '優缺點',
|
||||
'journey.detail.photos': '照片',
|
||||
'journey.detail.day': '第{number}天',
|
||||
'journey.detail.places': '個地點',
|
||||
'journey.stats.days': '天',
|
||||
'journey.stats.cities': '城市',
|
||||
'journey.stats.entries': '條目',
|
||||
@@ -1871,7 +1874,7 @@ const zhTw: Record<string, string> = {
|
||||
'journey.synced.places': '個地點',
|
||||
'journey.synced.synced': '已同步',
|
||||
'journey.editor.uploadPhotos': '上傳照片',
|
||||
'journey.editor.fromGallery': '從相簿選擇',
|
||||
'journey.editor.fromGallery': '從相簿',
|
||||
'journey.editor.allPhotosAdded': '所有照片已新增',
|
||||
'journey.editor.writeStory': '寫下你的故事...',
|
||||
'journey.editor.prosCons': '優缺點',
|
||||
@@ -1887,6 +1890,7 @@ const zhTw: Record<string, string> = {
|
||||
'journey.editor.weather': '天氣',
|
||||
'journey.editor.photoFirst': '第1張',
|
||||
'journey.editor.makeFirst': '設為第1張',
|
||||
'journey.editor.searching': '搜尋中...',
|
||||
'journey.mood.amazing': '太棒了',
|
||||
'journey.mood.good': '不錯',
|
||||
'journey.mood.neutral': '一般',
|
||||
@@ -1931,6 +1935,13 @@ const zhTw: Record<string, string> = {
|
||||
'journey.share.linkDeleted': '分享連結已刪除',
|
||||
'journey.share.deleteFailed': '刪除失敗',
|
||||
'journey.share.updateFailed': '更新失敗',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': '角色',
|
||||
'journey.invite.viewer': '檢視者',
|
||||
'journey.invite.editor': '編輯者',
|
||||
'journey.invite.invite': '邀請',
|
||||
'journey.invite.inviting': '邀請中...',
|
||||
'journey.settings.title': '旅程設定',
|
||||
'journey.settings.coverImage': '封面圖片',
|
||||
'journey.settings.changeCover': '更換封面',
|
||||
@@ -1961,6 +1972,16 @@ const zhTw: Record<string, string> = {
|
||||
'journey.pdf.theEnd': '終',
|
||||
'journey.pdf.saveAsPdf': '儲存為 PDF',
|
||||
'journey.pdf.pages': '頁',
|
||||
'journey.picker.tripPeriod': '旅行期間',
|
||||
'journey.picker.dateRange': '日期範圍',
|
||||
'journey.picker.allPhotos': '所有照片',
|
||||
'journey.picker.albums': '相簿',
|
||||
'journey.picker.selected': '已選擇',
|
||||
'journey.picker.addTo': '新增至',
|
||||
'journey.picker.newGallery': '新相簿',
|
||||
'journey.picker.noAlbums': '未找到相簿',
|
||||
'journey.picker.selectDate': '選擇日期',
|
||||
'journey.picker.search': '搜尋',
|
||||
'dashboard.greeting.morning': '早安,',
|
||||
'dashboard.greeting.afternoon': '午安,',
|
||||
'dashboard.greeting.evening': '晚安,',
|
||||
|
||||
@@ -113,6 +113,7 @@ const mockJourneyDetail = {
|
||||
{
|
||||
id: 100,
|
||||
entry_id: 10,
|
||||
photo_id: 100,
|
||||
provider: 'local',
|
||||
file_path: 'photos/test.jpg',
|
||||
asset_id: null,
|
||||
@@ -547,17 +548,17 @@ describe('JourneyDetailPage', () => {
|
||||
...mockJourneyDetail.entries[0],
|
||||
photos: [
|
||||
{
|
||||
id: 100, entry_id: 10, provider: 'local' as const, file_path: 'photos/a.jpg',
|
||||
id: 100, entry_id: 10, photo_id: 100, provider: 'local' as const, file_path: 'photos/a.jpg',
|
||||
asset_id: null, owner_id: null, thumbnail_path: null,
|
||||
caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now,
|
||||
},
|
||||
{
|
||||
id: 101, entry_id: 10, provider: 'local' as const, file_path: 'photos/b.jpg',
|
||||
id: 101, entry_id: 10, photo_id: 101, provider: 'local' as const, file_path: 'photos/b.jpg',
|
||||
asset_id: null, owner_id: null, thumbnail_path: null,
|
||||
caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now,
|
||||
},
|
||||
{
|
||||
id: 102, entry_id: 10, provider: 'local' as const, file_path: 'photos/c.jpg',
|
||||
id: 102, entry_id: 10, photo_id: 102, provider: 'local' as const, file_path: 'photos/c.jpg',
|
||||
asset_id: null, owner_id: null, thumbnail_path: null,
|
||||
caption: null, sort_order: 2, width: 800, height: 600, shared: 1, created_at: now,
|
||||
},
|
||||
@@ -1991,7 +1992,7 @@ describe('JourneyDetailPage', () => {
|
||||
const immichEntry = {
|
||||
...mockJourneyDetail.entries[0],
|
||||
photos: [{
|
||||
id: 200, entry_id: 10, provider: 'immich', file_path: null,
|
||||
id: 200, entry_id: 10, photo_id: 200, provider: 'immich', file_path: null,
|
||||
asset_id: 'asset-123', owner_id: 1, thumbnail_path: null,
|
||||
caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now,
|
||||
}],
|
||||
@@ -2025,7 +2026,7 @@ describe('JourneyDetailPage', () => {
|
||||
const synologyEntry = {
|
||||
...mockJourneyDetail.entries[0],
|
||||
photos: [{
|
||||
id: 201, entry_id: 10, provider: 'synology', file_path: null,
|
||||
id: 201, entry_id: 10, photo_id: 201, provider: 'synology', file_path: null,
|
||||
asset_id: 'syn-456', owner_id: 1, thumbnail_path: null,
|
||||
caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now,
|
||||
}],
|
||||
@@ -2617,11 +2618,11 @@ describe('JourneyDetailPage', () => {
|
||||
const multiPhotoEntry = {
|
||||
...mockJourneyDetail.entries[0],
|
||||
photos: [
|
||||
{ id: 100, entry_id: 10, provider: 'local', file_path: 'photos/a.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 101, entry_id: 10, provider: 'local', file_path: 'photos/b.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 102, entry_id: 10, provider: 'local', file_path: 'photos/c.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 2, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 103, entry_id: 10, provider: 'local', file_path: 'photos/d.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 3, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 104, entry_id: 10, provider: 'local', file_path: 'photos/e.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 4, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 100, entry_id: 10, photo_id: 100, provider: 'local', file_path: 'photos/a.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 101, entry_id: 10, photo_id: 101, provider: 'local', file_path: 'photos/b.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 102, entry_id: 10, photo_id: 102, provider: 'local', file_path: 'photos/c.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 2, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 103, entry_id: 10, photo_id: 103, provider: 'local', file_path: 'photos/d.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 3, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 104, entry_id: 10, photo_id: 104, provider: 'local', file_path: 'photos/e.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 4, width: 800, height: 600, shared: 1, created_at: now },
|
||||
],
|
||||
};
|
||||
setupDefaultHandlers({
|
||||
@@ -2645,8 +2646,8 @@ describe('JourneyDetailPage', () => {
|
||||
const twoPhotoEntry = {
|
||||
...mockJourneyDetail.entries[0],
|
||||
photos: [
|
||||
{ id: 100, entry_id: 10, provider: 'local', file_path: 'photos/a.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 101, entry_id: 10, provider: 'local', file_path: 'photos/b.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 100, entry_id: 10, photo_id: 100, provider: 'local', file_path: 'photos/a.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 101, entry_id: 10, photo_id: 101, provider: 'local', file_path: 'photos/b.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now },
|
||||
],
|
||||
};
|
||||
setupDefaultHandlers({
|
||||
@@ -3344,7 +3345,7 @@ describe('JourneyDetailPage', () => {
|
||||
}),
|
||||
http.post('/api/journeys/entries/88/photos', () => {
|
||||
uploadCalled = true;
|
||||
return HttpResponse.json([{ id: 999, entry_id: 88, provider: 'local', file_path: 'photos/new.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 100, height: 100, shared: 1, created_at: now }]);
|
||||
return HttpResponse.json([{ id: 999, entry_id: 88, photo_id: 999, provider: 'local', file_path: 'photos/new.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 100, height: 100, shared: 1, created_at: now }]);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -3510,8 +3511,8 @@ describe('JourneyDetailPage', () => {
|
||||
const entryWithMultiPhotos = {
|
||||
...mockJourneyDetail.entries[0],
|
||||
photos: [
|
||||
{ id: 100, entry_id: 10, provider: 'local', file_path: 'photos/a.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 101, entry_id: 10, provider: 'local', file_path: 'photos/b.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 100, entry_id: 10, photo_id: 100, provider: 'local', file_path: 'photos/a.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 800, height: 600, shared: 1, created_at: now },
|
||||
{ id: 101, entry_id: 10, photo_id: 101, provider: 'local', file_path: 'photos/b.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 1, width: 800, height: 600, shared: 1, created_at: now },
|
||||
],
|
||||
};
|
||||
setupDefaultHandlers({
|
||||
@@ -3564,7 +3565,7 @@ describe('JourneyDetailPage', () => {
|
||||
}),
|
||||
http.post('/api/journeys/entries/11/photos', () => {
|
||||
uploadCalled = true;
|
||||
return HttpResponse.json([{ id: 300, entry_id: 11, provider: 'local', file_path: 'photos/new.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 100, height: 100, shared: 1, created_at: now }]);
|
||||
return HttpResponse.json([{ id: 300, entry_id: 11, photo_id: 300, provider: 'local', file_path: 'photos/new.jpg', asset_id: null, owner_id: null, thumbnail_path: null, caption: null, sort_order: 0, width: 100, height: 100, shared: 1, created_at: now }]);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -64,20 +64,14 @@ function groupByDate(entries: JourneyEntry[]): Map<string, JourneyEntry[]> {
|
||||
function formatDate(d: string): { weekday: string; month: string; day: number } {
|
||||
const date = new Date(d + 'T00:00:00')
|
||||
return {
|
||||
weekday: date.toLocaleDateString('en', { weekday: 'long' }),
|
||||
month: date.toLocaleDateString('en', { month: 'long' }),
|
||||
weekday: date.toLocaleDateString(undefined, { weekday: 'long' }),
|
||||
month: date.toLocaleDateString(undefined, { month: 'long' }),
|
||||
day: date.getDate(),
|
||||
}
|
||||
}
|
||||
|
||||
function photoUrl(p: JourneyPhoto, size: 'thumbnail' | 'original' = 'thumbnail'): string {
|
||||
if (p.provider === 'local') {
|
||||
return `/uploads/${p.file_path}`
|
||||
}
|
||||
// Immich / Synology — stream through the existing memories proxy
|
||||
// tripId=0 is a placeholder, the proxy uses owner_id to find credentials
|
||||
const kind = size === 'thumbnail' ? 'thumbnail' : 'original'
|
||||
return `/api/integrations/memories/${p.provider}/assets/0/${p.asset_id}/${p.owner_id}/${kind}`
|
||||
return `/api/photos/${p.photo_id}/${size}`
|
||||
}
|
||||
|
||||
export default function JourneyDetailPage() {
|
||||
@@ -195,7 +189,7 @@ export default function JourneyDetailPage() {
|
||||
{/* Back link — desktop */}
|
||||
<button onClick={() => navigate('/journey')} className="hidden md:inline-flex items-center gap-1.5 text-[12px] text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 mb-4 mx-0">
|
||||
<ArrowLeft size={14} />
|
||||
Back to Journey
|
||||
{t('journey.detail.backToJourney')}
|
||||
</button>
|
||||
|
||||
{/* Hero card — full width */}
|
||||
@@ -220,7 +214,7 @@ export default function JourneyDetailPage() {
|
||||
)}
|
||||
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/[0.12] backdrop-blur border border-white/15 rounded-full text-[11px] font-medium">
|
||||
<RefreshCw size={11} />
|
||||
Synced with Trips
|
||||
{t('journey.detail.syncedWithTrips')}
|
||||
</div>
|
||||
</div>
|
||||
{/* Mobile: back button on the left */}
|
||||
@@ -326,7 +320,7 @@ export default function JourneyDetailPage() {
|
||||
{dayIdx + 1}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[14px] font-semibold text-zinc-900 dark:text-white">{fd.weekday}, {fd.month} {fd.day}</h3>
|
||||
<h3 className="text-[14px] font-semibold text-zinc-900 dark:text-white">{new Date(date + 'T00:00:00').toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long' })}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[11px] text-zinc-500">
|
||||
@@ -405,17 +399,17 @@ export default function JourneyDetailPage() {
|
||||
|
||||
{/* Stats panel */}
|
||||
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl p-4">
|
||||
<div className="text-[10px] font-semibold tracking-[0.1em] uppercase text-zinc-500 mb-3.5">{t('journey.detail.journeyStats')}</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="text-[10px] font-semibold tracking-[0.1em] uppercase text-zinc-500 mb-3">{t('journey.detail.journeyStats')}</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ value: `${sortedDates.length}`, label: t('journey.stats.days') },
|
||||
{ value: `${current.stats.entries}`, label: t('journey.stats.entries') },
|
||||
{ value: `${current.stats.photos}`, label: t('journey.stats.photos') },
|
||||
{ value: `${current.stats.cities}`, label: t('journey.stats.cities') },
|
||||
{ value: sortedDates.length, label: t('journey.stats.days') },
|
||||
{ value: current.stats.entries, label: t('journey.stats.entries') },
|
||||
{ value: current.stats.photos, label: t('journey.stats.photos') },
|
||||
{ value: current.stats.cities, label: t('journey.stats.cities') },
|
||||
].map(s => (
|
||||
<div key={s.label}>
|
||||
<div className="text-[20px] font-bold tracking-[-0.02em] text-zinc-900 dark:text-white">{s.value}</div>
|
||||
<div className="text-[10px] uppercase tracking-[0.08em] text-zinc-500 font-medium">{s.label}</div>
|
||||
<div key={s.label} className="rounded-lg bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-zinc-700/50 px-3 py-2.5">
|
||||
<div className="text-[18px] font-bold tracking-[-0.02em] text-zinc-900 dark:text-white leading-none mb-0.5">{s.value}</div>
|
||||
<div className="text-[9px] uppercase tracking-[0.1em] text-zinc-400 dark:text-zinc-500 font-semibold">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -440,7 +434,7 @@ export default function JourneyDetailPage() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-zinc-900 dark:text-white truncate">{trip.title}</div>
|
||||
<div className="text-[10px] text-zinc-500 flex items-center gap-1.5">
|
||||
{trip.place_count || 0} places
|
||||
{trip.place_count || 0} {t('journey.detail.places')}
|
||||
<span className="inline-flex items-center gap-0.5 px-1.5 py-px rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[9px] font-medium"><span className="w-1 h-1 rounded-full bg-emerald-500" />{t('journey.synced.synced')}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -674,7 +668,7 @@ function MapView({ entries, mapEntries, sortedDates, activeLocationId, fullMapRe
|
||||
<div key={date}>
|
||||
{/* Day separator */}
|
||||
<div className="flex items-center gap-2.5 py-3">
|
||||
<span className="text-[10px] font-bold text-zinc-500 dark:text-zinc-400 tracking-[0.12em] uppercase">Day {dayIdx + 1}</span>
|
||||
<span className="text-[10px] font-bold text-zinc-500 dark:text-zinc-400 tracking-[0.12em] uppercase">{t('journey.detail.day', { number: dayIdx + 1 })}</span>
|
||||
<span className="text-[10px] text-zinc-400 font-medium">{fd.month} {fd.day}</span>
|
||||
<div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
|
||||
</div>
|
||||
@@ -834,14 +828,16 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<span className="text-[11px] text-zinc-500">{allPhotos.length} photos</span>
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 text-[10px] font-medium text-zinc-500 dark:text-zinc-400">
|
||||
<Camera size={10} /> {allPhotos.length} {t('journey.detail.photos')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => galleryFileRef.current?.click()}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[11px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100"
|
||||
>
|
||||
<Plus size={12} />
|
||||
Upload
|
||||
{t('common.upload')}
|
||||
</button>
|
||||
{availableProviders.map(p => (
|
||||
<button
|
||||
@@ -873,7 +869,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
|
||||
onClick={() => onPhotoClick(entry.photos, entry.photos.indexOf(photo))}
|
||||
>
|
||||
<img
|
||||
src={photo.provider !== 'local' ? photoUrl(photo, 'original') : photoUrl(photo)}
|
||||
src={photoUrl(photo, 'original')}
|
||||
alt={photo.caption || ''}
|
||||
className="w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||
loading="lazy"
|
||||
@@ -901,7 +897,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
|
||||
)}
|
||||
<div className="absolute bottom-1.5 left-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-black/50 backdrop-blur text-white">
|
||||
{entry.entry_date}
|
||||
{new Date(entry.entry_date + 'T12:00:00').toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1272,7 +1268,7 @@ function CheckinCard({ entry, onClick }: { entry: JourneyEntry; onClick: () => v
|
||||
}
|
||||
|
||||
function PhotoImg({ photo, className, style, onClick }: { photo: JourneyPhoto; className?: string; style?: React.CSSProperties; onClick?: () => void }) {
|
||||
const src = photo.provider !== 'local' ? photoUrl(photo, 'original') : photoUrl(photo)
|
||||
const src = photoUrl(photo, 'original')
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
@@ -1368,7 +1364,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
onAdd: (assetIds: string[], entryId: number | null) => Promise<void>
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [filter, setFilter] = useState<'trip' | 'custom' | 'album'>('trip')
|
||||
const [filter, setFilter] = useState<'trip' | 'custom' | 'all' | 'album'>('trip')
|
||||
const [photos, setPhotos] = useState<any[]>([])
|
||||
const [albums, setAlbums] = useState<any[]>([])
|
||||
const [selectedAlbum, setSelectedAlbum] = useState<string | null>(null)
|
||||
@@ -1413,6 +1409,8 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
useEffect(() => {
|
||||
if (filter === 'trip' && tripRange.from && tripRange.to) {
|
||||
searchPhotos(tripRange.from, tripRange.to)
|
||||
} else if (filter === 'all') {
|
||||
searchPhotos('', '')
|
||||
} else if (filter === 'album' && albums.length === 0) {
|
||||
loadAlbums()
|
||||
}
|
||||
@@ -1432,7 +1430,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
|
||||
const targetLabel = targetEntryId
|
||||
? entries.find(e => e.id === targetEntryId)?.title || entries.find(e => e.id === targetEntryId)?.entry_date || t('journey.stats.entries')
|
||||
: 'Gallery'
|
||||
: t('journey.picker.newGallery')
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center p-5" style={{ background: 'rgba(9,9,11,0.6)', backdropFilter: 'blur(6px)' }}>
|
||||
@@ -1453,9 +1451,10 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1.5 mb-3">
|
||||
{[
|
||||
{ id: 'trip' as const, label: t('journey.trips.link') },
|
||||
{ id: 'custom' as const, label: t('common.edit') },
|
||||
{ id: 'album' as const, label: t('journey.share.gallery') },
|
||||
{ id: 'trip' as const, label: t('journey.picker.tripPeriod') },
|
||||
{ id: 'custom' as const, label: t('journey.picker.dateRange') },
|
||||
{ id: 'all' as const, label: t('journey.picker.allPhotos') },
|
||||
{ id: 'album' as const, label: t('journey.picker.albums') },
|
||||
].map(f => (
|
||||
<button
|
||||
key={f.id}
|
||||
@@ -1479,11 +1478,11 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
<>
|
||||
<Calendar size={13} className="text-zinc-400" />
|
||||
<span className="font-medium text-zinc-900 dark:text-white">
|
||||
{new Date(tripRange.from + 'T00:00:00').toLocaleDateString('en', { month: 'short', day: 'numeric' })}
|
||||
{new Date(tripRange.from + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
<span className="text-zinc-400">—</span>
|
||||
<span className="font-medium text-zinc-900 dark:text-white">
|
||||
{new Date(tripRange.to + 'T00:00:00').toLocaleDateString('en', { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||
{new Date(tripRange.to + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||
</span>
|
||||
<span className="ml-1 text-zinc-400">
|
||||
({Math.ceil((new Date(tripRange.to).getTime() - new Date(tripRange.from).getTime()) / 86400000) + 1} days)
|
||||
@@ -1502,7 +1501,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
<div className="flex-1"><DatePicker value={customTo} onChange={setCustomTo} /></div>
|
||||
<button onClick={handleCustomSearch}
|
||||
className="px-3 py-1.5 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[12px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 flex-shrink-0">
|
||||
Search
|
||||
{t('journey.picker.search')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -1522,16 +1521,16 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
{a.albumName || a.name || 'Album'}{a.assetCount != null ? ` (${a.assetCount})` : ''}
|
||||
</button>
|
||||
))}
|
||||
{albums.length === 0 && !loading && <span className="text-[12px] text-zinc-400">No albums found</span>}
|
||||
{albums.length === 0 && !loading && <span className="text-[12px] text-zinc-400">{t('journey.picker.noAlbums')}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add-to */}
|
||||
{/* Add-to entry selector */}
|
||||
<div className="px-6 py-2.5 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<div className="relative flex items-center gap-2">
|
||||
<span className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500">Add to</span>
|
||||
<span className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500">{t('journey.picker.addTo')}</span>
|
||||
<button
|
||||
onClick={() => setAddToOpen(!addToOpen)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-zinc-200 dark:border-zinc-700 text-[12px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800"
|
||||
@@ -1552,10 +1551,12 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
}`}
|
||||
>
|
||||
<Camera size={12} />
|
||||
Gallery
|
||||
{t('journey.picker.newGallery')}
|
||||
</button>
|
||||
<div className="h-px bg-zinc-200 dark:bg-zinc-700 my-1" />
|
||||
{entries.map(e => (
|
||||
{entries.filter(e => e.type !== 'skeleton' && e.title !== 'Gallery' && e.title !== '[Trip Photos]').length > 0 && (
|
||||
<div className="h-px bg-zinc-200 dark:bg-zinc-700 my-1" />
|
||||
)}
|
||||
{entries.filter(e => e.type !== 'skeleton' && e.title !== 'Gallery' && e.title !== '[Trip Photos]').map(e => (
|
||||
<button
|
||||
key={e.id}
|
||||
onClick={() => { setTargetEntryId(e.id); setAddToOpen(false) }}
|
||||
@@ -1565,7 +1566,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
: 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
{e.title || e.location_name || e.entry_date}
|
||||
{e.title || e.location_name || new Date(e.entry_date + 'T12:00:00').toLocaleDateString(undefined, { day: 'numeric', month: 'short' })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1638,19 +1639,20 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<span className="text-[12px] text-zinc-500">
|
||||
<strong className="text-zinc-900 dark:text-white">{selected.size}</strong> selected
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-zinc-200/60 dark:bg-zinc-700/60 text-[11px] leading-none text-zinc-500 dark:text-zinc-400">
|
||||
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[10px] leading-none font-bold">{selected.size}</span>
|
||||
<span className="leading-[18px]">{t('journey.picker.selected')}</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAdd([...selected], targetEntryId)}
|
||||
disabled={selected.size === 0}
|
||||
className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Add {selected.size > 0 ? `(${selected.size})` : ''}
|
||||
{t('common.add')} {selected.size > 0 ? `(${selected.size})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1666,6 +1668,7 @@ function DatePicker({ value, onChange, tripDates }: {
|
||||
onChange: (date: string) => void
|
||||
tripDates?: Set<string>
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [viewMonth, setViewMonth] = useState(() => {
|
||||
const d = value ? new Date(value + 'T00:00:00') : new Date()
|
||||
@@ -1674,7 +1677,7 @@ function DatePicker({ value, onChange, tripDates }: {
|
||||
|
||||
const daysInMonth = new Date(viewMonth.year, viewMonth.month + 1, 0).getDate()
|
||||
const firstDow = new Date(viewMonth.year, viewMonth.month, 1).getDay()
|
||||
const monthName = new Date(viewMonth.year, viewMonth.month).toLocaleDateString('en', { month: 'long', year: 'numeric' })
|
||||
const monthName = new Date(viewMonth.year, viewMonth.month).toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
|
||||
|
||||
const prevMonth = () => {
|
||||
setViewMonth(p => p.month === 0 ? { year: p.year - 1, month: 11 } : { ...p, month: p.month - 1 })
|
||||
@@ -1689,7 +1692,7 @@ function DatePicker({ value, onChange, tripDates }: {
|
||||
for (let i = 0; i < firstDow; i++) cells.push(null)
|
||||
for (let d = 1; d <= daysInMonth; d++) cells.push(d)
|
||||
|
||||
const formatted = value ? new Date(value + 'T00:00:00').toLocaleDateString('en', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Select date'
|
||||
const formatted = value ? new Date(value + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : t('journey.picker.selectDate')
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -1719,8 +1722,8 @@ function DatePicker({ value, onChange, tripDates }: {
|
||||
|
||||
{/* Weekday headers */}
|
||||
<div className="grid grid-cols-7 mb-1">
|
||||
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(d => (
|
||||
<div key={d} className="text-center text-[10px] font-medium text-zinc-400 py-1">{d}</div>
|
||||
{Array.from({ length: 7 }, (_, i) => new Date(2024, 0, i).toLocaleDateString(undefined, { weekday: 'narrow' })).map((d, i) => (
|
||||
<div key={i} className="text-center text-[10px] font-medium text-zinc-400 py-1">{d}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1870,7 +1873,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className="flex-1 border border-dashed border-zinc-200 dark:border-zinc-700 rounded-lg py-4 text-[12px] text-zinc-500 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800 flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<Plus size={13} /> Upload photos
|
||||
<Plus size={13} /> {t('journey.editor.uploadPhotos')}
|
||||
</button>
|
||||
{galleryPhotos.length > 0 && (
|
||||
<button
|
||||
@@ -1881,7 +1884,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
: 'border-dashed border-zinc-200 dark:border-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Image size={13} /> From Gallery
|
||||
<Image size={13} /> {t('journey.editor.fromGallery')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1906,7 +1909,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
}}
|
||||
className="aspect-square rounded-lg overflow-hidden cursor-pointer hover:ring-2 hover:ring-zinc-900 dark:hover:ring-white hover:ring-offset-1 dark:hover:ring-offset-zinc-900 transition-all"
|
||||
>
|
||||
<img src={photoUrl(gp)} alt="" className="w-full h-full object-cover" loading="lazy" onError={e => { if (gp.provider !== 'local') { const img = e.currentTarget; const orig = photoUrl(gp, 'original'); if (!img.src.includes('/original')) img.src = orig } }} />
|
||||
<img src={photoUrl(gp)} alt="" className="w-full h-full object-cover" loading="lazy" onError={e => { const img = e.currentTarget; const orig = photoUrl(gp, 'original'); if (!img.src.includes('/original')) img.src = orig }} />
|
||||
</div>
|
||||
))}
|
||||
{galleryPhotos.filter(gp => !photos.some(p => p.id === gp.id)).length === 0 && (
|
||||
@@ -1920,7 +1923,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{photos.map((p, idx) => (
|
||||
<div key={p.id} className={`w-20 h-20 rounded-lg overflow-hidden relative group ${idx === 0 && photos.length > 1 ? 'ring-2 ring-zinc-900 dark:ring-white ring-offset-1 dark:ring-offset-zinc-900' : ''}`}>
|
||||
<img src={photoUrl(p)} className="w-full h-full object-cover" alt="" onError={e => { if (p.provider !== 'local') { const img = e.currentTarget; const orig = photoUrl(p, 'original'); if (!img.src.includes('/original')) img.src = orig } }} />
|
||||
<img src={photoUrl(p)} className="w-full h-full object-cover" alt="" onError={e => { const img = e.currentTarget; const orig = photoUrl(p, 'original'); if (!img.src.includes('/original')) img.src = orig }} />
|
||||
{idx === 0 && photos.length > 1 && (
|
||||
<span className="absolute bottom-0.5 left-0.5 px-1 py-px rounded text-[8px] font-bold bg-zinc-900/70 text-white">{t('journey.editor.photoFirst')}</span>
|
||||
)}
|
||||
@@ -1938,7 +1941,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
}}
|
||||
className="absolute bottom-0.5 left-0.5 px-1.5 py-0.5 rounded bg-black/60 text-white text-[8px] font-semibold opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
Make 1st
|
||||
{t('journey.editor.makeFirst')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -2017,7 +2020,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
onClick={() => setPros([...pros, ''])}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full border border-dashed border-green-200 dark:border-green-800/40 rounded-[10px] text-[12px] font-medium text-green-700 dark:text-green-400 hover:border-green-300 dark:hover:border-green-700 transition-colors"
|
||||
>
|
||||
<Plus size={13} strokeWidth={2.5} /> Add another
|
||||
<Plus size={13} strokeWidth={2.5} /> {t('journey.editor.addAnother')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2051,7 +2054,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
onClick={() => setCons([...cons, ''])}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full border border-dashed border-red-200 dark:border-red-800/40 rounded-[10px] text-[12px] font-medium text-red-700 dark:text-red-400 hover:border-red-300 dark:hover:border-red-700 transition-colors"
|
||||
>
|
||||
<Plus size={13} strokeWidth={2.5} /> Add another
|
||||
<Plus size={13} strokeWidth={2.5} /> {t('journey.editor.addAnother')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2129,7 +2132,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
)}
|
||||
{locationSearching && (
|
||||
<div className="absolute left-0 right-0 top-full mt-1 z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-3 text-center text-[12px] text-zinc-400">
|
||||
Searching...
|
||||
{t('journey.editor.searching')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2268,7 +2271,7 @@ function AddTripDialog({ journeyId, existingTripIds, onClose, onAdded }: {
|
||||
disabled={adding === t.id}
|
||||
className="px-3 py-1.5 rounded-lg text-[11px] font-semibold bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-200 disabled:opacity-50"
|
||||
>
|
||||
{adding === t.id ? '...' : 'Link'}
|
||||
{adding === t.id ? '...' : t('journey.trips.link')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -2376,19 +2379,19 @@ function ContributorInviteDialog({ journeyId, existingUserIds, onClose, onInvite
|
||||
|
||||
{/* Role selector */}
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Role</label>
|
||||
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.invite.role')}</label>
|
||||
<div className="flex gap-2">
|
||||
{(['viewer', 'editor'] as const).map(r => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => setRole(r)}
|
||||
className={`flex-1 py-2 rounded-lg text-[12px] font-medium border transition-all capitalize ${
|
||||
className={`flex-1 py-2 rounded-lg text-[12px] font-medium border transition-all ${
|
||||
role === r
|
||||
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
|
||||
: 'border-zinc-200 dark:border-zinc-700 text-zinc-500 hover:border-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{r}
|
||||
{t(`journey.invite.${r}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -2397,14 +2400,14 @@ function ContributorInviteDialog({ journeyId, existingUserIds, onClose, onInvite
|
||||
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleInvite}
|
||||
disabled={!selectedUserId || sending}
|
||||
className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{sending ? 'Inviting...' : 'Invite'}
|
||||
{sending ? t('journey.invite.inviting') : t('journey.invite.invite')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2471,7 +2474,7 @@ function JourneyShareSection({ journeyId }: { journeyId: number }) {
|
||||
onClick={createLink}
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<Link size={14} /> Create share link
|
||||
<Link size={14} /> {t('journey.share.createLink')}
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -2483,7 +2486,7 @@ function JourneyShareSection({ journeyId }: { journeyId: number }) {
|
||||
onClick={copyLink}
|
||||
className="flex-shrink-0 px-2.5 py-1 rounded-md bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[11px] font-medium hover:bg-zinc-700 dark:hover:bg-zinc-200"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
{copied ? t('journey.share.copied') : t('journey.share.copy')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ const mockJourneyData = {
|
||||
weather: 'cloudy',
|
||||
pros_cons: null,
|
||||
photos: [
|
||||
{ id: 100, entry_id: 11, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/temple.jpg', caption: 'Temple entrance' },
|
||||
{ id: 100, entry_id: 11, photo_id: 100, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/temple.jpg', caption: 'Temple entrance' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -348,9 +348,9 @@ describe('JourneyPublicPage', () => {
|
||||
entry_time: null, location_name: null, location_lat: null, location_lng: null,
|
||||
mood: null, weather: null, pros_cons: null,
|
||||
photos: [
|
||||
{ id: 200, entry_id: 20, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/a.jpg', caption: 'Photo A' },
|
||||
{ id: 201, entry_id: 20, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/b.jpg', caption: 'Photo B' },
|
||||
{ id: 202, entry_id: 20, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/c.jpg', caption: 'Photo C' },
|
||||
{ id: 200, entry_id: 20, photo_id: 200, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/a.jpg', caption: 'Photo A' },
|
||||
{ id: 201, entry_id: 20, photo_id: 201, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/b.jpg', caption: 'Photo B' },
|
||||
{ id: 202, entry_id: 20, photo_id: 202, provider: 'local', asset_id: null, owner_id: null, file_path: 'journey/c.jpg', caption: 'Photo C' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -26,7 +26,8 @@ interface PublicEntry {
|
||||
interface PublicPhoto {
|
||||
id: number
|
||||
entry_id: number
|
||||
provider: string
|
||||
photo_id: number
|
||||
provider?: string
|
||||
asset_id?: string | null
|
||||
owner_id?: number | null
|
||||
file_path?: string | null
|
||||
@@ -34,8 +35,7 @@ interface PublicPhoto {
|
||||
}
|
||||
|
||||
function photoUrl(p: PublicPhoto, shareToken: string): string {
|
||||
if (p.provider === 'local') return `/api/public/journey/${shareToken}/photo/local/${encodeURIComponent(p.file_path || '')}/0/original`
|
||||
return `/api/public/journey/${shareToken}/photo/${p.provider}/${p.asset_id}/${p.owner_id}/original`
|
||||
return `/api/public/journey/${shareToken}/photos/${p.photo_id}/original`
|
||||
}
|
||||
|
||||
function formatDate(d: string): { weekday: string; month: string; day: number } {
|
||||
|
||||
@@ -42,17 +42,19 @@ export interface JourneyEntry {
|
||||
export interface JourneyPhoto {
|
||||
id: number
|
||||
entry_id: number
|
||||
provider: 'local' | 'immich' | 'synologyphotos'
|
||||
photo_id: number
|
||||
caption?: string | null
|
||||
sort_order: number
|
||||
shared: 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
|
||||
caption?: string | null
|
||||
sort_order: number
|
||||
width?: number | null
|
||||
height?: number | null
|
||||
shared: number
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export interface JourneyTrip {
|
||||
|
||||
Reference in New Issue
Block a user