mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
Fix skeleton entry deletion and add hide suggestions toggle (#619)
- Revert filled skeleton entries back to skeleton on delete instead of permanently removing them - Add per-user hide_skeletons preference on journey_contributors (migration 99) - Add PATCH /journeys/:id/preferences endpoint for toggling skeleton visibility - Add Eye/EyeOff toggle button with custom tooltip in journey detail header - Filter skeleton entries from timeline when hidden - Add i18n keys for all 14 languages
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -1557,6 +1557,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.detail.backToJourney': 'العودة للمجلة',
|
||||
'journey.detail.day': 'اليوم {number}',
|
||||
'journey.detail.places': 'أماكن',
|
||||
'journey.skeletons.show': 'إظهار الاقتراحات',
|
||||
'journey.skeletons.hide': 'إخفاء الاقتراحات',
|
||||
|
||||
// Journey — Invite
|
||||
'journey.invite.role': 'الدور',
|
||||
|
||||
@@ -1895,6 +1895,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1900,6 +1900,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1901,6 +1901,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1906,6 +1906,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1902,6 +1902,8 @@ const es: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -1896,6 +1896,8 @@ const fr: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -1897,6 +1897,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1897,6 +1897,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1896,6 +1896,8 @@ const nl: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -1889,6 +1889,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -1896,6 +1896,8 @@ const ru: Record<string, string> = {
|
||||
'journey.stats.entries': 'Записей',
|
||||
'journey.stats.photos': 'Фото',
|
||||
'journey.stats.places': 'Мест',
|
||||
'journey.skeletons.show': 'Показать предложения',
|
||||
'journey.skeletons.hide': 'Скрыть предложения',
|
||||
'journey.verdict.lovedIt': 'Понравилось',
|
||||
'journey.verdict.couldBeBetter': 'Могло быть лучше',
|
||||
'journey.synced.places': 'мест',
|
||||
|
||||
@@ -1896,6 +1896,8 @@ const zh: Record<string, string> = {
|
||||
'journey.stats.entries': '条目',
|
||||
'journey.stats.photos': '照片',
|
||||
'journey.stats.places': '地点',
|
||||
'journey.skeletons.show': '显示建议',
|
||||
'journey.skeletons.hide': '隐藏建议',
|
||||
'journey.verdict.lovedIt': '非常喜欢',
|
||||
'journey.verdict.couldBeBetter': '有待改进',
|
||||
'journey.synced.places': '个地点',
|
||||
|
||||
@@ -1856,6 +1856,8 @@ const zhTw: Record<string, string> = {
|
||||
'journey.stats.entries': '條目',
|
||||
'journey.stats.photos': '照片',
|
||||
'journey.stats.places': '地點',
|
||||
'journey.skeletons.show': '顯示建議',
|
||||
'journey.skeletons.hide': '隱藏建議',
|
||||
'journey.verdict.lovedIt': '非常喜歡',
|
||||
'journey.verdict.couldBeBetter': '有待改進',
|
||||
'journey.synced.places': '個地點',
|
||||
|
||||
@@ -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() {
|
||||
</button>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button onClick={() => { import('../components/PDF/JourneyBookPDF').then(m => m.downloadJourneyBookPDF(current)) }} className="w-[34px] h-[34px] rounded-lg bg-white/15 backdrop-blur flex items-center justify-center hover:bg-white/25"><Download size={14} /></button>
|
||||
<button onClick={() => setShowSettings(true)} className="w-[34px] h-[34px] rounded-lg bg-white/15 backdrop-blur flex items-center justify-center hover:bg-white/25"><Share2 size={14} /></button>
|
||||
<div className="relative group">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const next = !hideSkeletons
|
||||
setHideSkeletons(next)
|
||||
await journeyApi.updatePreferences(current.id, { hide_skeletons: next })
|
||||
}}
|
||||
className={`w-[34px] h-[34px] rounded-lg backdrop-blur flex items-center justify-center ${hideSkeletons ? 'bg-white/30' : 'bg-white/15 hover:bg-white/25'}`}
|
||||
>
|
||||
{hideSkeletons ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
<span className="absolute top-full mt-2 left-1/2 -translate-x-1/2 px-2 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 text-[11px] font-medium whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity">
|
||||
{hideSkeletons ? t('journey.skeletons.show') : t('journey.skeletons.hide')}
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={() => setShowSettings(true)} className="w-[34px] h-[34px] rounded-lg bg-white/15 backdrop-blur flex items-center justify-center hover:bg-white/25"><MoreHorizontal size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface JourneyDetail extends Journey {
|
||||
trips: JourneyTrip[]
|
||||
contributors: JourneyContributor[]
|
||||
stats: { entries: number; photos: number; cities: number }
|
||||
hide_skeletons?: boolean
|
||||
}
|
||||
|
||||
interface JourneyState {
|
||||
|
||||
Reference in New Issue
Block a user