Skip to content

Commit 4b967b4

Browse files
authored
Merge pull request #1255 from david-roper/responsive-dashboard
Responsive dashboard
2 parents e0305a0 + 82b73af commit 4b967b4

1 file changed

Lines changed: 225 additions & 39 deletions

File tree

apps/web/src/routes/_app/dashboard.tsx

Lines changed: 225 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22

3-
import { Heading, Select, StatisticCard } from '@douglasneuroinformatics/libui/components';
3+
import { Dialog, Heading, Select, Spinner, StatisticCard } from '@douglasneuroinformatics/libui/components';
44
import { useTheme, useTranslation } from '@douglasneuroinformatics/libui/hooks';
55
import type { Theme } from '@douglasneuroinformatics/libui/hooks';
66
import { ClipboardDocumentIcon, DocumentTextIcon, UserIcon, UsersIcon } from '@heroicons/react/24/solid';
77
import type { AppSubjectName } from '@opendatacapture/schemas/core';
8-
import { createFileRoute, redirect } from '@tanstack/react-router';
8+
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
9+
import { AnimatePresence, motion } from 'motion/react';
910
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
1011

1112
import { PageHeader } from '@/components/PageHeader';
13+
import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery';
14+
import { useInstrumentRecords } from '@/hooks/useInstrumentRecords';
1215
import { summaryQueryOptions, useSummaryQuery } from '@/hooks/useSummaryQuery';
16+
import { useUsersQuery } from '@/hooks/useUsersQuery';
1317
import { useAppStore } from '@/store';
1418

