Skip to content

Commit

Permalink
refactored and commented some components
Browse files Browse the repository at this point in the history
(cherry picked from commit d7f7c4c)
  • Loading branch information
n1kPLV committed Jul 31, 2023
1 parent 97dc50c commit f733a63
Show file tree
Hide file tree
Showing 40 changed files with 398 additions and 288 deletions.
32 changes: 32 additions & 0 deletions Website/src/app/components/base_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import './globals.css'
import Header from "@/app/components/header";
import Footer from "@/app/components/footer";

/**
* The general layout for this site to be used both for pages
* using the app-router and the legacy pages-router.
*
* This will ensure that each page has a header and a footer.
*
* Because this is a flex-layout, children can use the `grow`
* class to grow to take up the remaining space on the page.
*
* @param children The actual page this layout should wrap. In JSX, these are the
* children of this element.
* @constructor
*/
export default function BaseLayout({
children,
}: {
children: React.ReactNode
}) {
return (
// inline styling using tailwind will keep styling information in the same
// file as the markup information.
<div className='h-full min-h-screen flex flex-initial flex-col'>
<Header/>
{children}
<Footer/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";
import dynamic from 'next/dynamic';
import LoadMapScreen from './loadmap';
import {Vehicle} from "@/lib/api.website";
import {IMapRefreshConfig, RevalidateError} from '@/lib/types';
import {Vehicle} from "@/utils/api.website";
import {IMapRefreshConfig, RevalidateError} from '@/utils/types';
import useSWR from "swr";

const _internal_DynamicMap = dynamic(() => import('@/components/map'), {
const _internal_DynamicMap = dynamic(() => import('@/app/components/map'), {
loading: LoadMapScreen,
ssr: false
});
Expand Down
21 changes: 21 additions & 0 deletions Website/src/app/components/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";

/**
* A component that wraps any Form, giving them a consistent appearance.
*
* @param children The actual page this layout should wrap. In JSX, these are the
* children of this element.
* @constructor
*/
export function FormWrapper({children}: { children: React.ReactNode }) {
return (
<main className="mx-auto max-w-2xl grow">
<div className={'bg-white dark:bg-slate-800 dark:text-white p-4 rounded'}>
{children}
</div>
</main>
)
}

// TODO: create a component for a form in a dialog to replace/refactor
// the LoginDialog and SelectionDialog components.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client"
import {IMapRefreshConfig} from "@/lib/types";
import {IMapRefreshConfig} from "@/utils/types";
import {useState} from "react";
import {LoginDialog} from "@/components/login";
import {SelectionDialog} from "@/components/track_selection";
import {LoginDialog} from "@/app/components/login";
import {SelectionDialog} from "@/app/components/track_selection";

const LoginWrapper = ({logged_in, track_selected, map_conf, child}: {logged_in: boolean, track_selected: boolean, map_conf: IMapRefreshConfig, child: (conf: IMapRefreshConfig) => JSX.Element}) => {
const [loginState, setLogin] = useState(logged_in);
Expand Down
165 changes: 165 additions & 0 deletions Website/src/app/components/map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"use client";
import L from "leaflet"
import "leaflet-rotatedmarker"
import 'leaflet/dist/leaflet.css'
import {useEffect, useMemo, useRef, useState} from "react";
import {IMapConfig} from '@/utils/types'
import {batteryLevelFormatter, coordinateFormatter} from "@/utils/helpers";
import assert from "assert";
import {createPortal} from "react-dom";

function Map(props: IMapConfig) {

// console.log('props', props);

const {position: initial_position, zoom_level, server_vehicles: vehicles, init_data, focus: initial_focus} = props;

const mapRef = useRef(undefined as L.Map | undefined);
const markerRef = useRef([] as L.Marker[])
const mapContainerRef = useRef(null as HTMLDivElement | null)
const [position, setPosition] = useState(initial_position)
const [focus, setFocus] = useState(initial_focus);
const [popupContainer, setPopupContainer] = useState(undefined as undefined | HTMLDivElement);
const markerIcon = useMemo(() => new L.Icon({
iconUrl: "generic_rail_bound_vehicle.svg",
iconSize: L.point(45, 45)
}), []);

const vehicleInFocus = vehicles.find((v) => v.id == focus);

// debugger;

function insertMap() {
// debugger;
// console.log(mapRef, mapRef.current);
assert(mapContainerRef.current, "Error: Ref to Map Container not populated");
assert(mapRef.current == undefined, "Error: Trying to insert map more than once");
mapRef.current = L.map(mapContainerRef.current);

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(mapRef.current);

/*const openrailwaymap = L.tileLayer('http://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
{
attribution: '<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</a>, Style: <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA 2.0</a> <a href="http://www.openrailwaymap.org/">OpenRailwayMap</a> and OpenStreetMap',
minZoom: 2,
maxZoom: 19,
tileSize: 256
}).addTo(mapRef.current);*/

}

function setMapZoom() {
assert(mapRef.current != undefined, "Error: Map not ready!");

mapRef.current.setZoom(zoom_level);
}

function setMapPosition() {
assert(mapRef.current != undefined, "Error: Map not ready!");
assert(!Number.isNaN(mapRef.current?.getZoom()), "Error: ZoomLevel MUST be set before position is set!")

mapRef.current.setView(position);
}


function addTrackPath() {
assert(mapRef.current != undefined, "Error: Map not ready!");

const trackPath = L.geoJSON(init_data?.trackPath, {style: {color: 'red'}})
trackPath.addTo(mapRef.current)

// Add a callback to remove the track path to remove the track path in case of a re-render.
return () => {
trackPath.remove();
}
}

function updateMarkers() {

assert(mapRef.current != undefined, "Error: Map not ready!");

while (markerRef.current.length > vehicles.length) {
const m = markerRef.current.pop()
if (m) {
m.remove()
} else {
break;
}
}
vehicles.forEach((v, i) => {
if (i >= markerRef.current.length) {
if (mapRef.current) {
const m = L.marker(vehicles[i].pos, {
icon: markerIcon,
rotationOrigin: "center"
}).addTo(mapRef.current);
markerRef.current.push(m);
}
}
const m = markerRef.current[i];
m.setLatLng(vehicles[i].pos)
// m.setPopupContent(popupContent(vehicles[i]))
m.setRotationAngle(vehicles[i].heading || 0)

if (v.id === focus) {
const current_popup = m.getPopup()
if (current_popup == undefined) {
// create a div element to contain the popup content.
// We can then use a React portal to place content in there.
const popupElement = document.createElement('div');
popupElement.className = "w-64 flex p-1.5 flex-row flex-wrap";
m.bindPopup(popupElement);
setPopupContainer(popupElement);
// unset the focussed element on popup closing.
m.on('popupclose', () => {
setFocus(undefined)
})
}
m.openPopup();
setPosition(vehicles[i].pos);
} else {
m.closePopup();
m.unbindPopup();
}
m.on('click', () => {
// set the vehicle as the focussed vehicle if it is clicked.
setFocus(v.id)
})

}
)
}

useEffect(insertMap, []);
useEffect(setMapZoom, [zoom_level]);
useEffect(setMapPosition, [position]);
useEffect(addTrackPath, [init_data?.trackPath]);
useEffect(updateMarkers, [focus, markerIcon, vehicles]);

return (
<>
<div id='map' className="h-full" ref={mapContainerRef}/>
{popupContainer && createPortal(vehicleInFocus ?
<>
<h4 className={'col-span-2 basis-full text-xl text-center'}>Vehicle &quot;{vehicleInFocus?.name}&quot;</h4>
<div className={'basis-1/2'}>Tracker-Level:</div>
<div
className={'basis-1/2'}>{vehicleInFocus ? batteryLevelFormatter.format(vehicleInFocus.batteryLevel) : 'unbekannt'}</div>
<div className={'basis-1/2'}>Position:</div>
<div
className={'basis-1/2'}>{
vehicleInFocus
? <>{coordinateFormatter.format(vehicleInFocus?.pos.lat)} N {coordinateFormatter.format(vehicleInFocus?.pos.lng)} E</>
: 'unbekannt'}
</div>
</> : <div/>, popupContainer
)}
</>
);
}


export default Map
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

import {FormEventHandler, useEffect, useRef} from "react";

import { UrlObject } from 'url';
import Footer from "@/app/components/footer";
import {RevalidateError} from "@/lib/types";
import {RevalidateError} from "@/utils/types";
import useSWR from "swr";
import {TrackList} from "@/lib/api.website";
import {TrackList} from "@/utils/api.website";
import {setCookie} from "cookies-next";
type Url = string | UrlObject;
import {inter} from "@/utils/common";

const selectTrack: FormEventHandler = (e) => {
e.preventDefault()
Expand All @@ -34,37 +33,37 @@ const fetcher = (url: string) => fetch(url).then(
}
).then(res => res.json());

export default function Selection({dst_url}: {dst_url?: Url}) {
export default function Selection() {
// @type data TrackList
const {data, error, isLoading}: {data: TrackList, error?: any, isLoading: boolean} = useSWR('/webapi/tracks/list', fetcher);

return (
<form onSubmit={selectTrack} className="grid grid-cols-2 gap-y-1 mx-1.5 items-center">
<form onSubmit={selectTrack} className="grid grid-cols-2 gap-y-1 my-1.5 items-center">
{isLoading ? <p> Lädt... </p> : (error ? <p> {error.toString()} </p> : (<>
<label htmlFor="track">Strecke: </label>
<select id={'track'} name={'track'}>
{data.map(({id, name}) => (<option value={id} key={id}>{name}</option>))}
<label className={''} htmlFor="track">Strecke: </label>
<select id={'track'} name={'track'} className="dark:bg-slate-700 rounded">
{data.map(({id, name}) => (<option value={id} key={id} className={`dark:bg-slate-700 dark:text-white ${inter.className}`}>{name}</option>))}
</select>
<button type="submit" className="col-span-2 rounded-full bg-gray-700 text-white">Auswählen</button>
</>))}
</form>
)
}

export function SelectionDialog({dst_url, login_callback, children}: React.PropsWithChildren<{dst_url?: Url, login_callback?: (success: boolean) => void}>) {
export function SelectionDialog({children}: React.PropsWithChildren<{}>) {
const dialogRef = useRef(null as HTMLDialogElement | null)

useEffect(() => {
if (!dialogRef.current?.open) {
dialogRef.current?.showModal();
}
})
}, []);

return (<dialog ref={dialogRef} onCancel={(event) => {
event.preventDefault();
}} className="drop-shadow-xl shadow-black backdrop:bg-gray-200/30 backdrop:backdrop-blur" >
}} className="drop-shadow-xl shadow-black bg-white p-4 rounded max-w-2xl w-full dark:bg-slate-800 dark:text-white backdrop:bg-gray-200/30 backdrop:backdrop-blur" >
{children}
<Selection dst_url={dst_url} />
<Selection />
<Footer />
</dialog>)

Expand Down
24 changes: 18 additions & 6 deletions Website/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import RootLayout from "@/components/layout"
import BaseLayout from "@/app/components/base_layout"
import {inter, meta_info} from "@/utils/common";

export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export const metadata = meta_info;

export default RootLayout;
/**
* The Layout to use on all pages in the app-directory.
* Effectively defers to BaseLayout with minimal adjustments.
*/
export default function RootLayout({children,}: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<BaseLayout>
{children}
</BaseLayout>
</body>
</html>
)
}


19 changes: 8 additions & 11 deletions Website/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import Login from "@/components/login";
import Login from "@/app/components/login";
import {FormWrapper} from "@/app/components/form";


export default function Home(x: any) {
export default function LoginPage() {

return (
// <div className='h-full min-h-screen'>
<main className="container mx-auto max-w-2xl grow">
<div className={'bg-white p-4 rounded'}>
<Login/>
</div>
</main>
//</div>
<FormWrapper>
<Login/>
</FormWrapper>
)
}
}
2 changes: 1 addition & 1 deletion Website/src/app/management/add_track/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import {FormEvent, useRef, useState} from "react";
import {TrackPath} from "@/lib/api.website";
import {TrackPath} from "@/utils/api.website";

export default function Home() {
const formRef = useRef(null as (null | HTMLFormElement))
Expand Down
10 changes: 5 additions & 5 deletions Website/src/app/management/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {FormWrapper} from "@/app/components/form";

export default function Layout({children,}: { children: React.ReactNode }) {
return (
<main className="container mx-auto max-w-2xl grow">
<div className={'bg-white dark:bg-slate-800 dark:text-white p-4 rounded'}>
{children}
</div>
</main>
<FormWrapper>
{children}
</FormWrapper>
);
}
Loading

0 comments on commit f733a63

Please sign in to comment.