diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index c2db2218..e9d4b48d 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -158,16 +158,16 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, return (
{/* Header */} -
-
-

{t('admin.addons.title')}

-

+

+
+

{t('admin.addons.title')}

+

{t('admin.addons.subtitleBefore')}TREK{t('admin.addons.subtitleAfter')}

{addons.length === 0 ? ( -
+
{t('admin.addons.noAddons')}
) : ( @@ -175,9 +175,9 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, {/* Trip Addons */} {tripAddons.length > 0 && (
-
- - +
+ + {t('admin.addons.type.trip')} — {t('admin.addons.tripHint')}
@@ -185,11 +185,11 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
{addon.id === 'packing' && addon.enabled && onToggleBagTracking && ( -
+
-
{t('admin.bagTracking.title')}
-
{t('admin.bagTracking.subtitle')}
+
{t('admin.bagTracking.title')}
+
{t('admin.bagTracking.subtitle')}
@@ -205,7 +205,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
)} {addon.id === 'collab' && addon.enabled && collabFeatures && onToggleCollabFeature && ( -
+
{COLLAB_SUB_FEATURES.map(feat => { const enabled = collabFeatures[feat.key] @@ -214,8 +214,8 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
-
{t(feat.titleKey)}
-
{t(feat.subtitleKey)}
+
{t(feat.titleKey)}
+
{t(feat.subtitleKey)}
@@ -242,9 +242,9 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, {/* Global Addons */} {globalAddons.length > 0 && (
-
- - +
+ + {t('admin.addons.type.global')} — {t('admin.addons.globalHint')}
@@ -253,7 +253,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, {/* Memories providers as sub-items under Journey addon */} {addon.id === 'journey' && providerOptions.length > 0 && ( -
+
{providerOptions.map(provider => { const ProviderIcon = PROVIDER_ICONS[provider.key] @@ -261,8 +261,8 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
{ProviderIcon && }
-
{provider.label}
-
{provider.description}
+
{provider.label}
+
{provider.description}
@@ -291,9 +291,9 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, {/* Integration Addons */} {integrationAddons.length > 0 && (
-
- - +
+ + {t('admin.addons.type.integration')} — {t('admin.addons.integrationHint')}
@@ -336,26 +336,26 @@ function AddonRow({ addon, onToggle, t, nameOverride, descriptionOverride, statu const displayDescription = descriptionOverride || label.description const enabledState = statusOverride ?? addon.enabled return ( -
+
{/* Icon */} -
+
{/* Info */}
- {displayName} + {displayName} {isComingSoon && ( - + Coming Soon )} - + {addon.type === 'global' ? t('admin.addons.type.global') : addon.type === 'integration' ? t('admin.addons.type.integration') : t('admin.addons.type.trip')}
-

{displayDescription}

+

{displayDescription}

{/* Toggle */} @@ -371,9 +371,8 @@ function AddonRow({ addon, onToggle, t, nameOverride, descriptionOverride, statu style={{ background: (enabledState && !isComingSoon) ? 'var(--text-primary)' : 'var(--border-primary)', cursor: isComingSoon ? 'not-allowed' : 'pointer' }} > diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index 7cc3f421..325cb5ee 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -67,7 +67,7 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b elements.push(
    {listItems.map((item, i) => ( -
  • +
  • @@ -96,14 +96,14 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b if (trimmed.startsWith('### ')) { flushList() elements.push( -

    +

    {trimmed.slice(4)}

    ) } else if (trimmed.startsWith('## ')) { flushList() elements.push( -

    +

    {trimmed.slice(3)}

    ) @@ -112,7 +112,7 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b } else { flushList() elements.push( -

    ) @@ -130,8 +130,8 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b href="https://ko-fi.com/mauriceboe" target="_blank" rel="noopener noreferrer" - className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]" - style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)', textDecoration: 'none' }} + className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" + style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ff5e5b'; e.currentTarget.style.boxShadow = '0 0 0 1px #ff5e5b22' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} > @@ -139,17 +139,17 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b

-
Ko-fi
-
{t('admin.github.support')}
+
Ko-fi
+
{t('admin.github.support')}
- + { e.currentTarget.style.borderColor = '#ffdd00'; e.currentTarget.style.boxShadow = '0 0 0 1px #ffdd0022' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} > @@ -157,17 +157,17 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
-
Buy Me a Coffee
-
{t('admin.github.support')}
+
Buy Me a Coffee
+
{t('admin.github.support')}
- +
{ e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} > @@ -175,10 +175,10 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
-
Discord
-
Join the community
+
Discord
+
Join the community
- +
@@ -187,8 +187,8 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b href="https://github.com/mauriceboe/TREK/issues/new?template=bug_report.yml" target="_blank" rel="noopener noreferrer" - className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]" - style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)', textDecoration: 'none' }} + className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" + style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.boxShadow = '0 0 0 1px #ef444422' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} > @@ -196,17 +196,17 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
-
{t('settings.about.reportBug')}
-
{t('settings.about.reportBugHint')}
+
{t('settings.about.reportBug')}
+
{t('settings.about.reportBugHint')}
- + { e.currentTarget.style.borderColor = '#f59e0b'; e.currentTarget.style.boxShadow = '0 0 0 1px #f59e0b22' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} > @@ -214,17 +214,17 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
-
{t('settings.about.featureRequest')}
-
{t('settings.about.featureRequestHint')}
+
{t('settings.about.featureRequest')}
+
{t('settings.about.featureRequestHint')}
- +
{ e.currentTarget.style.borderColor = '#6366f1'; e.currentTarget.style.boxShadow = '0 0 0 1px #6366f122' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} > @@ -232,40 +232,39 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
-
Wiki
-
{t('settings.about.wikiHint')}
+
Wiki
+
{t('settings.about.wikiHint')}
- +
{/* Loading / Error / Releases */} {loading ? ( -
+
- +
) : error ? ( -
+
-

{t('admin.github.error')}

-

{error}

+

{t('admin.github.error')}

+

{error}

) : ( -
-
+
+
-

{t('admin.github.title')}

-

{t('admin.github.subtitle').replace('{repo}', REPO)}

+

{t('admin.github.title')}

+

{t('admin.github.subtitle').replace('{repo}', REPO)}

GitHub @@ -299,7 +298,7 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b {/* Release content */}
- + {release.tag_name} {isLatest && ( @@ -317,18 +316,18 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
{release.name && release.name !== release.tag_name && ( -

+

{release.name}

)}
- + {formatDate(release.published_at || release.created_at)} {release.author && ( - + {t('admin.github.by')} {release.author.login} )} @@ -339,15 +338,14 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
{isExpanded && ( -
+
{renderBody(release.body)}
)} @@ -366,8 +364,7 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b +
+ +
+ {/* Search */} +
+ + setSearch(e.target.value)} + placeholder={t('journey.contributors.searchPlaceholder')} + className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500" + /> +
+ + {/* User list */} +
+ {filtered.length === 0 && ( +

{t('journey.contributors.noUsers')}

+ )} + {filtered.map(u => ( +
setSelectedUserId(u.id)} + className={`flex items-center gap-2.5 p-2.5 rounded-lg cursor-pointer transition-all ${ + selectedUserId === u.id + ? 'bg-zinc-100 dark:bg-zinc-800 border border-zinc-900 dark:border-white' + : 'hover:bg-zinc-50 dark:hover:bg-zinc-800 border border-transparent' + }`} + > +
+ {u.username[0].toUpperCase()} +
+
+
{u.username}
+
{u.email}
+
+ {selectedUserId === u.id && ( +
+ +
+ )} +
+ ))} +
+ + {/* Role selector */} +
+ +
+ {(['viewer', 'editor'] as const).map(r => ( + + ))} +
+
+
+ +
+ + +
+
+
+ ) +} diff --git a/client/src/components/Journey/JourneyShareSection.tsx b/client/src/components/Journey/JourneyShareSection.tsx new file mode 100644 index 00000000..122987b1 --- /dev/null +++ b/client/src/components/Journey/JourneyShareSection.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react' +import { Link, List, Grid, MapPin, Check } from 'lucide-react' +import { journeyApi } from '../../api/client' +import { useTranslation } from '../../i18n' +import { useToast } from '../shared/Toast' + +export default function JourneyShareSection({ journeyId }: { journeyId: number }) { + const { t } = useTranslation() + const [link, setLink] = useState<{ token: string; share_timeline: boolean; share_gallery: boolean; share_map: boolean } | null>(null) + const [loading, setLoading] = useState(true) + const [copied, setCopied] = useState(false) + const toast = useToast() + + useEffect(() => { + journeyApi.getShareLink(journeyId).then(d => setLink(d.link || null)).catch(() => {}).finally(() => setLoading(false)) + }, [journeyId]) + + const createLink = async () => { + try { + const res = await journeyApi.createShareLink(journeyId, { share_timeline: true, share_gallery: true, share_map: true }) + setLink({ token: res.token, share_timeline: true, share_gallery: true, share_map: true }) + toast.success(t('journey.share.linkCreated')) + } catch { toast.error(t('journey.share.createFailed')) } + } + + const togglePerm = async (key: 'share_timeline' | 'share_gallery' | 'share_map') => { + if (!link) return + const updated = { ...link, [key]: !link[key] } + setLink(updated) + try { + await journeyApi.createShareLink(journeyId, { share_timeline: updated.share_timeline, share_gallery: updated.share_gallery, share_map: updated.share_map }) + } catch { setLink(link); toast.error(t('journey.share.updateFailed')) } + } + + const deleteLink = async () => { + try { + await journeyApi.deleteShareLink(journeyId) + setLink(null) + toast.success(t('journey.share.linkDeleted')) + } catch { toast.error(t('journey.share.deleteFailed')) } + } + + const shareUrl = link ? `${window.location.origin}/public/journey/${link.token}` : '' + + const copyLink = () => { + navigator.clipboard.writeText(shareUrl) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + if (loading) return null + + return ( +
+ + + {!link ? ( + + ) : ( +
+ {/* URL + Copy */} +
+ + {shareUrl} + +
+ + {/* Permission toggles */} +
+ {[ + { key: 'share_timeline' as const, label: t('journey.share.timeline'), icon: List }, + { key: 'share_gallery' as const, label: t('journey.share.gallery'), icon: Grid }, + { key: 'share_map' as const, label: t('journey.share.map'), icon: MapPin }, + ].map(({ key, label, icon: Icon }) => ( + + ))} +
+ + {/* Delete link */} + +
+ )} +
+ ) +} diff --git a/client/src/components/Layout/InAppNotificationBell.tsx b/client/src/components/Layout/InAppNotificationBell.tsx index 0b220382..42df852b 100644 --- a/client/src/components/Layout/InAppNotificationBell.tsx +++ b/client/src/components/Layout/InAppNotificationBell.tsx @@ -45,8 +45,7 @@ export default function InAppNotificationBell(): React.ReactElement {
) : notifications.length === 0 ? (
- -

{t('notifications.empty')}

-

{t('notifications.emptyDescription')}

+ +

{t('notifications.empty')}

+

{t('notifications.emptyDescription')}

) : ( notifications.slice(0, 10).map(n => ( @@ -151,10 +145,8 @@ export default function InAppNotificationBell(): React.ReactElement { {/* Footer */} {userMenuOpen && ReactDOM.createPortal( <>
setUserMenuOpen(false)} /> -
-
-

{user.username}

-

{user.email}

+
+
+

{user.username}

+

{user.email}

{user.role === 'admin' && ( - + {t('nav.administrator')} )} @@ -249,8 +247,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
setUserMenuOpen(false)} - className="flex items-center gap-2 px-4 py-2 text-sm transition-colors" - style={{ color: 'var(--text-secondary)' }} + className="flex items-center gap-2 px-4 py-2 text-sm transition-colors text-content-secondary" onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> @@ -259,8 +256,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: {user.role === 'admin' && ( setUserMenuOpen(false)} - className="flex items-center gap-2 px-4 py-2 text-sm transition-colors" - style={{ color: 'var(--text-secondary)' }} + className="flex items-center gap-2 px-4 py-2 text-sm transition-colors text-content-secondary" onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> @@ -269,14 +265,14 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: )}
-
+
{appVersion && ( -
+
TREK diff --git a/client/src/components/Layout/PageSidebar.tsx b/client/src/components/Layout/PageSidebar.tsx index 9c293b9f..74832354 100644 --- a/client/src/components/Layout/PageSidebar.tsx +++ b/client/src/components/Layout/PageSidebar.tsx @@ -47,27 +47,23 @@ export default function PageSidebar({ return (
{/* Mobile top bar with hamburger */}
-
+
{activeLabel}
@@ -75,11 +71,9 @@ export default function PageSidebar({ {/* Desktop sidebar (always visible on lg) */}