1519
const RouteComponent = () => {
@@ -19,6 +23,44 @@ const RouteComponent = () => {
1923
const { t } = useTranslation();
2024
const [theme] = useTheme();
2125
const summaryQuery = useSummaryQuery({ params: { groupId: currentGroup?.id } });
26+
const navigate = useNavigate();
27+
const [isInstrumentModalOpen, setIsInstrumentModalOpen] = useState(false);
28+
const [isUserModalOpen, setIsUserModalOpen] = useState(false);
29+
const [isRecordModalOpen, setIsRecordModalOpen] = useState(false);
30+
const instrumentInfoQuery = useInstrumentInfoQuery();
31+
const userInfoQuery = useUsersQuery();
32+
33+
const recordsQuery = useInstrumentRecords({
34+
enabled: true,
35+
params: {
36+
groupId: currentGroup?.id
37+
}
38+
});
39+
40+
const instrumentData = currentGroup
41+
? instrumentInfoQuery.data?.filter((instrument) => {
42+
return currentGroup.accessibleInstrumentIds.includes(instrument.id);
43+
})
44+
: instrumentInfoQuery.data;
45+
46+
const instrumentInfo = instrumentData?.map((instrument) => {
47+
return {
48+
id: instrument.id,
49+
kind: instrument.kind,
50+
title: instrument.details.title
51+
};
52+
});
53+
54+
const recordIds = recordsQuery.data?.map((record) => record.instrumentId);
55+
56+
const recordCounter =
57+
instrumentInfo?.map((title) => {
58+
return {
59+
id: title.id,
60+
count: recordIds?.filter((val) => val === title.id).length ?? 0,
61+
instrumentTitle: title.title
62+
};
63+
}) ?? [];
2264

2365
const chartColors = {
2466
records: {
@@ -133,23 +175,76 @@ const RouteComponent = () => {
133175
</div>
134176
<div className="body-font" data-testid="dashboard-statistics">
135177
<div className="grid grid-cols-1 gap-6 text-center lg:grid-cols-2 xl:grid-cols-4">
136-
<div className="group transform transition-all duration-300 hover:scale-105" data-testid="statistic-users">
137-
<StatisticCard
138-
icon={
139-
<UsersIcon className="h-12 w-12 text-blue-600 transition-transform duration-300 group-hover:scale-110 dark:text-blue-400" />
140-
}
141-
label={t({
142-
en: 'Total Users',
143-
fr: "Nombre d'utilisateurs"
144-
})}
145-
value={summaryQuery.data.counts.users}
146-
/>
147-
</div>
148178
<div
149-
className="group transform transition-all duration-300 hover:scale-105"
179+
className="group flex transform transition-all duration-300 hover:scale-105"
180+
data-testid="statistic-users"
181+
>
182+
<Dialog open={isUserModalOpen} onOpenChange={setIsUserModalOpen}>
183+
<Dialog.Trigger className="grow">
184+
<StatisticCard
185+
icon={
186+
<UsersIcon className="h-12 w-12 text-blue-600 transition-transform duration-300 group-hover:scale-110 dark:text-blue-400" />
187+
}
188+
label={t({
189+
en: 'Total Users',
190+
fr: "Nombre d'utilisateurs"
191+
})}
192+
value={summaryQuery.data.counts.users}
193+
/>
194+
</Dialog.Trigger>
195+
<Dialog.Content data-spotlight-type="users-Modal-modal" data-testid="dashboard-users-Modal-dialog">
196+
<Dialog.Header>
197+
<Dialog.Title>
198+
{t({
199+
en: 'Users',
200+
fr: 'Les utilisateurs'
201+
})}
202+
</Dialog.Title>
203+
</Dialog.Header>
204+
<hr></hr>
205+
<ul className="flex flex-col gap-5 overflow-auto">
206+
{userInfoQuery.isLoading && <Spinner />}
207+
{userInfoQuery.isError && (
208+
<p>
209+
{t({
210+
en: 'Error finding users',
211+
fr: "erreur lors de la recherche d'utilisateurs"
212+
})}
213+
</p>
214+
)}
215+
<AnimatePresence mode="popLayout">
216+
{userInfoQuery.data?.map((user, i) => {
217+
return (
218+
<motion.li
219+
layout
220+
animate={{ opacity: 1, y: 0 }}
221+
exit={{ opacity: 0 }}
222+
initial={{ opacity: 0 }}
223+
key={user.username}
224+
transition={{ bounce: 0.2, delay: 0.15 * i, duration: 1.5, type: 'spring' }}
225+
>
226+
<div className="flex justify-between gap-4">
227+
<p>{user.username}</p>
228+
</div>
229+
</motion.li>
230+
);
231+
})}
232+
</AnimatePresence>
233+
</ul>
234+
</Dialog.Content>
235+
</Dialog>
236+
</div>
237+
<button
238+
className="group flex transform transition-all duration-300 hover:scale-105"
150239
data-testid="statistic-subjects"
240+
onClick={() => {
241+
void navigate({
242+
to: '/datahub'
243+
});
244+
}}
151245
>
152246
<StatisticCard
247+
className="grow"
153248
icon={
154249
<UserIcon className="h-12 w-12 text-emerald-600 transition-transform duration-300 group-hover:scale-110 dark:text-emerald-400" />
155250
}
@@ -159,36 +254,127 @@ const RouteComponent = () => {
159254
})}
160255
value={summaryQuery.data.counts.subjects}
161256
/>
162-
</div>
257+
</button>
163258
<div
164-
className="group transform transition-all duration-300 hover:scale-105"
259+
className="group flex transform transition-all duration-300 hover:scale-105"
165260
data-testid="statistic-instruments"
166261
>
167-
<StatisticCard
168-
icon={
169-
<ClipboardDocumentIcon className="h-12 w-12 text-amber-600 transition-transform duration-300 group-hover:scale-110 dark:text-amber-400" />
170-
}
171-
label={t({
172-
en: 'Total Instruments',
173-
fr: "Nombre d'instruments"
174-
})}
175-
value={summaryQuery.data.counts.instruments}
176-
/>
262+
<Dialog open={isInstrumentModalOpen} onOpenChange={setIsInstrumentModalOpen}>
263+
<Dialog.Trigger className="grow">
264+
<StatisticCard
265+
icon={
266+
<ClipboardDocumentIcon className="h-12 w-12 text-amber-600 transition-transform duration-300 group-hover:scale-110 dark:text-amber-400" />
267+
}
268+
label={t({
269+
en: 'Total Instruments',
270+
fr: "Nombre d'instruments"
271+
})}
272+
value={summaryQuery.data.counts.instruments}
273+
></StatisticCard>
274+
</Dialog.Trigger>
275+
<Dialog.Content
276+
data-spotlight-type="instruments-Modal-modal"
277+
data-testid="dashboard-instruments-Modal-dialog"
278+
>
279+
<Dialog.Header>
280+
<Dialog.Title>
281+
{t({
282+
en: 'Available Instruments',
283+
fr: 'Les instruments'
284+
})}
285+
</Dialog.Title>
286+
</Dialog.Header>
287+
<ul className="flex flex-col gap-5 overflow-auto">
288+
<AnimatePresence mode="popLayout">
289+
<div className="flex justify-between gap-4 font-bold">
290+
<p>
291+
{t({
292+
en: 'Title',
293+
fr: 'Titre'
294+
})}
295+
</p>{' '}
296+
<p>{t({ en: 'Kind', fr: 'Genre' })}</p>
297+
</div>
298+
<hr></hr>
299+
{instrumentInfo?.map((instrument, i) => {
300+
return (
301+
<motion.li
302+
layout
303+
animate={{ opacity: 1, y: 0 }}
304+
exit={{ opacity: 0 }}
305+
initial={{ opacity: 0 }}
306+
key={instrument.id}
307+
transition={{ bounce: 0.2, delay: 0.15 * i, duration: 1.5, type: 'spring' }}
308+
>
309+
<div className="flex justify-between gap-4">
310+
<p>{instrument.title}</p> <p>{instrument.kind}</p>
311+
</div>
312+
</motion.li>
313+
);
314+
})}
315+
</AnimatePresence>
316+
</ul>
317+
</Dialog.Content>
318+
</Dialog>
177319
</div>
178320
<div
179-
className="group transform transition-all duration-300 hover:scale-105"
321+
className="group flex transform transition-all duration-300 hover:scale-105"
180322
data-testid="statistic-records"
181323
>
182-
<StatisticCard
183-
icon={
184-
<DocumentTextIcon className="h-12 w-12 text-purple-600 transition-transform duration-300 group-hover:scale-110 dark:text-purple-400" />
185-
}
186-
label={t({
187-
en: 'Total Records',
188-
fr: "Nombre d'enregistrements"
189-
})}
190-
value={summaryQuery.data.counts.records}
191-
/>
324+
<Dialog open={isRecordModalOpen} onOpenChange={setIsRecordModalOpen}>
325+
<Dialog.Trigger className="grow">
326+
<StatisticCard
327+
icon={
328+
<DocumentTextIcon className="h-12 w-12 text-purple-600 transition-transform duration-300 group-hover:scale-110 dark:text-purple-400" />
329+
}
330+
label={t({
331+
en: 'Total Records',
332+
fr: "Nombre d'enregistrements"
333+
})}
334+
value={summaryQuery.data.counts.records}
335+
/>
336+
</Dialog.Trigger>
337+
<Dialog.Content data-spotlight-type="record-Modal-modal" data-testid="dashboard-record-Modal-dialog">
338+
<Dialog.Header>
339+
<Dialog.Title>
340+
{t({
341+
en: 'Number of Records',
342+
fr: "Nombre d'enregistrements"
343+
})}
344+
</Dialog.Title>
345+
</Dialog.Header>
346+
<ul className="flex flex-col gap-5 overflow-auto">
347+
<AnimatePresence mode="popLayout">
348+
<div className="flex justify-between gap-4 font-bold">
349+
<p>
350+
{t({
351+
en: 'Title',
352+
fr: 'Titre'
353+
})}
354+
</p>{' '}
355+
<p>{t({ en: 'Number', fr: 'Numero' })}</p>
356+
</div>
357+
<hr></hr>
358+
{recordCounter?.map((instrument, i) => {
359+
return (
360+
<motion.li
361+
layout
362+
animate={{ opacity: 1, y: 0 }}
363+
exit={{ opacity: 0 }}
364+
initial={{ opacity: 0 }}
365+
key={instrument.instrumentTitle}
366+
transition={{ bounce: 0.2, delay: 0.15 * i, duration: 1.5, type: 'spring' }}
367+
>
368+
<div className="flex justify-between gap-4">
369+
<p>{instrument.instrumentTitle}</p> <p>{instrument.count}</p>
370+
</div>
371+
</motion.li>
372+
);
373+
})}
374+
</AnimatePresence>
375+
</ul>
376+
</Dialog.Content>
377+
</Dialog>
192378
</div>
193379
</div>
194380
</div>

0 commit comments

Comments
 (0)