From 7338adfbd8a81fb0e6987287b6ddf20629f4a666 Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Fri, 22 Nov 2024 03:42:56 +0200 Subject: [PATCH] Improve UX for election round selector --- .../Monitoring/Endpoint.cs | 1 + utils/SubmissionsFaker/Program.cs | 18 +++ web/src/common/types.ts | 7 + .../DataSourceSwitcher/DataSourceSwitcher.tsx | 3 +- web/src/components/layout/Header/Header.tsx | 72 ++++++++-- web/src/components/ui/dropdown-menu.tsx | 128 ++++++++++-------- .../election-event/models/election-event.ts | 16 +-- web/src/styles/tailwind.css | 2 +- 8 files changed, 172 insertions(+), 75 deletions(-) diff --git a/api/src/Vote.Monitor.Api.Feature.ElectionRound/Monitoring/Endpoint.cs b/api/src/Vote.Monitor.Api.Feature.ElectionRound/Monitoring/Endpoint.cs index fa205e834..ac082d6dd 100644 --- a/api/src/Vote.Monitor.Api.Feature.ElectionRound/Monitoring/Endpoint.cs +++ b/api/src/Vote.Monitor.Api.Feature.ElectionRound/Monitoring/Endpoint.cs @@ -26,6 +26,7 @@ public override async Task> ExecuteAsync(Request req, CancellationTok .Include(x => x.ElectionRound) .ThenInclude(x => x.MonitoringNgoForCitizenReporting) .Where(x => x.NgoId == req.NgoId) + .OrderBy(x => x.ElectionRound.StartDate) .Select(x => new NgoElectionRoundView { MonitoringNgoId = x.Id, diff --git a/utils/SubmissionsFaker/Program.cs b/utils/SubmissionsFaker/Program.cs index f80cb3e27..807dd167d 100644 --- a/utils/SubmissionsFaker/Program.cs +++ b/utils/SubmissionsFaker/Program.cs @@ -114,6 +114,24 @@ await AnsiConsole.Progress() .WithQuickReport(ScenarioObserver.Bob, ScenarioPollingStation.Bacau) .WithQuickReport(ScenarioObserver.Bob, ScenarioPollingStation.Cluj) )) + .WithElectionRound(ScenarioElectionRound.B, er => er + .WithPollingStation(ScenarioPollingStation.Iasi) + .WithPollingStation(ScenarioPollingStation.Bacau) + .WithPollingStation(ScenarioPollingStation.Cluj) + .WithMonitoringNgo(ScenarioNgos.Alfa) + .WithMonitoringNgo(ScenarioNgos.Beta)) + .WithElectionRound(ScenarioElectionRound.C, er => er + .WithPollingStation(ScenarioPollingStation.Iasi) + .WithPollingStation(ScenarioPollingStation.Bacau) + .WithPollingStation(ScenarioPollingStation.Cluj) + .WithMonitoringNgo(ScenarioNgos.Alfa) + .WithMonitoringNgo(ScenarioNgos.Beta)) + .WithElectionRound(ScenarioElectionRound.D, er => er + .WithPollingStation(ScenarioPollingStation.Iasi) + .WithPollingStation(ScenarioPollingStation.Bacau) + .WithPollingStation(ScenarioPollingStation.Cluj) + .WithMonitoringNgo(ScenarioNgos.Alfa) + .WithMonitoringNgo(ScenarioNgos.Beta)) .Please(); diff --git a/web/src/common/types.ts b/web/src/common/types.ts index 531149b1f..d4f3cbcbf 100644 --- a/web/src/common/types.ts +++ b/web/src/common/types.ts @@ -180,6 +180,12 @@ export const MultiSelectAnswerSchema = BaseAnswerSchema.extend({ }); export type MultiSelectAnswer = z.infer; +export enum ElectionRoundStatus { + NotStarted = 'NotStarted', + Started = 'Started', + Archived = 'Archived', +} + export type ElectionRoundMonitoring = { monitoringNgoId: string; electionRoundId: string; @@ -190,6 +196,7 @@ export type ElectionRoundMonitoring = { countryId: string; isMonitoringNgoForCitizenReporting: boolean; isCoalitionLeader: boolean; + status: ElectionRoundStatus; }; export type LevelNode = { diff --git a/web/src/components/DataSourceSwitcher/DataSourceSwitcher.tsx b/web/src/components/DataSourceSwitcher/DataSourceSwitcher.tsx index 6a56157e6..4d479986e 100644 --- a/web/src/components/DataSourceSwitcher/DataSourceSwitcher.tsx +++ b/web/src/components/DataSourceSwitcher/DataSourceSwitcher.tsx @@ -10,9 +10,8 @@ import { omit } from '../../lib/utils'; export function DataSourceSwitcher(): FunctionComponent { const isCoalitionLeader = useCurrentElectionRoundStore((s) => s.isCoalitionLeader); - const navigate = useNavigate(); - + const search: any = useSearch({ strict: false, }); diff --git a/web/src/components/layout/Header/Header.tsx b/web/src/components/layout/Header/Header.tsx index be9bef6f3..62b88d74d 100644 --- a/web/src/components/layout/Header/Header.tsx +++ b/web/src/components/layout/Header/Header.tsx @@ -5,8 +5,10 @@ import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, + DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Skeleton } from '@/components/ui/skeleton'; @@ -18,12 +20,13 @@ import { sleep } from '@/lib/utils'; import { queryClient } from '@/main'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { Bars3Icon, ChevronDownIcon, XMarkIcon } from '@heroicons/react/24/outline'; -import { UserCircleIcon } from '@heroicons/react/24/solid'; +import { PauseCircleIcon, PlayCircleIcon, StopCircleIcon, UserCircleIcon } from '@heroicons/react/24/solid'; import { useQuery } from '@tanstack/react-query'; import { Link, useNavigate, useRouter } from '@tanstack/react-router'; import clsx from 'clsx'; -import { Fragment, useContext, useEffect, useState } from 'react'; -import type { ElectionRoundMonitoring, FunctionComponent } from '../../../common/types'; +import { sortBy } from 'lodash'; +import { Fragment, useContext, useEffect, useMemo, useState } from 'react'; +import { ElectionRoundStatus, type ElectionRoundMonitoring, type FunctionComponent } from '../../../common/types'; import Logo from './Logo'; const navigation = [ @@ -43,8 +46,12 @@ const Header = (): FunctionComponent => { const navigate = useNavigate(); const [selectedElectionRound, setSelectedElection] = useState(); const router = useRouter(); - const { setCurrentElectionRoundId, setIsMonitoringNgoForCitizenReporting, currentElectionRoundId, setIsCoalitionLeader } = - useCurrentElectionRoundStore((s) => s); + const { + setCurrentElectionRoundId, + setIsMonitoringNgoForCitizenReporting, + currentElectionRoundId, + setIsCoalitionLeader, + } = useCurrentElectionRoundStore((s) => s); const handleSelectElectionRound = async (electionRound?: ElectionRoundMonitoring): Promise => { if (electionRound && selectedElectionRound?.electionRoundId != electionRound.electionRoundId) { @@ -52,7 +59,7 @@ const Header = (): FunctionComponent => { setCurrentElectionRoundId(electionRound.electionRoundId); setIsMonitoringNgoForCitizenReporting(electionRound.isMonitoringNgoForCitizenReporting); setIsCoalitionLeader(electionRound.isCoalitionLeader); - + sleep(1); await queryClient.invalidateQueries({ @@ -90,6 +97,22 @@ const Header = (): FunctionComponent => { } }, [electionRounds]); + const activeElections = useMemo(() => { + return sortBy( + [...(electionRounds ?? [])].filter((er) => er.status !== ElectionRoundStatus.Archived), + (er) => new Date(er.startDate).getTime(), + (er) => er.title + ); + }, [electionRounds]); + + const archivedElections = useMemo(() => { + return sortBy( + [...(electionRounds ?? [])].filter((er) => er.status === ElectionRoundStatus.Archived), + (er) => new Date(er.startDate).getTime(), + (er) => er.title + ); + }, [electionRounds]); + return ( {({ open }) => ( @@ -125,12 +148,12 @@ const Header = (): FunctionComponent => {
{status === 'pending' ? ( - + ) : ( - - {selectedElectionRound?.title} + +
{selectedElectionRound?.title}
@@ -141,11 +164,38 @@ const Header = (): FunctionComponent => { const electionRound = electionRounds?.find((er) => er.electionRoundId === value); handleSelectElectionRound(electionRound); }}> - {electionRounds?.map((electionRound) => ( + Upcomming elections + + {activeElections?.map((electionRound) => ( - {electionRound.title} +
+ {electionRound?.status === ElectionRoundStatus.NotStarted ? ( + + ) : null} + {electionRound?.status === ElectionRoundStatus.Started ? ( + + ) : null} +
+ {electionRound.title} +
+
+
+ ))} + + Archived elections + {archivedElections?.map((electionRound) => ( + +
+ + +
+ {electionRound.title} +
+
))} diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx index 8743a25e0..8e75d679b 100644 --- a/web/src/components/ui/dropdown-menu.tsx +++ b/web/src/components/ui/dropdown-menu.tsx @@ -1,40 +1,42 @@ -import * as React from 'react'; -import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils" -const DropdownMenu = DropdownMenuPrimitive.Root; +const DropdownMenu = DropdownMenuPrimitive.Root -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger -const DropdownMenuGroup = DropdownMenuPrimitive.Group; +const DropdownMenuGroup = DropdownMenuPrimitive.Group -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; +const DropdownMenuPortal = DropdownMenuPrimitive.Portal -const DropdownMenuSub = DropdownMenuPrimitive.Sub; +const DropdownMenuSub = DropdownMenuPrimitive.Sub -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean; + inset?: boolean } >(({ className, inset, children, ...props }, ref) => ( + {...props} + > {children} - + -)); -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -43,13 +45,14 @@ const DropdownMenuSubContent = React.forwardRef< -)); -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -60,32 +63,32 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - 'z-50 min-w-[8rem] overflow-y-auto max-h-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} {...props} /> -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean; + inset?: boolean } >(({ className, inset, ...props }, ref) => ( -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -94,20 +97,22 @@ const DropdownMenuCheckboxItem = React.forwardRef< - + {...props} + > + - + {children} -)); -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -116,46 +121,63 @@ const DropdownMenuRadioItem = React.forwardRef< - + {...props} + > + - + {children} -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean; + inset?: boolean } >(({ className, inset, ...props }, ref) => ( -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { - return ; -}; -DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" export { DropdownMenu, @@ -173,4 +195,4 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, -}; +} diff --git a/web/src/features/election-event/models/election-event.ts b/web/src/features/election-event/models/election-event.ts index 4b271d319..47d5f0691 100644 --- a/web/src/features/election-event/models/election-event.ts +++ b/web/src/features/election-event/models/election-event.ts @@ -1,17 +1,17 @@ -export type ElectionEventStatus = "NotStarted" | "Started" | "Archived"; +import { ElectionRoundStatus } from "@/common/types"; export interface ElectionEvent { id: string; title: string; englishTitle: string; startDate: string; - status: ElectionEventStatus; + status: ElectionRoundStatus; countryId: string; - countryIso2: string - countryIso3: string - countryNumericCode: string - countryName: string - countryFullName: string + countryIso2: string; + countryIso3: string; + countryNumericCode: string; + countryName: string; + countryFullName: string; createdOn: string; lastModifiedOn: string; -} \ No newline at end of file +} diff --git a/web/src/styles/tailwind.css b/web/src/styles/tailwind.css index 26ff6ba37..f98f3aae7 100644 --- a/web/src/styles/tailwind.css +++ b/web/src/styles/tailwind.css @@ -69,7 +69,7 @@ } .election-text { - max-width: 160px; + max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;