Skip to content

Commit

Permalink
ProjectDashboard: Migrate Map and Teaser
Browse files Browse the repository at this point in the history
  • Loading branch information
tordans committed Jan 18, 2023
1 parent ac658fa commit 71c80a9
Show file tree
Hide file tree
Showing 20 changed files with 15,111 additions and 10,944 deletions.
25,574 changes: 14,667 additions & 10,907 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,21 @@
"@hookform/error-message": "2.0.1",
"@hookform/resolvers": "2.9.10",
"@prisma/client": "4.6.0",
"@turf/turf": "6.5.0",
"autoprefixer": "10.4.13",
"blitz": "2.0.0-beta.20",
"clsx": "1.2.1",
"date-fns": "2.29.3",
"mapbox-gl": "2.12.0",
"maplibre-gl": "2.4.0",
"next": "12.2.5",
"postcss": "8.4.20",
"prisma": "4.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.41.0",
"react-intl": "6.2.5",
"react-map-gl": "7.0.21",
"react-remark": "2.1.0",
"tailwindcss": "3.2.4",
"zod": "3.20.2"
Expand Down
43 changes: 9 additions & 34 deletions src/pages/[projectSlug]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,34 @@
import { Routes, useParam } from "@blitzjs/next"
import { usePaginatedQuery, useQuery } from "@blitzjs/rpc"
import { useRouter } from "next/router"
import { useQuery } from "@blitzjs/rpc"
import { Suspense } from "react"
import { Link } from "src/core/components/links"
import { PageHeader } from "src/core/components/PageHeader"
import { Pagination } from "src/core/components/Pagination"
import { LayoutArticle, MetaTags } from "src/core/layouts"
import { BaseMapSections, SectionsMap } from "src/projects/components/Map"
import { SectionsTeasers } from "src/projects/components/Map/SectionsTeaser/SectionsTeasers"
import getProject from "src/projects/queries/getProject"
import getSections from "src/sections/queries/getSections"

const ITEMS_PER_PAGE = 100

