Skip to content

Commit ea8a95c

Browse files
Merge pull request #17 from Juliusolsson05/fix/dashboard-route-relationships
fix: preserve dashboard entity relationships in routes
2 parents 076db25 + 55c2b39 commit ea8a95c

9 files changed

Lines changed: 135 additions & 47 deletions

File tree

src/features/actors/components/ActorsContent.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
'use client';
22

3-
import { useMemo, useState } from 'react';
3+
import { useCallback, useState } from 'react';
44

5-
import { ArrowLeft,Users } from 'lucide-react';
5+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
66

7-
import { track } from '@/shared/lib/analytics';
7+
import { ArrowLeft, Users } from 'lucide-react';
88

99
import { Button } from '@/components/ui/button';
1010
import { ResizableHandle,ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
@@ -16,31 +16,43 @@ import { ListDetailScreenSkeleton } from '@/shared/components/loading/screen-ske
1616
import { DaySelector } from '@/shared/components/shared/DaySelector';
1717
import { EmptyState } from '@/shared/components/shared/EmptyState';
1818

19+
import { track } from '@/shared/lib/analytics';
1920
import { useConflictDay } from '@/shared/hooks/use-conflict-day';
2021
import { useIsLandscapePhone } from '@/shared/hooks/use-is-landscape-phone';
2122
import { useIsMobile } from '@/shared/hooks/use-is-mobile';
2223
import { useLandscapeScrollEmitter } from '@/shared/hooks/use-landscape-scroll-emitter';
2324
import { usePanelLayout } from '@/shared/hooks/use-panel-layout';
2425

2526
export function ActorsContent() {
26-
const initActor = useMemo(() => {
27-
if (typeof window === 'undefined') return null;
28-
return new URLSearchParams(window.location.search).get('actor');
29-
}, []);
27+
const pathname = usePathname();
28+
const router = useRouter();
29+
const searchParams = useSearchParams();
3030
const { currentDay, setDay } = useConflictDay();
3131
const isMobile = useIsMobile(1024);
3232
const isLandscapePhone = useIsLandscapePhone();
3333
const usePageScroll = isMobile && isLandscapePhone;
3434
const onLandscapeScroll = useLandscapeScrollEmitter(usePageScroll);
3535

36-
const [selId, setSelId] = useState<string | null>(() => initActor);
36+
const selId = searchParams.get('actor');
3737
const [tab, setTab] = useState<'intel' | 'signals' | 'military'>('intel');
3838
const { defaultLayout, onLayoutChanged } = usePanelLayout({ id: 'actors' });
3939

4040
const { data: actors, isLoading } = useActors(undefined, currentDay || undefined);
4141
const { data: actorDetail } = useActor(undefined, selId ?? undefined);
4242
const selected = actorDetail ?? actors?.find(a => a.id === selId) ?? null;
4343

44+
const handleSelect = useCallback((id: string | null) => {
45+
const next = new URLSearchParams(searchParams.toString());
46+
if (id) next.set('actor', id);
47+
else next.delete('actor');
48+
const qs = next.toString();
49+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
50+
if (id) {
51+
setTab('intel');
52+
track('actor_selected', { actor_id: id });
53+
}
54+
}, [pathname, router, searchParams]);
55+
4456
if (isLoading) return <ListDetailScreenSkeleton />;
4557

4658
if (isMobile) {
@@ -55,7 +67,7 @@ export function ActorsContent() {
5567
<Button
5668
variant="ghost"
5769
size="xs"
58-
onClick={() => setSelId(null)}
70+
onClick={() => handleSelect(null)}
5971
className="mono h-7 px-2 text-[9px] font-bold tracking-[0.06em]"
6072
>
6173
<ArrowLeft size={12} />
@@ -70,7 +82,7 @@ export function ActorsContent() {
7082
) : (
7183
<ActorList
7284
selectedId={selId}
73-
onSelect={id => { setSelId(id); if (id) { setTab('intel'); track('actor_selected', { actor_id: id }); } }}
85+
onSelect={handleSelect}
7486
currentDay={currentDay}
7587
onDayChange={setDay}
7688
compact={usePageScroll}
@@ -86,7 +98,7 @@ export function ActorsContent() {
8698
<ResizablePanel id="list" defaultSize="22%" minSize="15%" maxSize="40%" className="flex flex-col overflow-hidden min-w-[180px]">
8799
<ActorList
88100
selectedId={selId}
89-
onSelect={id => { setSelId(id); if (id) { setTab('intel'); track('actor_selected', { actor_id: id }); } }}
101+
onSelect={handleSelect}
90102
currentDay={currentDay}
91103
onDayChange={setDay}
92104
/>

src/features/dashboard/components/MobileOverview.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useMemo, useState } from 'react';
44

55
import Link from 'next/link';
66

7-
import { ArrowRight, BookOpen, Map as MapIcon, TrendingUp,Users, Zap } from 'lucide-react';
7+
import { ArrowRight, BookOpen, Map as MapIcon, TrendingUp, Users, Zap } from 'lucide-react';
88

99
import { useActors } from '@/features/actors/queries';
1010
import { CasChip } from '@/features/dashboard/components/CasChip';
@@ -56,7 +56,21 @@ export function MobileOverview() {
5656
const totalStories = stories.length;
5757
const critCount = recentEvents.filter(e => e.severity === 'CRITICAL').length;
5858
const [expandedSummary, setExpandedSummary] = useState(false);
59-
const [expandedEvents, setExpandedEvents] = useState<Set<string>>(new Set());
59+
const feedHref = (eventId?: string) => {
60+
const params = new URLSearchParams();
61+
if (latestDay) params.set('day', latestDay);
62+
if (eventId) params.set('event', eventId);
63+
const qs = params.toString();
64+
return qs ? `/dashboard/feed?${qs}` : '/dashboard/feed';
65+
};
66+
const mapHref = (storyId?: string) => {
67+
const params = new URLSearchParams();
68+
if (storyId) params.set('story', storyId);
69+
const qs = params.toString();
70+
return qs ? `/dashboard/map?${qs}` : '/dashboard/map';
71+
};
72+
const actorsHref = latestDay ? `/dashboard/actors?day=${latestDay}` : '/dashboard/actors';
73+
const briefHref = latestDay ? `/dashboard/brief?day=${latestDay}` : '/dashboard/brief';
6074

6175
return (
6276
<div className="flex-1 min-h-0 overflow-y-auto bg-[var(--bg-1)] safe-pb">
@@ -91,7 +105,7 @@ export function MobileOverview() {
91105
)}
92106

93107
{/* ── GO TO MAP hero ── */}
94-
<Link href="/dashboard/map" className="no-underline">
108+
<Link href={mapHref()} className="no-underline">
95109
<div className="safe-px my-3 py-4 bg-[var(--blue-dim)] border border-[var(--blue)] flex items-center justify-between">
96110
<div className="flex items-center gap-3">
97111
<MapIcon size={20} strokeWidth={2} className="text-[var(--blue-l)]" />
@@ -123,15 +137,15 @@ export function MobileOverview() {
123137
<div className="border-t border-[var(--bd)]">
124138
<div className="flex items-center justify-between safe-px py-2 bg-[var(--bg-2)] border-b border-[var(--bd)]">
125139
<span className="section-title">Latest Events</span>
126-
<Link href="/dashboard/feed" className="no-underline flex items-center gap-1">
140+
<Link href={feedHref()} className="no-underline flex items-center gap-1">
127141
<span className="mono text-[9px] text-[var(--blue-l)] font-bold">See all</span>
128142
<ArrowRight size={10} className="text-[var(--blue-l)]" />
129143
</Link>
130144
</div>
131145
{recentEvents.map((evt, i) => {
132146
const sc = SEV_C[evt.severity] ?? 'var(--info)';
133147
return (
134-
<Link key={evt.id} href={`/dashboard/feed?event=${evt.id}`} className="no-underline">
148+
<Link key={evt.id} href={feedHref(evt.id)} className="no-underline">
135149
<div
136150
className="flex gap-2.5 items-start safe-px py-2 hover:bg-[var(--bg-3)] transition-colors"
137151
style={{
@@ -159,7 +173,7 @@ export function MobileOverview() {
159173
<div className="border-t border-[var(--bd)] mt-0">
160174
<div className="flex items-center justify-between safe-px py-2 bg-[var(--bg-2)] border-b border-[var(--bd)]">
161175
<span className="section-title">Active Stories</span>
162-
<Link href="/dashboard/map" className="no-underline flex items-center gap-1">
176+
<Link href={mapHref()} className="no-underline flex items-center gap-1">
163177
<span className="mono text-[9px] text-[var(--blue-l)] font-bold">Map</span>
164178
<ArrowRight size={10} className="text-[var(--blue-l)]" />
165179
</Link>
@@ -171,7 +185,7 @@ export function MobileOverview() {
171185
};
172186
const c = catColor[story.category] ?? 'var(--t3)';
173187
return (
174-
<Link key={story.id} href="/dashboard/map" className="no-underline">
188+
<Link key={story.id} href={mapHref(story.id)} className="no-underline">
175189
<div
176190
className="flex gap-2.5 items-start safe-px py-2.5 hover:bg-[var(--bg-3)] transition-colors"
177191
style={{
@@ -222,8 +236,9 @@ export function MobileOverview() {
222236
<div className="border-t border-[var(--bd)] safe-px py-3">
223237
<div className="grid grid-cols-2 gap-2">
224238
{[
225-
{ href: '/dashboard/actors', label: 'ACTORS', icon: Users, color: 'var(--teal)' },
239+
{ href: actorsHref, label: 'ACTORS', icon: Users, color: 'var(--teal)' },
226240
{ href: '/dashboard/predictions', label: 'PREDICTIONS', icon: TrendingUp, color: 'var(--warning)' },
241+
{ href: briefHref, label: 'BRIEF', icon: BookOpen, color: 'var(--info)' },
227242
].map(nav => (
228243
<Link key={nav.href} href={nav.href} className="no-underline">
229244
<div className="flex items-center gap-2.5 px-3 py-3 border border-[var(--bd)] bg-[var(--bg-2)] hover:bg-[var(--bg-3)] transition-colors">

src/features/dashboard/components/WorkspaceDashboard.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ import { widgetComponents } from './widgets';
3939

4040
import { useAppDispatch,useAppSelector } from '@/shared/state';
4141

42-
const WIDGET_LINKS: Partial<Record<WidgetKey, { href: string; label: string }>> = {
43-
latest: { href: '/dashboard/feed', label: 'View All' },
44-
actors: { href: '/dashboard/actors', label: 'Dossiers' },
42+
const WIDGET_LINKS: Partial<Record<WidgetKey, { href: string; label: string; preserveDay?: boolean }>> = {
43+
latest: { href: '/dashboard/feed', label: 'View All', preserveDay: true },
44+
actors: { href: '/dashboard/actors', label: 'Dossiers', preserveDay: true },
4545
signals: { href: '/dashboard/signals', label: 'All Signals' },
4646
map: { href: '/dashboard/map', label: 'Full Map' },
4747
predictions: { href: '/dashboard/predictions', label: 'All Markets' },
48-
brief: { href: '/dashboard/brief', label: 'Full Brief' },
48+
brief: { href: '/dashboard/brief', label: 'Full Brief', preserveDay: true },
4949
};
5050

5151
export function WorkspaceDashboard() {
@@ -58,6 +58,14 @@ export function WorkspaceDashboard() {
5858
const allDays = bootstrap?.days ?? [];
5959
const [dashDay, setDashDay] = useState<string>('');
6060
const effectiveDashDay = dashDay || allDays[allDays.length - 1] || '';
61+
const widgetLinks = Object.fromEntries(
62+
Object.entries(WIDGET_LINKS).map(([key, value]) => {
63+
const href = effectiveDashDay && value!.preserveDay
64+
? `${value!.href}?day=${effectiveDashDay}`
65+
: value!.href;
66+
return [key, { ...value!, href }];
67+
}),
68+
) as typeof WIDGET_LINKS;
6169

6270
const { data: conflict, isLoading: conflictLoading } = useConflict();
6371
const { data: snapshots, isLoading: snapshotsLoading } = useConflictDays();
@@ -257,9 +265,9 @@ export function WorkspaceDashboard() {
257265
</div>
258266
)}
259267

260-
{!editing && WIDGET_LINKS[widget] && (
261-
<Link href={WIDGET_LINKS[widget]!.href} className="no-underline ml-auto flex items-center gap-1">
262-
<span className="text-[9px] text-[var(--blue-l)] font-semibold">{WIDGET_LINKS[widget]!.label}</span>
268+
{!editing && widgetLinks[widget] && (
269+
<Link href={widgetLinks[widget]!.href} className="no-underline ml-auto flex items-center gap-1">
270+
<span className="text-[9px] text-[var(--blue-l)] font-semibold">{widgetLinks[widget]!.label}</span>
263271
<ArrowRight size={10} strokeWidth={2} className="text-[var(--blue-l)]" />
264272
</Link>
265273
)}

src/features/dashboard/components/widgets/ActorsWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function ActorsWidget() {
2222
const actC = ACT_C[snap.activityLevel] ?? 'var(--t2)';
2323
const staC = STA_C[snap.stance] ?? 'var(--t2)';
2424
return (
25-
<Link key={actor.id} href={`/dashboard/actors?actor=${actor.id}`} className="no-underline">
25+
<Link key={actor.id} href={`/dashboard/actors?day=${day}&actor=${actor.id}`} className="no-underline">
2626
<div
2727
className="flex items-start gap-2.5 px-3 py-2 cursor-pointer hover:bg-[var(--bg-3)] transition-colors"
2828
style={{

src/features/dashboard/components/widgets/BriefWidget.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function BriefWidget() {
7878
{topEvents.map((evt, i) => {
7979
const sc = SEV_C[evt.severity] ?? 'var(--info)';
8080
return (
81-
<Link key={evt.id} href={`/dashboard/feed?event=${evt.id}`} className="no-underline">
81+
<Link key={evt.id} href={`/dashboard/feed?day=${day}&event=${evt.id}`} className="no-underline">
8282
<div
8383
className="flex gap-2.5 items-start py-1.5 hover:bg-[var(--bg-3)] transition-colors"
8484
style={{ borderBottom: i < topEvents.length - 1 ? '1px solid var(--bd-s)' : 'none', borderLeft: `3px solid ${sc}` }}
@@ -114,7 +114,7 @@ export function BriefWidget() {
114114

115115
{/* link to full brief */}
116116
<div className="px-4 py-2.5">
117-
<Link href="/dashboard/brief" className="no-underline flex items-center gap-1">
117+
<Link href={`/dashboard/brief?day=${day}`} className="no-underline flex items-center gap-1">
118118
<span className="text-[9px] text-[var(--blue-l)] font-semibold">Read Full Brief →</span>
119119
</Link>
120120
</div>

src/features/dashboard/components/widgets/LatestEventsWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function LatestEventsWidget() {
2929
{events.map((evt, i) => {
3030
const sc = SEV_C[evt.severity] ?? 'var(--info)';
3131
return (
32-
<Link key={evt.id} href={`/dashboard/feed?event=${evt.id}`} className="no-underline">
32+
<Link key={evt.id} href={`/dashboard/feed?day=${day}&event=${evt.id}`} className="no-underline">
3333
<div
3434
className="flex gap-3 items-start px-4 py-2 cursor-pointer hover:bg-[var(--bg-3)] transition-colors"
3535
style={{ borderBottom: i < events.length - 1 ? '1px solid var(--bd-s)' : 'none' }}

src/features/events/components/EventReportContent.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22
import Link from 'next/link';
3+
import { useSearchParams } from 'next/navigation';
34

45
import { ArrowRight } from 'lucide-react';
56

@@ -34,6 +35,8 @@ type Props = {
3435

3536
export function EventReportContent({ event, compact = false, pageScroll = false }: Props) {
3637
const sc = SEV_C[event.severity] ?? 'var(--info)';
38+
const searchParams = useSearchParams();
39+
const day = searchParams.get('day');
3740

3841
return (
3942
<div className={cn(compact ? (pageScroll ? 'safe-px py-3' : 'px-3 py-3') : 'px-6 py-5')}>
@@ -103,8 +106,11 @@ export function EventReportContent({ event, compact = false, pageScroll = false
103106
<div className="flex flex-col gap-1.5">
104107
{event.actorResponses.map((r, i) => {
105108
const stC = STANCE_C[r.stance] ?? 'var(--t2)';
109+
const actorHref = day
110+
? `/dashboard/actors?day=${day}&actor=${r.actorId}`
111+
: `/dashboard/actors?actor=${r.actorId}`;
106112
return (
107-
<Link key={i} href={`/dashboard/actors?actor=${r.actorId}`} className="no-underline">
113+
<Link key={i} href={actorHref} className="no-underline">
108114
<div
109115
className="px-3 py-2 border border-[var(--bd)] cursor-pointer hover:bg-[var(--bg-3)] transition-colors"
110116
style={{ borderLeft: `3px solid ${stC}` }}

src/features/events/components/FeedContent.tsx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
'use client';
22

3-
import { useMemo,useState } from 'react';
3+
import { useCallback, useMemo, useState } from 'react';
44

5-
import { ArrowLeft,FileText } from 'lucide-react';
5+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
66

7-
import { track } from '@/shared/lib/analytics';
7+
import { ArrowLeft, FileText } from 'lucide-react';
88

99
import { Button } from '@/components/ui/button';
1010
import { ResizableHandle,ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
1111

1212
import { EventDetail } from '@/features/events/components/EventDetail';
1313
import { EventLog } from '@/features/events/components/EventLog';
14-
import { ALL_TYPES,FeedFilterRail } from '@/features/events/components/FeedFilterRail';
14+
import { ALL_TYPES, FeedFilterRail } from '@/features/events/components/FeedFilterRail';
1515
import { useEvent, useEvents } from '@/features/events/queries';
1616
import { ListDetailScreenSkeleton } from '@/shared/components/loading/screen-skeletons';
1717
import { EmptyState } from '@/shared/components/shared/EmptyState';
1818

19+
import { track } from '@/shared/lib/analytics';
1920
import { useConflictDay } from '@/shared/hooks/use-conflict-day';
2021
import { useIsLandscapePhone } from '@/shared/hooks/use-is-landscape-phone';
2122
import { useIsMobile } from '@/shared/hooks/use-is-mobile';
@@ -25,10 +26,9 @@ import { usePanelLayout } from '@/shared/hooks/use-panel-layout';
2526
import type { EventType,Severity } from '@/types/domain';
2627

2728
export function FeedContent() {
28-
const initEvent = useMemo(() => {
29-
if (typeof window === 'undefined') return null;
30-
return new URLSearchParams(window.location.search).get('event');
31-
}, []);
29+
const pathname = usePathname();
30+
const router = useRouter();
31+
const searchParams = useSearchParams();
3232
const isMobile = useIsMobile(1024);
3333
const isLandscapePhone = useIsLandscapePhone();
3434
const usePageScroll = isMobile && isLandscapePhone;
@@ -42,7 +42,7 @@ export function FeedContent() {
4242
Object.fromEntries(ALL_TYPES.map(t => [t, true])) as Record<EventType, boolean>,
4343
);
4444
const [verOnly, setVerOnly] = useState(false);
45-
const [selId, setSelId] = useState<string | null>(() => initEvent);
45+
const selId = searchParams.get('event');
4646
const [tab, setTab] = useState<'report' | 'signals'>('report');
4747
const [filtersOpen, setFiltersOpen] = useState(false);
4848
const { defaultLayout, onLayoutChanged } = usePanelLayout({ id: 'feed', panelIds: ['filters', 'log', 'detail'] });
@@ -60,6 +60,18 @@ export function FeedContent() {
6060

6161
const selected = selectedEvent ?? allEvents?.find(e => e.id === selId) ?? null;
6262

63+
const handleSelect = useCallback((id: string | null) => {
64+
const next = new URLSearchParams(searchParams.toString());
65+
if (id) next.set('event', id);
66+
else next.delete('event');
67+
const qs = next.toString();
68+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
69+
if (id) {
70+
setTab('report');
71+
track('event_selected', { event_id: id });
72+
}
73+
}, [pathname, router, searchParams]);
74+
6375
if (isLoading) return <ListDetailScreenSkeleton />;
6476

6577
if (isMobile) {
@@ -74,7 +86,7 @@ export function FeedContent() {
7486
<Button
7587
variant="ghost"
7688
size="xs"
77-
onClick={() => setSelId(null)}
89+
onClick={() => handleSelect(null)}
7890
className="mono h-7 px-2 text-[9px] font-bold tracking-[0.06em]"
7991
>
8092
<ArrowLeft size={12} />
@@ -129,7 +141,7 @@ export function FeedContent() {
129141
<EventLog
130142
events={filtered}
131143
selectedId={selId}
132-
onSelect={id => { setSelId(id); if (id) { setTab('report'); track('event_selected', { event_id: id }); } }}
144+
onSelect={handleSelect}
133145
compact={usePageScroll}
134146
pageScroll={usePageScroll}
135147
/>
@@ -163,7 +175,7 @@ export function FeedContent() {
163175
<EventLog
164176
events={filtered}
165177
selectedId={selId}
166-
onSelect={id => { setSelId(id); if (id) { setTab('report'); track('event_selected', { event_id: id }); } }}
178+
onSelect={handleSelect}
167179
/>
168180
</ResizablePanel>
169181
<ResizableHandle />

0 commit comments

Comments
 (0)