Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions frontend/src/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Anchor } from "@mantine/core";
import { Anchor, AnchorProps } from "@mantine/core";
import { useRemails } from "./hooks/useRemails.ts";
import { RouteParams } from "./router.ts";
import { RouteName } from "./routes.ts";
Expand All @@ -8,18 +8,23 @@ interface LinkProps {
params?: RouteParams;
underline?: "always" | "hover" | "never";
children: React.ReactNode;
style?: AnchorProps;
}

export function Link({ to, params, underline, children }: LinkProps) {
const { navigate } = useRemails();
export function Link({ to, params, underline, children, style }: LinkProps) {
const { navigate, routeToPath } = useRemails();

const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (e.defaultPrevented || e.ctrlKey || e.metaKey) {
return;
}

e.preventDefault();
navigate(to, params);
};

return (
<Anchor onClick={onClick} underline={underline || "always"}>
<Anchor href={routeToPath(to, params)} onClick={onClick} underline={underline || "always"} {...style}>
{children}
</Anchor>
);
Expand Down
27 changes: 11 additions & 16 deletions frontend/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,26 @@ interface LoginProps {
setUser: (user: User | null) => void;
}

type LoginType = "login" | "register" | "reset_password";

const BUTTON_NAMES: { [type in LoginType]: string } = {
login: "Login",
register: "Register",
reset_password: "Reset password",
};

export default function Login({ setUser }: LoginProps) {
const {
state: { routerState },
navigate,
redirect,
} = useRemails();

let type: "login" | "register" | "reset_password" = "login";
let buttonName = "Login";

switch (routerState.params.type) {
case "register":
type = "register";
buttonName = "Register";
break;
case "reset_password":
type = "reset_password";
buttonName = "Reset password";
break;
}
const type = routerState.params.type in BUTTON_NAMES ? (routerState.params.type as LoginType) : "login";
const buttonName = BUTTON_NAMES[type];

const [globalError, setGlobalError] = useState<string | null>(null);

const xIcon = <IconX size={20} />;

const form = useForm({
initialValues: {
email: "",
Expand Down Expand Up @@ -193,7 +188,7 @@ export default function Login({ setUser }: LoginProps) {
/>
)}

{globalError && <Alert icon={xIcon}>{globalError}</Alert>}
{globalError && <Alert icon={<IconX size={20} />}>{globalError}</Alert>}
</Stack>

<Group justify="space-between" mt="xl">
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/components/EditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { RouteName } from "../routes.ts";
import { RouteParams } from "../router.ts";

export default function EditButton({ route, params }: { route: RouteName; params: RouteParams }) {
const { navigate } = useRemails();
const { navigate, routeToPath } = useRemails();

return (
<Button
variant="subtle"
onClick={() => {
component="a"
href={routeToPath(route, params)}
onClick={(e) => {
e.preventDefault();
navigate(route, params);
}}
>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/admin/OrganizationsOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function OrganizationsOverview() {

const {
state: { config },
routeToPath,
} = useRemails();
const { navigate } = useRemails();

Expand Down Expand Up @@ -66,7 +67,10 @@ export default function OrganizationsOverview() {
<ActionIcon
size="30"
variant="subtle"
onClick={() => {
component="a"
href={routeToPath("settings.admin", { org_id: organization.id })}
onClick={(e) => {
e.preventDefault();
navigate("settings.admin", { org_id: organization.id });
}}
>
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/hooks/useRemails.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ActionDispatch, createContext, useContext, useEffect, useReducer } from "react";
import { Action, State } from "../types.ts";
import { useRouter } from "./useRouter.ts";
import { routes } from "../routes.ts";
import { Navigate, Router } from "../router.ts";
import { RouteName, routes } from "../routes.ts";
import { Navigate, RouteParams, Router } from "../router.ts";
import { reducer } from "../reducer.ts";
import apiMiddleware from "../apiMiddleware.ts";
import { RemailsError } from "../error/error.ts";
Expand All @@ -14,6 +14,7 @@ export const RemailsContext = createContext<{
// Redirect to the page in the `redirect` query param. Used for navigating to the right page after logging in.
redirect: () => void;
match: Router["match"];
routeToPath: (name: RouteName, params?: RouteParams) => string | undefined;
}>({
state: {
user: null,
Expand Down Expand Up @@ -49,6 +50,9 @@ export const RemailsContext = createContext<{
redirect: () => {
throw new Error("RemailsContext must be used within RemailsProvider");
},
routeToPath: () => {
throw new Error("RemailsContext must be used within RemailsProvider");
},
});

export function useRemails() {
Expand Down Expand Up @@ -92,11 +96,21 @@ export function useLoadRemails() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const routeToPath = (name: RouteName, params?: RouteParams) => {
try {
return router.navigate(name, params, false).fullPath;
} catch (e) {
console.error("could not resolve route:", name, params, e);
return undefined;
}
};

return {
state,
dispatch,
navigate,
redirect,
match: router.match.bind(router),
routeToPath,
};
}
14 changes: 9 additions & 5 deletions frontend/src/layout/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Breadcrumbs as MantineBreadcrumbs, Text, Button, Box } from "@mantine/core";
import { Breadcrumbs as MantineBreadcrumbs, Text } from "@mantine/core";
import { useCredentials } from "../hooks/useCredentials.ts";
import { useDomains } from "../hooks/useDomains.ts";
import { useEmails } from "../hooks/useEmails.ts";
Expand All @@ -7,6 +7,7 @@ import { useProjects } from "../hooks/useProjects.ts";
import { useRemails } from "../hooks/useRemails.ts";
import { RouteName } from "../routes.ts";
import { JSX } from "react";
import { Link } from "../Link.tsx";

interface SegmentProps {
children: React.ReactNode;
Expand All @@ -15,17 +16,20 @@ interface SegmentProps {
}

function Segment({ children, last, route }: SegmentProps) {
const { navigate } = useRemails();
const props = { fz: "xs", c: "dark.3", px: "xs" };

if (last) {
return <Box {...props}>{children}</Box>;
return (
<Text span {...props}>
{children}
</Text>
);
}

return (
<Button {...props} td="underline" size="xs" h={20} variant="transparent" onClick={() => navigate(route)}>
<Link to={route} style={{ size: "xs", fw: "bold", ...props }}>
{children}
</Button>
</Link>
);
}

Expand Down
69 changes: 46 additions & 23 deletions frontend/src/layout/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
import { NavLink } from "@mantine/core";
import { BoxProps, NavLink as MantineNavLink } from "@mantine/core";
import { IconChartBar, IconGavel, IconServer, IconSettings, IconWorldWww } from "@tabler/icons-react";
import { useRemails } from "../hooks/useRemails.ts";
import { useDisclosure } from "@mantine/hooks";
import { NewOrganization } from "../components/organizations/NewOrganization.tsx";
import OrgDropDown from "./OrgDropDown.tsx";
import { RouteName } from "../routes.ts";

interface NavLinkProps {
label: string;
route: RouteName;
active: boolean;
close: () => void;
leftSection?: React.ReactNode;
style?: BoxProps;
}

function NavLink({ label, route, active, close, leftSection, style }: NavLinkProps) {
const { navigate, routeToPath } = useRemails();

return (
<MantineNavLink
label={label}
active={active}
leftSection={leftSection}
href={routeToPath(route)}
onClick={(e) => {
if (e.defaultPrevented || e.ctrlKey || e.metaKey) {
return;
}

e.preventDefault();
navigate(route);
close();
}}
{...style}
/>
);
}

export function NavBar({ close }: { close: () => void }) {
const {
Expand All @@ -20,14 +53,12 @@ export function NavBar({ close }: { close: () => void }) {
<>
{user.global_role === "admin" && (
<NavLink
mb="md"
label="Admin"
active={routerState.name.startsWith("admin")}
route="admin"
close={close}
leftSection={<IconGavel size={20} stroke={1.8} />}
onClick={() => {
navigate("admin");
close();
}}
style={{ mb: "md" }}
/>
)}

Expand All @@ -42,41 +73,33 @@ export function NavBar({ close }: { close: () => void }) {
<OrgDropDown openNewOrg={openNewOrg} />

<NavLink
mt="md"
label="Projects"
route="projects"
close={close}
active={routerState.name.startsWith("projects")}
leftSection={<IconServer size={20} stroke={1.8} />}
onClick={() => {
navigate("projects");
close();
}}
style={{ mt: "md" }}
/>
<NavLink
label="Domains"
route="domains"
close={close}
active={routerState.name.startsWith("domains")}
leftSection={<IconWorldWww size={20} stroke={1.8} />}
onClick={() => {
navigate("domains");
close();
}}
/>
<NavLink
label="Statistics"
route="statistics"
close={close}
active={routerState.name === "statistics"}
leftSection={<IconChartBar size={20} stroke={1.8} />}
onClick={() => {
navigate("statistics");
close();
}}
/>
<NavLink
label="Settings"
route="settings"
close={close}
active={routerState.name.startsWith("settings")}
leftSection={<IconSettings size={20} stroke={1.8} />}
onClick={() => {
navigate("settings");
close();
}}
/>
</>
);
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/layout/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function Tabs({ tabs, keepMounted }: { tabs: Tab[]; keepMounted?:
const {
state: { routerState },
navigate,
routeToPath,
} = useRemails();

const default_route = tabs[0].route;
Expand All @@ -30,7 +31,15 @@ export default function Tabs({ tabs, keepMounted }: { tabs: Tab[]; keepMounted?:
<MTabs value={tab_route} onChange={setActiveTab} keepMounted={keepMounted}>
<MTabs.List mb="md" mx="-lg" px="lg" className={classes.header}>
{tabs.map((t) => (
<MTabs.Tab size="lg" value={t.route} leftSection={t.icon} key={t.route}>
<MTabs.Tab
component="a"
onClick={(e) => e.preventDefault()}
size="lg"
value={t.route}
leftSection={t.icon}
key={t.route}
{...{ href: routeToPath(t.route) }}
>
{t.name}
</MTabs.Tab>
))}
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class Router {
return null;
}

navigate(name: RouteName, params: RouteParams): FullRouterState {
navigate(name: RouteName, params: RouteParams = {}, resetParamCache = true): FullRouterState {
const route = this.routes.find((route) => route.name === name);
if (!route) {
throw new Error(`Route with name ${name} not found`);
Expand All @@ -162,7 +162,9 @@ export class Router {
const query = Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined));
const pathParams = { ...this.pathParamCache, ...query };

this.pathParamCache = {};
if (resetParamCache) {
this.pathParamCache = {};
}

path = path.replace(/{(\w+)}/g, (_match, key) => {
const value = pathParams[key];
Expand Down
Loading
Loading