export const ProjectDashboardWithQuery = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
const projectSlug = useParam("projectSlug", "string")
const [project] = useQuery(getProject, { slug: projectSlug })
const [{ sections, hasMore }] = usePaginatedQuery(getSections, {
const [{ sections }] = useQuery(getSections, {
where: { project: { slug: projectSlug! } },
orderBy: { id: "asc" },
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
include: { subsections: { select: { id: true, geometry: true } } },
})

const goToPreviousPage = () => router.push({ query: { page: page - 1 } })
const goToNextPage = () => router.push({ query: { page: page + 1 } })
if (!sections.length) return null

return (
<>
<MetaTags noindex title={project.name} />
<PageHeader title={project.name} />

<h2>Alle Teilstrecken</h2>
<ul>
{sections.map((section) => (
<li key={section.id}>
<Link
href={Routes.SectionDashboardPage({
projectSlug: projectSlug!,
sectionSlug: section.slug,
})}
>
{section.name}
</Link>
</li>
))}
</ul>
<Pagination
hasMore={hasMore}
page={page}
handlePrev={goToPreviousPage}
handleNext={goToNextPage}
/>

<SectionsMap sections={sections as BaseMapSections} />
<SectionsTeasers sections={sections} />

<h2>Kommende Termine</h2>
<code>todo</code>
Expand All @@ -69,8 +46,6 @@ export const ProjectDashboardWithQuery = () => {
}

const ProjectDashboardPage = () => {
const projectSlug = useParam("projectSlug", "string")

return (
<LayoutArticle>
<Suspense fallback={<div>Daten werden geladen…</div>}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Fragment } from "react"
import { Listbox, Transition } from "@headlessui/react"
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"
import clsx from "clsx"

export type LayerType = "vector" | "satellite"

const labels: { [index: string]: string } = {
vector: "Kartenlayer einblenden",
satellite: "Satellitenlayer einblenden",
}

type Props = {
value: LayerType
onChange: (_: LayerType) => void
className: string
}

export const BackgroundSwitcher: React.FC<Props> = ({ value, onChange, className }) => {
return (
<div className={className}>
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<div className="relative mt-1">
<Listbox.Button className="relative cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<span className="block truncate">{labels[value]}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>

<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{Object.keys(labels).map((id) => (
<Listbox.Option
key={id}
className={({ active }) =>
clsx(
active ? "bg-indigo-600 text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
)
}
value={id}
>
{({ selected, active }) => (
<>
<span
className={clsx(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{labels[id]}
</span>

{selected ? (
<span
className={clsx(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
</div>
)
}
1 change: 1 addition & 0 deletions src/projects/components/Map/BackgroundSwitcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./BackgroundSwitcher"
178 changes: 178 additions & 0 deletions src/projects/components/Map/BaseMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Routes, useParam } from "@blitzjs/next"
import { Section, Subsection } from "@prisma/client"
import { lineString } from "@turf/helpers"
import clsx from "clsx"
import maplibregl from "maplibre-gl"
import "maplibre-gl/dist/maplibre-gl.css"
import { useRouter } from "next/router"
import React, { useState } from "react"
import Map, { Layer, NavigationControl, ScaleControl, Source } from "react-map-gl"
import { BackgroundSwitcher, LayerType } from "./BackgroundSwitcher/BackgroundSwitcher"
import { sectionBbox, geometryStartEndPoint } from "./utils"

export type BaseMapSections = (Section & {
subsections: Pick<Subsection, "id" | "geometry">[]
})[]

type Props = {
children?: React.ReactNode
sections: BaseMapSections
selectedSection?: Section
className?: string
isInteractive?: boolean
}

export const BaseMap: React.FC<Props> = ({
children,
sections,
selectedSection,
className,
isInteractive = true,
}) => {
const router = useRouter()
const projectSlug = useParam("projectSlug", "string")

const [hoveredSectionIds, setHoveredSectionIds] = useState<number[]>([])

const [selectedLayer, setSelectedLayer] = useState<LayerType>("vector")
const handleLayerSwitch = (layer: LayerType) => {
setSelectedLayer(layer)
}

const maptilerApiKey = "ECOoUBmpqklzSCASXxcu"
const vectorStyle = `https://api.maptiler.com/maps/a4824657-3edd-4fbd-925e-1af40ab06e9c/style.json?key=${maptilerApiKey}`
const satelliteStyle = `https://api.maptiler.com/maps/hybrid/style.json?key=${maptilerApiKey}`

// Layer style for segments depending on selected section and segment (if
// enableHover is true).
type PickLineColor = {
section: Section
}
const pickLineColor = ({ section }: PickLineColor) => {
let lineColor = "#eab308"

if (hoveredSectionIds.includes(section.id)) {
lineColor = "#e6007d"
}

if (selectedSection && section.id === selectedSection.id) {
lineColor = "#0a64ae"
}

return lineColor
}

const handleClick = async (e: mapboxgl.MapLayerMouseEvent) => {
if (!isInteractive) return
const sectionSlug = e?.features?.[0]?.properties?.sectionSlug
if (sectionSlug) {
await router.push(Routes.SectionDashboardPage({ projectSlug: projectSlug!, sectionSlug }))
}
}

const interactiveLayerIds = sections
.map((s) => s.subsections.map((ss) => `layer_${ss.id}`))
.flat()
// TODO re-evaluate previous code:
// selectedSection?.subSegments
// .filter((segment) => segment !== selectedSegment)
// .map((segment) => String(segment.id)) || [];

const [cursorStyle, setCursorStyle] = useState("grab")
const handleMouseEnter = (e: mapboxgl.MapLayerMouseEvent) => {
if (!isInteractive) return
setCursorStyle("pointer")
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO find out why TS is unhappy
setHoveredSectionIds(e.features.map((f) => f?.properties?.sectionId))
}
const handleMouseLeave = () => {
if (!isInteractive) return
setCursorStyle("grab")
}

const [minX, minY, maxX, maxY] = sectionBbox(sections)

if (!minX || !minY || !maxX || !maxY) return null

return (
<div className={clsx(className, "relative h-full w-full")}>
<Map
mapLib={maplibregl}
initialViewState={{
bounds: [minX, minY, maxX, maxY],
fitBoundsOptions: {
padding: 60,
},
}}
scrollZoom={false}
mapStyle={selectedLayer === "vector" ? vectorStyle : satelliteStyle}
onClick={handleClick}
cursor={cursorStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
interactiveLayerIds={interactiveLayerIds}
>
{children}

{sections.map((section) => {
return section.subsections.map((subsection) => {
return (
<Source
key={subsection.id}
type="geojson"
data={lineString(JSON.parse(subsection.geometry), {
sectionSlug: section.slug,
sectionId: section.id,
})}
>
<Layer
id={`layer_${subsection.id}`}
type="line"
paint={{
"line-width": 7,
"line-color": pickLineColor({
section,
}),
}}
/>
</Source>
)
})
})}

{sections
// TODO re-evaluate this old code; I think we don't need this…
// .filter((section) => section === selectedSection)
.map((section) => {
return section.subsections.map((subsection) => (
<Source
key={`layer_dots_${subsection.id}`}
type="geojson"
data={geometryStartEndPoint(subsection.geometry)}
>
<Layer
id={`layer_dots_${subsection.id}`}
type="circle"
paint={{
"circle-color": pickLineColor({
section,
}),
"circle-radius": 6,
}}
/>
</Source>
))
})}

<NavigationControl showCompass={false} />
<ScaleControl />
</Map>
<BackgroundSwitcher
className="absolute top-4 left-4"
value={selectedLayer}
onChange={handleLayerSwitch}
/>
</div>
)
}
7 changes: 7 additions & 0 deletions src/projects/components/Map/SectionMarker/SectionMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const SectionMarker: React.FC<{ number: number }> = ({ number }) => {
return (
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-full border-2 border-rsv-ochre bg-white pt-0.5 font-sans text-xl font-bold leading-none">
{number}
</div>
)
}
1 change: 1 addition & 0 deletions src/projects/components/Map/SectionMarker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./SectionMarker"
24 changes: 24 additions & 0 deletions src/projects/components/Map/SectionPanel/SectionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Section } from "@prisma/client"
import clsx from "clsx"

type Props = {
section: Section
className?: string
}

export const SectionPanel: React.FC<Props> = ({ section, className }) => {
return (
<div className={clsx(className, "overflow-hidden rounded-md bg-white drop-shadow-sm")}>
<div className="overflow-auto border-t-[10px] border-[#979797] py-4 px-2">
<h2 className="my-4 text-center text-[30px] font-semibold leading-[36px]">
Abschnitt {section.id}
</h2>
<p className="mb-4">
<strong>{section.name}</strong>
</p>
{/* <p>Status: {section.status}</p>
<p>Segments: {section.subsections.length}</p> */}
</div>
</div>
)
}
1 change: 1 addition & 0 deletions src/projects/components/Map/SectionPanel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./SectionPanel"

0 comments on commit 71c80a9

Please sign in to comment.