Skip to content

Commit

Permalink
feat: #266 Vessel search bar improvements + color fixes on map (#357)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexphiev authored Dec 12, 2024
1 parent 80ad8e8 commit 4fd4016
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 177 deletions.
173 changes: 93 additions & 80 deletions frontend/components/core/command/vessel-finder.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
"use client"

import { useShallow } from "zustand/react/shallow"
import { useState } from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { getVesselFirstExcursionSegments } from "@/services/backend-rest-client"
import { FlyToInterpolator } from "deck.gl"
import { useShallow } from "zustand/react/shallow"

import { Vessel, VesselPosition } from "@/types/vessel"
import { VesselPosition } from "@/types/vessel"
import { useMapStore } from "@/libs/stores/map-store"
import { useTrackModeOptionsStore } from "@/libs/stores/track-mode-options-store"
import { useVesselsStore } from "@/libs/stores/vessels-store"
import {
CommandDialog,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"
import { useMapStore } from "@/libs/stores/map-store"
import { useTrackModeOptionsStore } from "@/libs/stores/track-mode-options-store"
import { useVesselsStore } from "@/libs/stores/vessels-store"

type Props = {
wideMode: boolean
setWideMode: (wideMode: boolean) => void
}

const SEPARATOR = "___"

export function VesselFinderDemo({ wideMode }: Props) {
export function VesselFinderDemo({ wideMode, setWideMode }: Props) {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState<string>("")
const inputRef = useRef<HTMLInputElement>(null)

const { addTrackedVessel, trackedVesselIDs } = useTrackModeOptionsStore(
useShallow((state) => ({
Expand All @@ -36,28 +38,53 @@ export function VesselFinderDemo({ wideMode }: Props) {
}))
)

const {
setActivePosition,
viewState,
latestPositions,
setViewState,
} = useMapStore(
useShallow((state) => ({
viewState: state.viewState,
latestPositions: state.latestPositions,
setActivePosition: state.setActivePosition,
setViewState: state.setViewState,
}))
)
const { setActivePosition, viewState, latestPositions, setViewState } =
useMapStore(
useShallow((state) => ({
viewState: state.viewState,
latestPositions: state.latestPositions,
setActivePosition: state.setActivePosition,
setViewState: state.setViewState,
}))
)

const { vessels: allVessels } = useVesselsStore(
useShallow((state) => ({
vessels: state.vessels,
}))
)

const simpleVessels = useMemo(() => {
return allVessels.map((vessel) => ({
id: vessel.id,
title: vessel.ship_name,
subtitle: `MMSI ${vessel.mmsi} | IMO ${vessel.imo}`,
value: `${vessel.ship_name}${SEPARATOR}${vessel.mmsi}${SEPARATOR}${vessel.imo}${SEPARATOR}${vessel.id}`,
}))
}, [allVessels])

const filteredItems = useMemo(
() =>
simpleVessels.filter(
(vessel) =>
vessel.value.toLowerCase().includes(search.toLowerCase()) &&
!trackedVesselIDs.includes(vessel.id)
),
[simpleVessels, search, trackedVesselIDs]
)

useEffect(() => {
if (!wideMode && open) {
setOpen(false)
}
}, [wideMode])

const displayedItems = filteredItems.slice(0, 50)

const onSelectVessel = async (vesselIdentifier: string) => {
setOpen(false)
const vesselId = parseInt(vesselIdentifier.split(SEPARATOR)[3])

const response = await getVesselFirstExcursionSegments(vesselId)
if (vesselId && !trackedVesselIDs.includes(vesselId)) {
addTrackedVessel(vesselId)
Expand All @@ -83,67 +110,53 @@ export function VesselFinderDemo({ wideMode }: Props) {
}

return (
<>
<button
type="button"
className="dark:highlight-white/5 flex items-center rounded-md bg-color-3 py-1.5 pl-2 pr-3 text-sm leading-6 text-slate-400 shadow-sm ring-1 ring-color-2 hover:bg-slate-700 hover:ring-slate-300"
<Command
className={`border-[0.5px] border-solid ${!wideMode ? "cursor-pointer hover:border-primary hover:text-primary" : "cursor-default"}`}
onClick={() => {
if (!wideMode) {
setWideMode(true)
setOpen(true)
inputRef.current?.focus()
}
}}
>
<CommandInput
ref={inputRef}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
onClick={() => setOpen(true)}
onValueChange={(value) => setSearch(value)}
placeholder="Type MMSI, IMO or vessel name to search..."
/>
<CommandList
hidden={!open}
onMouseDown={(e) => {
e.preventDefault()
}}
>
<svg
width="24"
height="24"
fill="none"
aria-hidden="true"
className="mr-3 flex-none"
>
<path
d="m19 19-3.5-3.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<circle
cx="11"
cy="11"
r="6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></circle>
</svg>
{wideMode && <>Find vessels...</>}
</button>

<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder="Type MMSI, IMO or vessel name to search..."
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Vessels">
{allVessels.map((vessel: Vessel) => {
return (
<CommandItem
key={`${vessel.id}`}
onSelect={(value) => onSelectVessel(value)}
value={`${vessel.ship_name}${SEPARATOR}${vessel.mmsi}${SEPARATOR}${vessel.imo}${SEPARATOR}${vessel.id}`} // so we can search by name, mmsi, imo
>
<span>{vessel.ship_name}</span>
<span className="ml-2 text-xxxs">
{" "}
MMSI {vessel.mmsi} | IMO {vessel.imo}
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Vessels">
{displayedItems.map((vessel) => {
return (
<CommandItem
className="border-none"
key={`${vessel.id}`}
onSelect={(value) => onSelectVessel(value)}
value={vessel.value}
>
<div className="flex flex-wrap items-baseline gap-1">
<span>{vessel.title}</span>
<span>-</span>
<span className="text-xxs text-neutral-300">
{vessel.subtitle}
</span>
</CommandItem>
)
})}
</CommandGroup>
<CommandSeparator />
</CommandList>
</CommandDialog>
</>
</div>
</CommandItem>
)
})}
</CommandGroup>
<CommandSeparator />
</CommandList>
</Command>
)
}
9 changes: 6 additions & 3 deletions frontend/components/core/left-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { useEffect, useState } from "react"
import { useEffect } from "react"
import Image from "next/image"
import TrawlWatchLogo from "@/public/trawlwatch.svg"
import { ChartBarIcon } from "@heroicons/react/24/outline"
Expand Down Expand Up @@ -114,7 +114,7 @@ export default function LeftPanel() {
name="Dashboard"
wide={leftPanelOpened}
>
<ChartBarIcon className="w-8 min-w-8 stroke-neutral-200 stroke-1 hover:stroke-[1.25]" />
<ChartBarIcon className="size-8 stroke-[0.75] hover:text-primary" />
</NavigationLink>
</div>
<div className="flex flex-col gap-3 bg-color-3 p-5">
Expand All @@ -123,7 +123,10 @@ export default function LeftPanel() {
<Spinner className="text-white" />
</div>
) : (
<VesselFinderDemo wideMode={leftPanelOpened} />
<VesselFinderDemo
wideMode={leftPanelOpened}
setWideMode={setLeftPanelOpened}
/>
)}
</div>
</>
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/core/map/filter-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export function FilterButton({
<Button
variant="outline"
size="sm"
className={`h-7 w-fit justify-start font-normal ${
className={`h-7 w-fit justify-start font-normal hover:text-primary-foreground/90 ${
isActive
? "bg-primary text-primary-foreground hover:bg-primary/90"
? "bg-primary hover:bg-primary/90"
: "bg-muted hover:bg-muted/50"
}`}
onClick={() => onToggle(value)}
Expand Down
16 changes: 10 additions & 6 deletions frontend/components/core/map/zone-filter-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Image from "next/image"
import { LayersIcon } from "lucide-react"

import { ZoneCategory } from "@/types/zone"
import { cn } from "@/libs/utils"
import Spinner from "@/components/ui/custom/spinner"
import {
Dialog,
Expand All @@ -12,8 +14,6 @@ import {
import IconButton from "@/components/ui/icon-button"

import { FilterButton } from "./filter-button"
import { cn } from "@/libs/utils"
import { LayersIcon } from "lucide-react"

interface ZoneFilterModalProps {
activeZones: string[]
Expand Down Expand Up @@ -59,18 +59,22 @@ export default function ZoneFilterModal({
<IconButton
disabled={isLoading}
description="Configure layers display settings"
className="dark:text-white"
>
{isLoading ? (
<Spinner />
<Spinner className="text-black dark:text-white" />
) : (
<LayersIcon className="size-5 text-black dark:text-white" />
)}
</IconButton>
</DialogTrigger>
<DialogContent className={cn("flex w-64 flex-col gap-6 bg-white", className)}>
<DialogContent
className={cn(
"flex w-64 flex-col gap-6 bg-white text-background",
className
)}
>
<DialogHeader className="flex flex-row items-center justify-between space-y-0">
<DialogTitle className="flex items-center gap-2 text-xl text-black">
<DialogTitle className="flex items-center gap-2 text-xl text-primary-foreground">
<LayersIcon className="size-5" />
Zones
</DialogTitle>
Expand Down
Loading

0 comments on commit 4fd4016

Please sign in to comment.