Skip to content

Commit

Permalink
Merge pull request #177 from kieler/175-frontend-improve-user-navigation
Browse files Browse the repository at this point in the history
Improve user navigation on the frontend
  • Loading branch information
Kebslock authored Sep 20, 2023
2 parents 281434c + 510c6dc commit 9d8f70c
Show file tree
Hide file tree
Showing 19 changed files with 194 additions and 48 deletions.
11 changes: 11 additions & 0 deletions Website/src/app/@modal/(.)select_track/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { SelectionDialog } from "@/app/components/track_selection";

/**
* A modal track selection dialog that will be overlayed over the rest of the page
*/
export default function SelectTrackModal() {

return <SelectionDialog modal={true} />;
}
6 changes: 6 additions & 0 deletions Website/src/app/@modal/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* By default, there is no modal present
*/
export default function Default() {
return null;
}
4 changes: 2 additions & 2 deletions Website/src/app/components/base_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./globals.css";
import Header from "@/app/components/header";
import Footer from "@/app/components/footer";
import Header from "@/app/components/layout/header";
import Footer from "@/app/components/layout/footer";
import React from "react";

/**
Expand Down
14 changes: 9 additions & 5 deletions Website/src/app/components/dynlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TrackerCharge from "@/app/components/tracker";
import { FunctionComponent } from "react";
import { getFetcher } from "@/utils/fetcher";
import { useRouter } from "next/navigation";
import { SelectTrackButton } from "@/app/components/selectTrackButton";

/**
* A component to focus a vehicle. A link to the map view with the respective search parameter
Expand Down Expand Up @@ -77,7 +78,7 @@ export function VehicleList({
</td>
)}
<td className={"px-2 text-center"}>
<div className={"max-w-[16rem] mx-auto"}>
<div className={"max-w-[16rem] w-fit mx-auto"}>
{v.trackerIds.map(trackerId => (
<TrackerCharge key={trackerId} trackerId={trackerId} />
))}
Expand Down Expand Up @@ -127,7 +128,7 @@ export default function DynamicList({
const sorted_vehicles = vehicles?.sort((a, b) => a.id - b.id);

// obtain the NextJS router
const router = useRouter()
const router = useRouter();

if (logged_in && error) {
if (error instanceof UnauthorizedError || (error instanceof RevalidateError && error.statusCode === 401)) {
Expand All @@ -139,9 +140,12 @@ export default function DynamicList({

return (
<>
<h2>
Fahrzeuge der Strecke {track_data?.start} - {track_data?.end}
</h2>
<div className={"flex flex-wrap justify-between items-center gap-2 mb-2"}>
<h2 className={"text-xl text-left"}>
Fahrzeuge der Strecke {track_data?.start} - {track_data?.end}
</h2>
<SelectTrackButton />
</div>
<VehicleList sorted_vehicles={sorted_vehicles} FocusVehicle={FocusVehicle} />
</>
);
Expand Down
5 changes: 5 additions & 0 deletions Website/src/app/components/dynmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import useSWR from "swr";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { getFetcher } from "@/utils/fetcher";
import { SelectTrackButton } from "@/app/components/selectTrackButton";

// This complicated thing with `dynamic` is necessary to disable server side rendering
// for the actual map, which does not work with leaflet.
Expand Down Expand Up @@ -76,6 +77,10 @@ export default function DynamicMap({
setFocus
}}
/>
{/* This will stack over the map, if all map layers have a z-index < 1100 (which should be the default) */}
<div className={"absolute left-5 bottom-5 z-1100"}>
<SelectTrackButton />
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion Website/src/app/components/dynmap_with_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function DynamicMapList({
}}
/>
</div>
<div className={"basis-30 flex flex-col gap-2 mr-2"}>
<div className={"basis-32 flex grow-0 flex-col gap-2 mr-2"}>
<div className={"grow overflow-y-auto basis-0"}>
<VehicleList
sorted_vehicles={sorted_vehicles}
Expand Down
16 changes: 7 additions & 9 deletions Website/src/app/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ import React from "react";
* children of this element.
* @constructor
*/
export function FormWrapper({children}: { children: React.ReactNode }) {
return (
<main className="mx-auto max-w-2xl w-full grow">
<div className={'bg-white dark:bg-slate-800 dark:text-white p-4 rounded'}>
{children}
</div>
</main>
)
export function FormWrapper({ children }: { children: React.ReactNode }) {
return (
<main className="mx-auto mt-2 max-w-2xl w-full 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.
// the LoginDialog and SelectionDialog components.
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
"use client";

import Link from "next/link";
import { useContext } from "react";
import { UsernameContext } from "@/app/components/username-provider";
import Link from "next/link";

/**
* The header for the web page
* A component showing the name of the currently logged-in user, with a logout link,
* or a login link if the user is not logged-in.
*/
export default function Header() {
export function CurrentUser({ className }: { className?: string }) {
const username = useContext(UsernameContext);

return (
<header className={"flex flex-row w-full flex-initial justify-items-center p-2"}>
<div>
<Link href={"/"}>RailTrail Admin interface</Link>
</div>
<div className={"grow"} />
<>
{username ? (
<div>
<div className={className}>
Hello {username} &ndash;{" "}
<Link href={"/logout"} prefetch={false}>
Logout
</Link>
</div>
) : (
<div>
<div className={className}>
<Link href={"/login"}>Login</Link>
</div>
)}
</header>
</>
);
}
File renamed without changes.
30 changes: 30 additions & 0 deletions Website/src/app/components/layout/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Link from "next/link";
import { CurrentUser } from "@/app/components/layout/currentUser";
import SmartNavigation from "@/app/components/layout/smart_navigation";

/**
* The header for the web page
*/
export default function Header() {
return (
<header
className={
"flex flex-row w-full items-center justify-around p-2 flex-wrap bg-white dark:bg-slate-900 gap-2"
}>
<div className={"mr-auto"}>
<Link href={"/"}>RailTrail Verwaltung</Link>
</div>
<CurrentUser className={"ml-auto md:order-last"} />
{/* Force a line break for small devices */}
<div className={"w-full md:hidden"} />

<SmartNavigation href={"/map"}>Karte</SmartNavigation>
<SmartNavigation href={"/list"}>Liste</SmartNavigation>
<SmartNavigation href={"/mapList"} className={"px-2 border-2 border-transparent hidden sm:block"}>
Karte + Liste
</SmartNavigation>

<SmartNavigation href={"/management"}>Datenverwaltung</SmartNavigation>
</header>
);
}
39 changes: 39 additions & 0 deletions Website/src/app/components/layout/smart_navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import Link, { LinkProps } from "next/link";
import { usePathname } from "next/navigation";
import { PropsWithChildren } from "react";

/**
* A navigation link that has a different style when the user is on the page it links to, or on a sub-page
* @param href The URL to navigate to
* @param className CSS classes for the enclosing div
* @param activeClassName Additional CSS classes for the enclosing div, when active
* @param linkClassName The CSS classes for the anchor tag
* @param children The contents in the anchor tag
* @param props Other options applicable to <Link>
* @constructor
*/
export default function SmartNavigation({
href,
className = "px-2 border-2 border-transparent",
linkClassName,
children,
...props
}: PropsWithChildren<LinkProps & { href: string; className?: string; linkClassName?: string }>) {
// get the path of the currently open page
const currentPath = usePathname();

// and determine if we are currently on that path
const active = (currentPath === href || currentPath?.startsWith(href + "/")) ?? false;

const activeClassName = "bg-neutral-500/20 !border-gray-500 rounded";

return (
<div className={className + (active ? " " + activeClassName : "")}>
<Link href={href} className={linkClassName} {...props}>
{children}
</Link>
</div>
);
}
2 changes: 1 addition & 1 deletion Website/src/app/components/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useRouter } from "next/navigation";
import { FormEventHandler, PropsWithChildren, Suspense, useEffect, useRef, useState } from "react";

import Footer from "@/app/components/footer";
import Footer from "@/app/components/layout/footer";
import { ErrorMessage } from "@/app/management/components/errorMessage";

/**
Expand Down
14 changes: 14 additions & 0 deletions Website/src/app/components/selectTrackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Link from "next/link";

/**
* A link that somewhat resembles a button to select a different track.
*/
export function SelectTrackButton() {
return (
<Link
href={"/select_track"}
className={"bg-gray-200 dark:bg-slate-600 border-2 border-gray-500 rounded px-2 py-1 no-a-style"}>
Andere Strecke wählen
</Link>
);
}
34 changes: 25 additions & 9 deletions Website/src/app/components/track_selection.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"use client";

import { Dispatch, FormEventHandler, PropsWithChildren, useEffect, useRef, useState } from "react";

import Footer from "@/app/components/footer";
import Footer from "@/app/components/layout/footer";
import useSWR from "swr";
import { setCookie } from "cookies-next";
import { getCookie, setCookie } from "cookies-next";
import { inter } from "@/utils/common";
import { getFetcher } from "@/utils/fetcher";
import { useRouter } from "next/navigation";
Expand All @@ -24,6 +22,7 @@ export default function Selection({
const { data, error, isLoading } = useSWR("/webapi/tracks/list", getFetcher<"/webapi/tracks/list">);
// get the next page router
const router = useRouter();
const selectedTrack = getCookie("track_id")?.toString();

const selectTrack: FormEventHandler = e => {
e.preventDefault();
Expand All @@ -41,7 +40,7 @@ export default function Selection({
};

return (
<form onSubmit={selectTrack} className="grid grid-cols-2 gap-y-1 my-1.5 items-center h-24">
<form onSubmit={selectTrack} className="grid grid-cols-[1fr, 7fr] gap-y-1 my-1.5 items-center h-24">
{isLoading ? (
<div className={"flex col-span-2 justify-center items-center gap-5"}>
<Spinner className={"h-10 w-auto"} />
Expand All @@ -52,14 +51,18 @@ export default function Selection({
) : completed ? (
<div className={"flex col-span-2 justify-center items-center gap-5"}>
<Spinner className={"h-10 w-auto"} />
<div>Wird gepeichert...</div>
<div>Wird gespeichert...</div>
</div>
) : (
<>
<label className={""} htmlFor="track">
Strecke:{" "}
</label>
<select id={"track"} name={"track"} className="dark:bg-slate-700 rounded">
<select
defaultValue={selectedTrack}
id={"track"}
name={"track"}
className="dark:bg-slate-700 rounded">
{data?.map(({ id, start, end }) => (
<option
value={id}
Expand All @@ -80,10 +83,12 @@ export default function Selection({

/**
* The track selection form wrapped in a dialog, for easy display in a modal way.
* @param children HTML elements to display over the login form in the dialog, for example for explanations.
* @param children HTML elements to display over the login form in the dialog, for example for explanations.
* @param modal Whether this is shown as part of a modal route.
*/
export function SelectionDialog({ children }: PropsWithChildren) {
export function SelectionDialog({ children, modal = false }: PropsWithChildren<{ modal?: boolean }>) {
const dialogRef = useRef(null as HTMLDialogElement | null);
const router = useRouter();

// get a "completed" state
const [completed, setCompleted] = useState(false);
Expand All @@ -94,10 +99,21 @@ export function SelectionDialog({ children }: PropsWithChildren) {
}
}, []);

// if this is a modal, we need to move back to the previous page using the router
useEffect(() => {
if (completed && modal) {
router.back();
}
}, [completed, modal, router]);

return (
<dialog
ref={dialogRef}
onCancel={event => {
if (modal) {
// if this is a modal, we need to move back to the previous page using the router
router.back();
}
event.preventDefault();
}}
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">
Expand Down
8 changes: 4 additions & 4 deletions Website/src/app/components/tracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ export default function TrackerCharge({ trackerId }: { trackerId: string }) {
return (
<>
{tracker_data && (
<div className={"w-full flex flex-nowrap my-1 gap-1"}>
<div className={"group relative sm:grow shrink min-w-0 basis-32 text-left"}>
<div className={"truncate w-32 sm:w-full max-w-full min-w-0"}>{tracker_data.id}</div>
<div className={"flex flex-nowrap my-1 gap-1 min-w-0 w-32 sm:w-44 md:w-52"}>
<div className={"group relative grow-0 md:grow shrink min-w-0 basis-30 lg:basis-32 text-left"}>
<div className={"truncate basis-32 sm:w-full max-w-full min-w-0"}>{tracker_data.id}</div>
<div
className={
"opacity-0 group-hover:opacity-100 z-10 transition-opacity pointer-events-none absolute dark:bg-gray-900 dark:text-white bg-gray-100 rounded py-2 px-3 top-8 -left-3 w-max"
}>
{tracker_data.id}
</div>
</div>
<div className={"basis-10 text-right shrink-0"}>
<div className={"basis-14 text-right shrink-0"}>
{tracker_data.battery == undefined ? "?" : batteryLevelFormatter.format(tracker_data.battery)}
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion Website/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const metadata = meta_info;
* 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 }) {
export default function RootLayout({ children, modal }: { children: React.ReactNode; modal: React.ReactNode }) {
const token = cookies().get("token")?.value;
const username = token ? inlineTry(() => getUsername(token)) : undefined;

Expand All @@ -20,6 +20,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<body className={inter.className}>
<UsernameProvider username={username}>
<BaseLayout>{children}</BaseLayout>
{
/* Add any modals beneath the page layout. They will need to layer themselves over the content. */
modal
}
</UsernameProvider>
</body>
</html>
Expand Down
2 changes: 1 addition & 1 deletion Website/src/app/list/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default async function Home() {
};

return (
<main className="mx-auto w-full max-w-4xl grow">
<main className="mx-auto w-full max-w-4xl grow mt-2">
<div className={"bg-white dark:bg-slate-800 dark:text-white p-4 rounded"}>
<LoginWrapper
logged_in={token !== undefined}
Expand Down
Loading

0 comments on commit 9d8f70c

Please sign in to comment.