mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-24 07:41:47 +00:00
Fix map tooltips, journey creation, and contributor avatars
- Map tooltips now respect light/dark mode via CSS variables - Journey creation inherits cover image from first selected trip - Only day-assigned places are synced to journey (no unplanned places) - Place count in trip picker reflects assigned places only - Contributor avatars shown in journey detail page - Suggestion banner button visible in dark mode (!important override) - Dashboard list view uses correct trips array and status label
This commit is contained in:
@@ -299,7 +299,7 @@ body {
|
|||||||
|
|
||||||
/* ── iOS-style map tooltip ─────────────────────── */
|
/* ── iOS-style map tooltip ─────────────────────── */
|
||||||
.leaflet-tooltip.map-tooltip {
|
.leaflet-tooltip.map-tooltip {
|
||||||
background: rgba(9, 9, 11, 0.85);
|
background: var(--tooltip-bg);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
border: none;
|
border: none;
|
||||||
@@ -310,7 +310,7 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: #fff;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.leaflet-tooltip.map-tooltip::before,
|
.leaflet-tooltip.map-tooltip::before,
|
||||||
.leaflet-tooltip-left.map-tooltip::before,
|
.leaflet-tooltip-left.map-tooltip::before,
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ function TripListItem({ trip, onEdit, onCopy, onDelete, onArchive, onClick, t, l
|
|||||||
: status === 'today' ? t('dashboard.status.today')
|
: status === 'today' ? t('dashboard.status.today')
|
||||||
: status === 'tomorrow' ? t('dashboard.status.tomorrow')
|
: status === 'tomorrow' ? t('dashboard.status.tomorrow')
|
||||||
: status === 'future' ? t('dashboard.status.daysLeft', { count: daysUntil(trip.start_date) })
|
: status === 'future' ? t('dashboard.status.daysLeft', { count: daysUntil(trip.start_date) })
|
||||||
: t('dashboard.status.past')}
|
: t('dashboard.mobile.completed')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1065,7 +1065,7 @@ export default function DashboardPage(): React.ReactElement {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Trips — desktop grid or list */}
|
{/* Trips — desktop grid or list */}
|
||||||
{!isLoading && (viewMode === 'grid' ? rest : rest).length > 0 && (
|
{!isLoading && (viewMode === 'grid' ? rest : trips).length > 0 && (
|
||||||
viewMode === 'grid' ? (
|
viewMode === 'grid' ? (
|
||||||
<div className="trip-grid hidden md:grid" style={{ gap: 16, marginBottom: 40 }}>
|
<div className="trip-grid hidden md:grid" style={{ gap: 16, marginBottom: 40 }}>
|
||||||
{rest.map(trip => (
|
{rest.map(trip => (
|
||||||
@@ -1083,7 +1083,7 @@ export default function DashboardPage(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="hidden md:flex" style={{ flexDirection: 'column', gap: 8, marginBottom: 40 }}>
|
<div className="hidden md:flex" style={{ flexDirection: 'column', gap: 8, marginBottom: 40 }}>
|
||||||
{rest.map(trip => (
|
{trips.map(trip => (
|
||||||
<TripListItem
|
<TripListItem
|
||||||
key={trip.id}
|
key={trip.id}
|
||||||
trip={trip}
|
trip={trip}
|
||||||
|
|||||||
@@ -471,9 +471,13 @@ export default function JourneyDetailPage() {
|
|||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{current.contributors.map((c: any) => (
|
{current.contributors.map((c: any) => (
|
||||||
<div key={c.user_id} className="flex items-center gap-2.5">
|
<div key={c.user_id} className="flex items-center gap-2.5">
|
||||||
<div className="w-7 h-7 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center text-[11px] font-semibold">
|
{c.avatar_url ? (
|
||||||
{(c.username || '?')[0].toUpperCase()}
|
<img src={c.avatar_url} className="w-7 h-7 rounded-full object-cover" alt="" />
|
||||||
</div>
|
) : (
|
||||||
|
<div className="w-7 h-7 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 flex items-center justify-center text-[11px] font-semibold">
|
||||||
|
{(c.username || '?')[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-xs font-medium text-zinc-900 dark:text-white">{c.username}</div>
|
<div className="text-xs font-medium text-zinc-900 dark:text-white">{c.username}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default function JourneyPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openCreateModal(activeSuggestion.id)}
|
onClick={() => openCreateModal(activeSuggestion.id)}
|
||||||
className="px-3 py-1.5 rounded-lg bg-white text-zinc-900 text-[12px] font-medium hover:bg-zinc-100"
|
className="px-3 py-1.5 rounded-lg !bg-white !text-zinc-900 text-[12px] font-medium hover:!bg-zinc-100"
|
||||||
>
|
>
|
||||||
{t('journey.frontpage.createJourney')}
|
{t('journey.frontpage.createJourney')}
|
||||||
</button>
|
</button>
|
||||||
@@ -336,7 +336,11 @@ export default function JourneyPage() {
|
|||||||
}`}>
|
}`}>
|
||||||
{selected && <Check size={12} className="text-white dark:text-zinc-900" />}
|
{selected && <Check size={12} className="text-white dark:text-zinc-900" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12 h-12 rounded-lg flex-shrink-0" style={{ background: pickGradient(trip.id) }} />
|
<div className="w-12 h-12 rounded-lg flex-shrink-0 overflow-hidden" style={{ background: pickGradient(trip.id) }}>
|
||||||
|
{trip.cover_image && (
|
||||||
|
<img src={trip.cover_image} className="w-full h-full object-cover" alt="" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-[14px] font-semibold text-zinc-900 dark:text-white">{trip.title}</div>
|
<div className="text-[14px] font-semibold text-zinc-900 dark:text-white">{trip.title}</div>
|
||||||
<div className="text-[12px] text-zinc-500 flex items-center gap-2.5 mt-0.5">
|
<div className="text-[12px] text-zinc-500 flex items-center gap-2.5 mt-0.5">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
updateTime,
|
updateTime,
|
||||||
setParticipants,
|
setParticipants,
|
||||||
} from '../services/assignmentService';
|
} from '../services/assignmentService';
|
||||||
|
import { onPlaceCreated } from '../services/journeyService';
|
||||||
import { AuthRequest } from '../types';
|
import { AuthRequest } from '../types';
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
@@ -45,6 +46,7 @@ router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripA
|
|||||||
const assignment = createAssignment(dayId, place_id, notes);
|
const assignment = createAssignment(dayId, place_id, notes);
|
||||||
res.status(201).json({ assignment });
|
res.status(201).json({ assignment });
|
||||||
broadcast(tripId, 'assignment:created', { assignment }, req.headers['x-socket-id'] as string);
|
broadcast(tripId, 'assignment:created', { assignment }, req.headers['x-socket-id'] as string);
|
||||||
|
try { onPlaceCreated(Number(tripId), Number(place_id)); } catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requireTripAccess, (req: Request, res: Response) => {
|
router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requireTripAccess, (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ export function createJourney(userId: number, data: {
|
|||||||
for (const tripId of data.trip_ids) {
|
for (const tripId of data.trip_ids) {
|
||||||
addTripToJourney(journeyId, tripId, userId);
|
addTripToJourney(journeyId, tripId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inherit cover image from first selected trip
|
||||||
|
const firstTrip = db.prepare('SELECT cover_image FROM trips WHERE id = ?').get(data.trip_ids[0]) as { cover_image: string | null } | undefined;
|
||||||
|
if (firstTrip?.cover_image) {
|
||||||
|
// trip stores full path (/uploads/covers/x.jpg), journey stores relative (covers/x.jpg)
|
||||||
|
const relativePath = firstTrip.cover_image.replace(/^\/uploads\//, '');
|
||||||
|
db.prepare('UPDATE journeys SET cover_image = ? WHERE id = ?').run(relativePath, journeyId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.prepare('SELECT * FROM journeys WHERE id = ?').get(journeyId) as Journey;
|
return db.prepare('SELECT * FROM journeys WHERE id = ?').get(journeyId) as Journey;
|
||||||
@@ -125,11 +133,15 @@ export function getJourneyFull(journeyId: number, userId: number) {
|
|||||||
`).all(journeyId);
|
`).all(journeyId);
|
||||||
|
|
||||||
// contributors
|
// contributors
|
||||||
const contributors = db.prepare(`
|
const contributorsRaw = db.prepare(`
|
||||||
SELECT jc.journey_id, jc.user_id, jc.role, jc.added_at, u.username, u.avatar
|
SELECT jc.journey_id, jc.user_id, jc.role, jc.added_at, u.username, u.avatar
|
||||||
FROM journey_contributors jc JOIN users u ON jc.user_id = u.id
|
FROM journey_contributors jc JOIN users u ON jc.user_id = u.id
|
||||||
WHERE jc.journey_id = ? ORDER BY jc.added_at
|
WHERE jc.journey_id = ? ORDER BY jc.added_at
|
||||||
`).all(journeyId);
|
`).all(journeyId) as any[];
|
||||||
|
const contributors = contributorsRaw.map(c => ({
|
||||||
|
...c,
|
||||||
|
avatar_url: c.avatar ? `/uploads/avatars/${c.avatar}` : null,
|
||||||
|
}));
|
||||||
|
|
||||||
// stats
|
// stats
|
||||||
const entryCount = entries.filter(e => e.type === 'entry').length;
|
const entryCount = entries.filter(e => e.type === 'entry').length;
|
||||||
@@ -221,8 +233,8 @@ export function syncTripPlaces(journeyId: number, tripId: number, authorId: numb
|
|||||||
const places = db.prepare(`
|
const places = db.prepare(`
|
||||||
SELECT p.*, da.day_id, d.date as day_date, da.assignment_time, da.assignment_end_time, d.day_number
|
SELECT p.*, da.day_id, d.date as day_date, da.assignment_time, da.assignment_end_time, d.day_number
|
||||||
FROM places p
|
FROM places p
|
||||||
LEFT JOIN day_assignments da ON da.place_id = p.id
|
INNER JOIN day_assignments da ON da.place_id = p.id
|
||||||
LEFT JOIN days d ON da.day_id = d.id
|
INNER JOIN days d ON da.day_id = d.id
|
||||||
WHERE p.trip_id = ?
|
WHERE p.trip_id = ?
|
||||||
ORDER BY d.day_number ASC, da.order_index ASC
|
ORDER BY d.day_number ASC, da.order_index ASC
|
||||||
`).all(tripId) as any[];
|
`).all(tripId) as any[];
|
||||||
@@ -303,11 +315,11 @@ export function onPlaceCreated(tripId: number, placeId: number) {
|
|||||||
const place = db.prepare(`
|
const place = db.prepare(`
|
||||||
SELECT p.*, da.day_id, d.date as day_date, da.assignment_time, d.day_number
|
SELECT p.*, da.day_id, d.date as day_date, da.assignment_time, d.day_number
|
||||||
FROM places p
|
FROM places p
|
||||||
LEFT JOIN day_assignments da ON da.place_id = p.id
|
INNER JOIN day_assignments da ON da.place_id = p.id
|
||||||
LEFT JOIN days d ON da.day_id = d.id
|
INNER JOIN days d ON da.day_id = d.id
|
||||||
WHERE p.id = ?
|
WHERE p.id = ?
|
||||||
`).get(placeId) as any;
|
`).get(placeId) as any;
|
||||||
if (!place) return;
|
if (!place) return; // not assigned to a day yet — skip
|
||||||
|
|
||||||
const now = ts();
|
const now = ts();
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
@@ -317,7 +329,7 @@ export function onPlaceCreated(tripId: number, placeId: number) {
|
|||||||
if (already) continue;
|
if (already) continue;
|
||||||
|
|
||||||
const journey = db.prepare('SELECT user_id FROM journeys WHERE id = ?').get(link.journey_id) as { user_id: number };
|
const journey = db.prepare('SELECT user_id FROM journeys WHERE id = ?').get(link.journey_id) as { user_id: number };
|
||||||
const entryDate = place.day_date || new Date().toISOString().split('T')[0];
|
const entryDate = place.day_date;
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO journey_entries (journey_id, source_trip_id, source_place_id, author_id, type, title, entry_date, entry_time, location_name, location_lat, location_lng, sort_order, created_at, updated_at)
|
INSERT INTO journey_entries (journey_id, source_trip_id, source_place_id, author_id, type, title, entry_date, entry_time, location_name, location_lat, location_lng, sort_order, created_at, updated_at)
|
||||||
@@ -701,7 +713,7 @@ export function getSuggestions(userId: number) {
|
|||||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT t.id, t.title, t.start_date, t.end_date, t.cover_image,
|
SELECT t.id, t.title, t.start_date, t.end_date, t.cover_image,
|
||||||
(SELECT COUNT(*) FROM places WHERE trip_id = t.id) as place_count
|
(SELECT COUNT(*) FROM places p INNER JOIN day_assignments da ON da.place_id = p.id WHERE p.trip_id = t.id) as place_count
|
||||||
FROM trips t
|
FROM trips t
|
||||||
LEFT JOIN trip_members tm ON t.id = tm.trip_id AND tm.user_id = ?
|
LEFT JOIN trip_members tm ON t.id = tm.trip_id AND tm.user_id = ?
|
||||||
WHERE (t.user_id = ? OR tm.user_id = ?)
|
WHERE (t.user_id = ? OR tm.user_id = ?)
|
||||||
@@ -718,7 +730,7 @@ export function getSuggestions(userId: number) {
|
|||||||
export function listUserTrips(userId: number) {
|
export function listUserTrips(userId: number) {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT t.id, t.title, t.start_date, t.end_date, t.cover_image,
|
SELECT t.id, t.title, t.start_date, t.end_date, t.cover_image,
|
||||||
(SELECT COUNT(*) FROM places WHERE trip_id = t.id) as place_count
|
(SELECT COUNT(*) FROM places p INNER JOIN day_assignments da ON da.place_id = p.id WHERE p.trip_id = t.id) as place_count
|
||||||
FROM trips t
|
FROM trips t
|
||||||
LEFT JOIN trip_members tm ON t.id = tm.trip_id AND tm.user_id = ?
|
LEFT JOIN trip_members tm ON t.id = tm.trip_id AND tm.user_id = ?
|
||||||
WHERE t.user_id = ? OR tm.user_id = ?
|
WHERE t.user_id = ? OR tm.user_id = ?
|
||||||
|
|||||||
Reference in New Issue
Block a user