diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 791c2a5e..6d981112 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -322,6 +322,9 @@ export const journeyApi = { updateContributor: (id: number, userId: number, role: string) => apiClient.patch(`/journeys/${id}/contributors/${userId}`, { role }).then(r => r.data), removeContributor: (id: number, userId: number) => apiClient.delete(`/journeys/${id}/contributors/${userId}`).then(r => r.data), + // Preferences + updatePreferences: (id: number, data: { hide_skeletons?: boolean }) => apiClient.patch(`/journeys/${id}/preferences`, data).then(r => r.data), + // Share getShareLink: (id: number) => apiClient.get(`/journeys/${id}/share-link`).then(r => r.data), createShareLink: (id: number, perms: { share_timeline?: boolean; share_gallery?: boolean; share_map?: boolean }) => apiClient.post(`/journeys/${id}/share-link`, perms).then(r => r.data), diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index 51054bef..206f063d 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -190,11 +190,12 @@ describe('AddonManager', () => { expect(screen.queryByText('Bag Tracking')).not.toBeInTheDocument(); }); - it('FE-ADMIN-ADDON-010: photo provider sub-toggles shown for Memories addon', async () => { + it('FE-ADMIN-ADDON-010: photo provider sub-toggles shown under Journey addon', async () => { server.use( http.get('/api/admin/addons', () => HttpResponse.json({ addons: [ + buildAddon({ id: 'journey', name: 'Journey', type: 'global', icon: 'Compass', enabled: true }), buildAddon({ id: 'photos', name: 'Memories', type: 'trip', icon: 'Image', enabled: false }), buildAddon({ id: 'unsplash', name: 'Unsplash', type: 'photo_provider', enabled: true }), buildAddon({ id: 'pexels', name: 'Pexels', type: 'photo_provider', enabled: false }), @@ -204,18 +205,16 @@ describe('AddonManager', () => { ); render(); - // Provider sub-rows are visible + // Provider sub-rows are visible under Journey addon await screen.findByText('Unsplash'); expect(screen.getByText('Pexels')).toBeInTheDocument(); - // Memories row shows name override - expect(screen.getByText('Memories providers')).toBeInTheDocument(); + // Journey addon is rendered + expect(screen.getByText('Journey')).toBeInTheDocument(); - // The photos addon row itself has no top-level toggle (hideToggle = true) - // The toggle buttons are only for the providers + // Toggle buttons: journey toggle + 2 provider toggles const toggleBtns = screen.getAllByRole('button').filter(b => b.classList.contains('rounded-full')); - // Should be 2 provider toggles (no main toggle for the photos addon) - expect(toggleBtns.length).toBe(2); + expect(toggleBtns.length).toBe(3); }); it('FE-ADMIN-ADDON-011: icon falls back to Puzzle when icon name unknown', async () => { diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index 5d9f7887..8a564381 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -4,10 +4,10 @@ import { useTranslation } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import { useAddonStore } from '../../store/addonStore' import { useToast } from '../shared/Toast' -import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass } from 'lucide-react' +import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen } from 'lucide-react' const ICON_MAP = { - ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, + ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, } interface Addon { @@ -103,11 +103,11 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking } } } - const tripAddons = addons.filter(a => a.type === 'trip') - const globalAddons = addons.filter(a => a.type === 'global') const photoProviderAddons = addons.filter(isPhotoProviderAddon) + const photosAddon = addons.filter(a => a.type === 'trip').find(isPhotosAddon) + const tripAddons = addons.filter(a => a.type === 'trip' && !isPhotosAddon(a)) + const globalAddons = addons.filter(a => a.type === 'global') const integrationAddons = addons.filter(a => a.type === 'integration') - const photosAddon = tripAddons.find(isPhotosAddon) const providerOptions: ProviderOption[] = photoProviderAddons.map((provider) => ({ key: provider.id, label: provider.name, @@ -153,42 +153,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking } {tripAddons.map(addon => (
- - {photosAddon && addon.id === photosAddon.id && providerOptions.length > 0 && ( -
-
- {providerOptions.map(provider => ( -
-
-
{provider.label}
-
{provider.description}
-
-
- - {provider.enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} - - -
-
- ))} -
-
- )} + {addon.id === 'packing' && addon.enabled && onToggleBagTracking && (
@@ -223,7 +188,37 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
{globalAddons.map(addon => ( - +
+ + {/* Memories providers as sub-items under Journey addon */} + {addon.id === 'journey' && providerOptions.length > 0 && ( +
+
+ {providerOptions.map(provider => ( +
+
+
{provider.label}
+
{provider.description}
+
+
+ + {provider.enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} + + +
+
+ ))} +
+
+ )} +
))}
)} diff --git a/client/src/components/Journey/JourneyMap.tsx b/client/src/components/Journey/JourneyMap.tsx index 082c4a4f..88b08d0b 100644 --- a/client/src/components/Journey/JourneyMap.tsx +++ b/client/src/components/Journey/JourneyMap.tsx @@ -168,7 +168,8 @@ const JourneyMap = forwardRef(function JourneyMap( L.tileLayer(mapTileUrl || defaultTile, { maxZoom: 18, attribution: '© OpenStreetMap', - }).addTo(map) + referrerPolicy: 'strict-origin-when-cross-origin', + } as any).addTo(map) const items = buildMarkerItems(entries) itemsRef.current = items diff --git a/client/src/components/Journey/MarkdownToolbar.tsx b/client/src/components/Journey/MarkdownToolbar.tsx index 6a82cadb..4ad519eb 100644 --- a/client/src/components/Journey/MarkdownToolbar.tsx +++ b/client/src/components/Journey/MarkdownToolbar.tsx @@ -6,7 +6,7 @@ interface Props { dark?: boolean } -type FormatAction = { type: 'wrap'; before: string; after: string } | { type: 'line'; prefix: string } +type FormatAction = { type: 'wrap'; before: string; after: string } | { type: 'line'; prefix: string } | { type: 'insert'; text: string } const ACTIONS: Array<{ icon: typeof Bold; label: string; action: FormatAction }> = [ { icon: Bold, label: 'Bold', action: { type: 'wrap', before: '**', after: '**' } }, @@ -16,7 +16,7 @@ const ACTIONS: Array<{ icon: typeof Bold; label: string; action: FormatAction }> { icon: Link, label: 'Link', action: { type: 'wrap', before: '[', after: '](url)' } }, { icon: List, label: 'List', action: { type: 'line', prefix: '- ' } }, { icon: ListOrdered, label: 'Ordered', action: { type: 'line', prefix: '1. ' } }, - { icon: Minus, label: 'Divider', action: { type: 'line', prefix: '\n---\n' } }, + { icon: Minus, label: 'Divider', action: { type: 'insert', text: '\n\n---\n\n' } }, ] export default function MarkdownToolbar({ textareaRef, onUpdate, dark }: Props) { @@ -35,6 +35,9 @@ export default function MarkdownToolbar({ textareaRef, onUpdate, dark }: Props) if (action.type === 'wrap') { result = text.slice(0, start) + action.before + selected + action.after + text.slice(end) cursorPos = selected ? end + action.before.length + action.after.length : start + action.before.length + } else if (action.type === 'insert') { + result = text.slice(0, start) + action.text + text.slice(end) + cursorPos = start + action.text.length } else { // line prefix — find start of current line const lineStart = text.lastIndexOf('\n', start - 1) + 1 diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index ac834d22..3a37d8f5 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -5,6 +5,8 @@ const ar: Record = { // Common 'common.save': 'حفظ', + 'common.showMore': 'عرض المزيد', + 'common.showLess': 'عرض أقل', 'common.cancel': 'إلغاء', 'common.delete': 'حذف', 'common.edit': 'تعديل', @@ -1557,6 +1559,8 @@ const ar: Record = { 'journey.detail.backToJourney': 'العودة للمجلة', 'journey.detail.day': 'اليوم {number}', 'journey.detail.places': 'أماكن', + 'journey.skeletons.show': 'إظهار الاقتراحات', + 'journey.skeletons.hide': 'إخفاء الاقتراحات', // Journey — Invite 'journey.invite.role': 'الدور', @@ -1567,6 +1571,7 @@ const ar: Record = { // Journey Entry Editor 'journey.editor.uploadPhotos': 'رفع صور', + 'journey.editor.uploading': '...جارٍ الرفع', 'journey.editor.fromGallery': 'من المعرض', 'journey.editor.addAnother': 'إضافة آخر', 'journey.editor.makeFirst': 'جعله الأول', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 512095f1..ef89acd7 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -1,6 +1,8 @@ const br: Record = { // Common 'common.save': 'Salvar', + 'common.showMore': 'Mostrar mais', + 'common.showLess': 'Mostrar menos', 'common.cancel': 'Cancelar', 'common.delete': 'Excluir', 'common.edit': 'Editar', @@ -1895,11 +1897,14 @@ const br: Record = { 'journey.stats.entries': 'Entradas', 'journey.stats.photos': 'Fotos', 'journey.stats.places': 'Lugares', + 'journey.skeletons.show': 'Mostrar sugestões', + 'journey.skeletons.hide': 'Ocultar sugestões', 'journey.verdict.lovedIt': 'Adorei', 'journey.verdict.couldBeBetter': 'Poderia ser melhor', 'journey.synced.places': 'lugares', 'journey.synced.synced': 'sincronizado', 'journey.editor.uploadPhotos': 'Enviar fotos', + 'journey.editor.uploading': 'Enviando...', 'journey.editor.fromGallery': 'Da galeria', 'journey.editor.allPhotosAdded': 'Todas as fotos já foram adicionadas', 'journey.editor.writeStory': 'Escreva sua história...', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index f5959522..3a0cf375 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -1,6 +1,8 @@ const cs: Record = { // Společné (Common) 'common.save': 'Uložit', + 'common.showMore': 'Zobrazit více', + 'common.showLess': 'Zobrazit méně', 'common.cancel': 'Zrušit', 'common.delete': 'Smazat', 'common.edit': 'Upravit', @@ -1900,11 +1902,14 @@ const cs: Record = { 'journey.stats.entries': 'Záznamy', 'journey.stats.photos': 'Fotky', 'journey.stats.places': 'Místa', + 'journey.skeletons.show': 'Zobrazit návrhy', + 'journey.skeletons.hide': 'Skrýt návrhy', 'journey.verdict.lovedIt': 'Skvělé', 'journey.verdict.couldBeBetter': 'Mohlo by být lepší', 'journey.synced.places': 'místa', 'journey.synced.synced': 'synchronizováno', 'journey.editor.uploadPhotos': 'Nahrát fotky', + 'journey.editor.uploading': 'Nahrávání...', 'journey.editor.fromGallery': 'Z galerie', 'journey.editor.allPhotosAdded': 'Všechny fotky již přidány', 'journey.editor.writeStory': 'Napište svůj příběh...', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 3e4f2f11..107b9903 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -1,6 +1,8 @@ const de: Record = { // Allgemein 'common.save': 'Speichern', + 'common.showMore': 'Mehr anzeigen', + 'common.showLess': 'Weniger anzeigen', 'common.cancel': 'Abbrechen', 'common.delete': 'Löschen', 'common.edit': 'Bearbeiten', @@ -1901,11 +1903,14 @@ const de: Record = { 'journey.stats.entries': 'Einträge', 'journey.stats.photos': 'Fotos', 'journey.stats.places': 'Orte', + 'journey.skeletons.show': 'Vorschläge anzeigen', + 'journey.skeletons.hide': 'Vorschläge ausblenden', 'journey.verdict.lovedIt': 'Toll', 'journey.verdict.couldBeBetter': 'Verbesserungswürdig', 'journey.synced.places': 'Orte', 'journey.synced.synced': 'synchronisiert', 'journey.editor.uploadPhotos': 'Fotos hochladen', + 'journey.editor.uploading': 'Hochladen...', 'journey.editor.fromGallery': 'Aus Galerie', 'journey.editor.allPhotosAdded': 'Alle Fotos bereits hinzugefügt', 'journey.editor.writeStory': 'Erzähle deine Geschichte...', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 91117374..8acb2238 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -1,6 +1,8 @@ const en: Record = { // Common 'common.save': 'Save', + 'common.showMore': 'Show more', + 'common.showLess': 'Show less', 'common.cancel': 'Cancel', 'common.delete': 'Delete', 'common.edit': 'Edit', @@ -1906,6 +1908,8 @@ const en: Record = { 'journey.stats.entries': 'Entries', 'journey.stats.photos': 'Photos', 'journey.stats.places': 'Places', + 'journey.skeletons.show': 'Show suggestions', + 'journey.skeletons.hide': 'Hide suggestions', // Journey Detail — Verdict 'journey.verdict.lovedIt': 'Loved it', @@ -1917,6 +1921,7 @@ const en: Record = { // Journey Entry Editor 'journey.editor.uploadPhotos': 'Upload photos', + 'journey.editor.uploading': 'Uploading...', 'journey.editor.fromGallery': 'From Gallery', 'journey.editor.allPhotosAdded': 'All photos already added', 'journey.editor.writeStory': 'Write your story...', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index c37097e9..518bac4e 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1,6 +1,8 @@ const es: Record = { // Common 'common.save': 'Guardar', + 'common.showMore': 'Ver más', + 'common.showLess': 'Ver menos', 'common.cancel': 'Cancelar', 'common.delete': 'Eliminar', 'common.edit': 'Editar', @@ -1902,11 +1904,14 @@ const es: Record = { 'journey.stats.entries': 'Entradas', 'journey.stats.photos': 'Fotos', 'journey.stats.places': 'Lugares', + 'journey.skeletons.show': 'Mostrar sugerencias', + 'journey.skeletons.hide': 'Ocultar sugerencias', 'journey.verdict.lovedIt': 'Me encantó', 'journey.verdict.couldBeBetter': 'Podría mejorar', 'journey.synced.places': 'lugares', 'journey.synced.synced': 'sincronizado', 'journey.editor.uploadPhotos': 'Subir fotos', + 'journey.editor.uploading': 'Subiendo...', 'journey.editor.fromGallery': 'Desde galería', 'journey.editor.allPhotosAdded': 'Todas las fotos ya fueron añadidas', 'journey.editor.writeStory': 'Escribe tu historia...', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index c00172c5..b5aaa41a 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1,6 +1,8 @@ const fr: Record = { // Common 'common.save': 'Enregistrer', + 'common.showMore': 'Voir plus', + 'common.showLess': 'Voir moins', 'common.cancel': 'Annuler', 'common.delete': 'Supprimer', 'common.edit': 'Modifier', @@ -1896,11 +1898,14 @@ const fr: Record = { 'journey.stats.entries': 'Entrées', 'journey.stats.photos': 'Photos', 'journey.stats.places': 'Lieux', + 'journey.skeletons.show': 'Afficher les suggestions', + 'journey.skeletons.hide': 'Masquer les suggestions', 'journey.verdict.lovedIt': 'Adoré', 'journey.verdict.couldBeBetter': 'Pourrait être mieux', 'journey.synced.places': 'lieux', 'journey.synced.synced': 'synchronisé', 'journey.editor.uploadPhotos': 'Téléverser des photos', + 'journey.editor.uploading': 'Envoi...', 'journey.editor.fromGallery': 'Depuis la galerie', 'journey.editor.allPhotosAdded': 'Toutes les photos ont déjà été ajoutées', 'journey.editor.writeStory': 'Écrivez votre histoire...', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 18117520..816fe69d 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -1,6 +1,8 @@ const hu: Record = { // Általános 'common.save': 'Mentés', + 'common.showMore': 'Továbbiak', + 'common.showLess': 'Kevesebb', 'common.cancel': 'Mégse', 'common.delete': 'Törlés', 'common.edit': 'Szerkesztés', @@ -1897,11 +1899,14 @@ const hu: Record = { 'journey.stats.entries': 'Bejegyzések', 'journey.stats.photos': 'Fotók', 'journey.stats.places': 'Helyszínek', + 'journey.skeletons.show': 'Javaslatok megjelenítése', + 'journey.skeletons.hide': 'Javaslatok elrejtése', 'journey.verdict.lovedIt': 'Imádtam', 'journey.verdict.couldBeBetter': 'Lehetne jobb', 'journey.synced.places': 'helyszín', 'journey.synced.synced': 'szinkronizálva', 'journey.editor.uploadPhotos': 'Fotók feltöltése', + 'journey.editor.uploading': 'Feltöltés...', 'journey.editor.fromGallery': 'Galériából', 'journey.editor.allPhotosAdded': 'Minden fotó már hozzáadva', 'journey.editor.writeStory': 'Írd meg a történeted...', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 7637ecb5..8c7d986e 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1,6 +1,8 @@ const it: Record = { // Common 'common.save': 'Salva', + 'common.showMore': 'Mostra di più', + 'common.showLess': 'Mostra meno', 'common.cancel': 'Annulla', 'common.delete': 'Elimina', 'common.edit': 'Modifica', @@ -1897,11 +1899,14 @@ const it: Record = { 'journey.stats.entries': 'Voci', 'journey.stats.photos': 'Foto', 'journey.stats.places': 'Luoghi', + 'journey.skeletons.show': 'Mostra suggerimenti', + 'journey.skeletons.hide': 'Nascondi suggerimenti', 'journey.verdict.lovedIt': 'Adorato', 'journey.verdict.couldBeBetter': 'Potrebbe essere meglio', 'journey.synced.places': 'luoghi', 'journey.synced.synced': 'sincronizzato', 'journey.editor.uploadPhotos': 'Carica foto', + 'journey.editor.uploading': 'Caricamento...', 'journey.editor.fromGallery': 'Dalla galleria', 'journey.editor.allPhotosAdded': 'Tutte le foto sono già state aggiunte', 'journey.editor.writeStory': 'Scrivi la tua storia...', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 37d1ea2a..ab7790bc 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1,6 +1,8 @@ const nl: Record = { // Common 'common.save': 'Opslaan', + 'common.showMore': 'Meer tonen', + 'common.showLess': 'Minder tonen', 'common.cancel': 'Annuleren', 'common.delete': 'Verwijderen', 'common.edit': 'Bewerken', @@ -1896,11 +1898,14 @@ const nl: Record = { 'journey.stats.entries': 'Vermeldingen', 'journey.stats.photos': 'Foto\'s', 'journey.stats.places': 'Plaatsen', + 'journey.skeletons.show': 'Suggesties tonen', + 'journey.skeletons.hide': 'Suggesties verbergen', 'journey.verdict.lovedIt': 'Geweldig', 'journey.verdict.couldBeBetter': 'Kan beter', 'journey.synced.places': 'plaatsen', 'journey.synced.synced': 'gesynchroniseerd', 'journey.editor.uploadPhotos': 'Foto\'s uploaden', + 'journey.editor.uploading': 'Uploaden...', 'journey.editor.fromGallery': 'Uit galerij', 'journey.editor.allPhotosAdded': 'Alle foto\'s al toegevoegd', 'journey.editor.writeStory': 'Schrijf je verhaal...', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index e7bc8507..a3a2953b 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -1,6 +1,8 @@ const pl: Record = { // Common 'common.save': 'Zapisz', + 'common.showMore': 'Pokaż więcej', + 'common.showLess': 'Pokaż mniej', 'common.cancel': 'Anuluj', 'common.delete': 'Usuń', 'common.edit': 'Edytuj', @@ -1889,11 +1891,14 @@ const pl: Record = { 'journey.stats.entries': 'Wpisy', 'journey.stats.photos': 'Zdjęcia', 'journey.stats.places': 'Miejsca', + 'journey.skeletons.show': 'Pokaż sugestie', + 'journey.skeletons.hide': 'Ukryj sugestie', 'journey.verdict.lovedIt': 'Świetne', 'journey.verdict.couldBeBetter': 'Mogłoby być lepiej', 'journey.synced.places': 'miejsca', 'journey.synced.synced': 'zsynchronizowane', 'journey.editor.uploadPhotos': 'Prześlij zdjęcia', + 'journey.editor.uploading': 'Przesyłanie...', 'journey.editor.fromGallery': 'Z galerii', 'journey.editor.allPhotosAdded': 'Wszystkie zdjęcia już dodane', 'journey.editor.writeStory': 'Napisz swoją historię...', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 23b02a94..6842f015 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -1,6 +1,8 @@ const ru: Record = { // Common 'common.save': 'Сохранить', + 'common.showMore': 'Показать больше', + 'common.showLess': 'Показать меньше', 'common.cancel': 'Отмена', 'common.delete': 'Удалить', 'common.edit': 'Редактировать', @@ -1896,11 +1898,14 @@ const ru: Record = { 'journey.stats.entries': 'Записей', 'journey.stats.photos': 'Фото', 'journey.stats.places': 'Мест', + 'journey.skeletons.show': 'Показать предложения', + 'journey.skeletons.hide': 'Скрыть предложения', 'journey.verdict.lovedIt': 'Понравилось', 'journey.verdict.couldBeBetter': 'Могло быть лучше', 'journey.synced.places': 'мест', 'journey.synced.synced': 'синхронизировано', 'journey.editor.uploadPhotos': 'Загрузить фото', + 'journey.editor.uploading': 'Загрузка...', 'journey.editor.fromGallery': 'Из галереи', 'journey.editor.allPhotosAdded': 'Все фото уже добавлены', 'journey.editor.writeStory': 'Напишите свою историю...', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 53b1e24d..e78e4b94 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -1,6 +1,8 @@ const zh: Record = { // Common 'common.save': '保存', + 'common.showMore': '显示更多', + 'common.showLess': '收起', 'common.cancel': '取消', 'common.delete': '删除', 'common.edit': '编辑', @@ -1896,11 +1898,14 @@ const zh: Record = { 'journey.stats.entries': '条目', 'journey.stats.photos': '照片', 'journey.stats.places': '地点', + 'journey.skeletons.show': '显示建议', + 'journey.skeletons.hide': '隐藏建议', 'journey.verdict.lovedIt': '非常喜欢', 'journey.verdict.couldBeBetter': '有待改进', 'journey.synced.places': '个地点', 'journey.synced.synced': '已同步', 'journey.editor.uploadPhotos': '上传照片', + 'journey.editor.uploading': '上传中...', 'journey.editor.fromGallery': '从相册', 'journey.editor.allPhotosAdded': '所有照片已添加', 'journey.editor.writeStory': '写下你的故事...', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 334fca61..e7393705 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -1,6 +1,8 @@ const zhTw: Record = { // Common 'common.save': '儲存', + 'common.showMore': '顯示更多', + 'common.showLess': '收起', 'common.cancel': '取消', 'common.delete': '刪除', 'common.edit': '編輯', @@ -1856,11 +1858,14 @@ const zhTw: Record = { 'journey.stats.entries': '條目', 'journey.stats.photos': '照片', 'journey.stats.places': '地點', + 'journey.skeletons.show': '顯示建議', + 'journey.skeletons.hide': '隱藏建議', 'journey.verdict.lovedIt': '非常喜歡', 'journey.verdict.couldBeBetter': '有待改進', 'journey.synced.places': '個地點', 'journey.synced.synced': '已同步', 'journey.editor.uploadPhotos': '上傳照片', + 'journey.editor.uploading': '上傳中...', 'journey.editor.fromGallery': '從相簿', 'journey.editor.allPhotosAdded': '所有照片已新增', 'journey.editor.writeStory': '寫下你的故事...', diff --git a/client/src/pages/AtlasPage.tsx b/client/src/pages/AtlasPage.tsx index 0195da71..4dc4047e 100644 --- a/client/src/pages/AtlasPage.tsx +++ b/client/src/pages/AtlasPage.tsx @@ -296,8 +296,9 @@ export default function AtlasPage(): React.ReactElement { updateWhenIdle: false, tileSize: 256, zoomOffset: 0, - crossOrigin: true - }).addTo(map) + crossOrigin: true, + referrerPolicy: 'strict-origin-when-cross-origin', + } as any).addTo(map) // Preload adjacent zoom level tiles L.tileLayer(tileUrl, { @@ -306,6 +307,7 @@ export default function AtlasPage(): React.ReactElement { opacity: 0, tileSize: 256, crossOrigin: true, + referrerPolicy: 'strict-origin-when-cross-origin', }).addTo(map) // Custom pane for region layer — above overlay (z-index 400) diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index 989ecfd4..709fd6a4 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -18,7 +18,7 @@ import { Clock, Package, Image, ChevronRight, UserPlus, Plus, Minus, Calendar, Camera, BookOpen, X, Check, ImagePlus, Trash2, Pencil, Laugh, Smile, Meh, Annoyed, Frown, - Sun, CloudSun, Cloud, CloudRain, CloudLightning, Snowflake, ChevronDown, + Sun, CloudSun, Cloud, CloudRain, CloudLightning, Snowflake, ChevronDown, Eye, EyeOff, } from 'lucide-react' import type { JourneyEntry, JourneyPhoto, JourneyDetail } from '../store/journeyStore' @@ -92,11 +92,16 @@ export default function JourneyDetailPage() { const [showAddTrip, setShowAddTrip] = useState(false) const [unlinkTrip, setUnlinkTrip] = useState<{ trip_id: number; title: string } | null>(null) const [showSettings, setShowSettings] = useState(false) + const [hideSkeletons, setHideSkeletons] = useState(false) useEffect(() => { if (id) loadJourney(Number(id)).catch(() => {}) }, [id]) + useEffect(() => { + if (current?.hide_skeletons !== undefined) setHideSkeletons(current.hide_skeletons) + }, [current?.hide_skeletons]) + useEffect(() => { if (notFound) { toast.error(t('journey.notFound')) @@ -193,7 +198,7 @@ export default function JourneyDetailPage() { ) } - const timelineEntries = current.entries.filter(e => e.title !== 'Gallery' && e.title !== '[Trip Photos]') + const timelineEntries = current.entries.filter(e => e.title !== 'Gallery' && e.title !== '[Trip Photos]' && (!hideSkeletons || e.type !== 'skeleton')) const dayGroups = groupByDate(timelineEntries) const sortedDates = [...dayGroups.keys()].sort() @@ -243,7 +248,21 @@ export default function JourneyDetailPage() {
- +
+ + + {hideSkeletons ? t('journey.skeletons.show') : t('journey.skeletons.hide')} + +
@@ -753,6 +772,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres const [showPicker, setShowPicker] = useState(false) const [pickerProvider, setPickerProvider] = useState(null) const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([]) + const [galleryUploading, setGalleryUploading] = useState(false) const toast = useToast() // check which providers are enabled AND connected for the current user @@ -797,27 +817,28 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres const handleGalleryUpload = async (e: React.ChangeEvent) => { const files = e.target.files if (!files?.length) return - // find existing "Gallery" entry or create one - let galleryEntry = entries.find(e => e.title === 'Gallery' && e.type === 'entry') - let entryId = galleryEntry?.id - if (!entryId) { - try { + setGalleryUploading(true) + try { + // find existing "Gallery" entry or create one + let galleryEntry = entries.find(e => e.title === 'Gallery' && e.type === 'entry') + let entryId = galleryEntry?.id + if (!entryId) { const entry = await journeyApi.createEntry(journeyId, { title: t('journey.share.gallery'), entry_date: new Date().toISOString().split('T')[0], type: 'entry', }) entryId = entry.id - } catch { return } - } - const formData = new FormData() - for (const f of files) formData.append('photos', f) - try { + } + const formData = new FormData() + for (const f of files) formData.append('photos', f) await journeyApi.uploadPhotos(entryId, formData) toast.success(t('journey.photosUploaded', { count: files.length })) onRefresh() } catch { toast.error(t('journey.settings.coverFailed')) + } finally { + setGalleryUploading(false) } e.target.value = '' } @@ -855,10 +876,14 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
{availableProviders.map(p => ( - {photo.provider !== 'local' && ( + {photo.provider && photo.provider !== 'local' && (
- {photo.provider === 'immich' ? 'Immich' : 'Synology'} + {photo.provider === 'immich' ? 'Immich' : photo.provider === 'synology' ? 'Synology' : photo.provider}
)} @@ -967,6 +992,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres // ── Expandable Story ───────────────────────────────────────────────────── function ExpandableStory({ story }: { story: string }) { + const { t } = useTranslation() const [expanded, setExpanded] = useState(false) const [clamped, setClamped] = useState(false) const ref = useRef(null) @@ -992,16 +1018,24 @@ function ExpandableStory({ story }: { story: string }) { onClick={() => { if (clamped || expanded) setExpanded(e => !e) }} className={`text-[13px] text-zinc-700 dark:text-zinc-300 leading-relaxed ${ expanded ? '' : 'line-clamp-3 md:line-clamp-[9]' - } ${clamped || expanded ? 'cursor-pointer md:cursor-auto' : ''}`} + } ${clamped || expanded ? 'cursor-pointer' : ''}`} >
{clamped && !expanded && ( + )} + {expanded && ( + )} @@ -1452,8 +1486,12 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on const assets = data.assets || [] setPhotos(prev => append ? [...prev, ...assets] : assets) setHasMore(!!data.hasMore) + } else { + setHasMore(false) } - } catch (e: any) { if (e.name !== 'AbortError') {} } + } catch (e: any) { + if (e.name !== 'AbortError') setHasMore(false) + } if (!signal.aborted) { setLoading(false); setLoadingMore(false) } } @@ -1466,6 +1504,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on const signal = cancelPending() setLoading(true) setPhotos([]) + setHasMore(false) try { const res = await fetch(`/api/integrations/memories/${provider}/albums/${albumId}/photos`, { credentials: 'include', signal }) if (res.ok) setPhotos((await res.json()).assets || []) @@ -1739,7 +1778,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on ) })} {/* Infinite scroll trigger */} - {hasMore && } + {hasMore && !selectedAlbum && } )} @@ -1899,6 +1938,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa const [pros, setPros] = useState(entry.pros_cons?.pros?.length ? entry.pros_cons.pros : ['']) const [cons, setCons] = useState(entry.pros_cons?.cons?.length ? entry.pros_cons.cons : ['']) const [saving, setSaving] = useState(false) + const [uploading, setUploading] = useState(false) const [photos, setPhotos] = useState(entry.photos || []) const [pendingFiles, setPendingFiles] = useState([]) const [pendingLinkIds, setPendingLinkIds] = useState([]) @@ -1947,10 +1987,15 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa // queue files for upload after save setPendingFiles(prev => [...prev, ...Array.from(files)]) } else { - const formData = new FormData() - for (const f of files) formData.append('photos', f) - const newPhotos = await onUploadPhotos(entry.id, formData) - if (newPhotos?.length) setPhotos(prev => [...prev, ...newPhotos]) + setUploading(true) + try { + const formData = new FormData() + for (const f of files) formData.append('photos', f) + const newPhotos = await onUploadPhotos(entry.id, formData) + if (newPhotos?.length) setPhotos(prev => [...prev, ...newPhotos]) + } finally { + setUploading(false) + } } } @@ -1978,9 +2023,14 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
{galleryPhotos.length > 0 && (
-
+
{/* Cover Image */}
@@ -2801,7 +2851,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
{/* Footer */} -
+