Skip to content

Commit

Permalink
add: missing breadcrumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
joonatank committed Dec 20, 2024
1 parent 54c5a75 commit 9c0a853
Show file tree
Hide file tree
Showing 20 changed files with 258 additions and 247 deletions.
17 changes: 14 additions & 3 deletions apps/ui/components/application/ApplicationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
import { useRouter } from "next/router";
import Stepper, { StepperProps } from "./Stepper";
import NotesWhenApplying from "@/components/application/NotesWhenApplying";
import { getApplicationPath } from "@/modules/urls";
import BreadcrumbWrapper from "../common/BreadcrumbWrapper";
import { applicationsPrefix, getApplicationPath } from "@/modules/urls";
import { Breadcrumb } from "../common/Breadcrumb";
import { H1 } from "common";

const InnerContainer = styled.div<{ $hideStepper: boolean }>`
Expand Down Expand Up @@ -111,9 +111,20 @@ export function ApplicationPageWrapper({
const title = t(`${translationKeyPrefix}.heading`);
const subTitle =
headContent || overrideText || t(`${translationKeyPrefix}.text`);

const routes = [
{
slug: applicationsPrefix,
title: t("breadcrumb:applications"),
},
{
title: t("breadcrumb:application"),
},
] as const;

return (
<>
<BreadcrumbWrapper route={["/applications", "application"]} />
<Breadcrumb routes={routes} />
<div>
<H1 $noMargin>{title}</H1>
<p>{subTitle}</p>
Expand Down
160 changes: 66 additions & 94 deletions apps/ui/components/common/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import React, { ElementType, Fragment } from "react";
import React from "react";
import styled, { css } from "styled-components";
import { IconAngleLeft, IconAngleRight } from "hds-react";
import { fontMedium } from "common";
import { breakpoints, fontMedium } from "common";
import { useMedia } from "react-use";
import Link from "next/link";
import { Flex } from "common/styles/util";
import { truncatedText } from "common/styles/cssFragments";
import { useTranslation } from "next-i18next";

export type RouteItem = {
title: string;
slug?: string;
};

type Props = {
routes: RouteItem[];
isMobile: boolean;
linkComponent?: ElementType;
className?: string;
};

const limits = {
default: 25,
current: 40,
routes: Readonly<RouteItem[]>;
};

const Nav = styled.nav<{ $isMobile?: boolean }>`
font-size: var(--fontsize-body-m);
display: flex;
align-items: center;
line-height: var(--spacing-3-xl);
Expand All @@ -35,19 +31,24 @@ const Nav = styled.nav<{ $isMobile?: boolean }>`
}
`;

const Item = styled.div`
display: flex;
align-items: center;
max-width: 100%;
`;
const Item = styled(Flex).attrs({
$alignItems: "center",
$direction: "row",
$gap: "none",
})``;

const currentCss = css`
color: var(--color-black);
${fontMedium}
`;

const Anchor = styled.span<{ $current?: boolean; $isMobile?: boolean }>`
// const LIMIT_CURRENT_CH = 40;
const LIMIT_DEFAULT_CH = 25;
const Anchor = styled(Link)<{ $current?: boolean; $isMobile?: boolean }>`
&& {
${truncatedText}
display: inline-block;
max-width: ${LIMIT_DEFAULT_CH}ch;
${({ $current }) => {
switch ($current) {
case true:
Expand All @@ -61,102 +62,73 @@ const Anchor = styled.span<{ $current?: boolean; $isMobile?: boolean }>`
}
}}
}
${({ $isMobile }) =>
$isMobile &&
`
overflow: hidden;
text-overflow: ellipsis;
}
`};
white-space: nowrap;
`;

const Slug = styled.span<{ $current?: boolean }>`
${({ $current }) => $current && currentCss}
white-space: nowrap;
${truncatedText}
max-width: ${LIMIT_DEFAULT_CH}ch;
`;

export function Breadcrumb({
routes = [],
function BreadcrumbImpl({
routes,
isMobile,
linkComponent,
className,
}: Props): JSX.Element {
const Link = linkComponent || Fragment;

}: Pick<Props, "routes"> & { isMobile: boolean }): JSX.Element | null {
const routesWithSlug = routes?.filter((n) => n.slug != null && n.slug !== "");
const lastRoute = routes[routes.length - 1];
const lastRouteWithSlug = routesWithSlug[routesWithSlug.length - 1];

// TODO why are we doing this? is there a case where we need to do this?
// or would it just be better to pass hideMobileBreadcrumb prop to the component
// instead of having hidden logic here?
const isMobileEnabled =
isMobile &&
routesWithSlug.length > 1 &&
routesWithSlug.length > 0 &&
lastRoute.slug !== lastRouteWithSlug.slug;

if (!isMobileEnabled && isMobile) {
return null;
}

if (isMobile) {
return (
<Item>
<IconAngleLeft size="s" aria-hidden="true" />
<Anchor href={lastRouteWithSlug?.slug ?? ""} $isMobile>
{lastRouteWithSlug.title}
</Anchor>
</Item>
);
}

return (
<Nav
className={className}
data-testid="breadcrumb__wrapper"
$isMobile={isMobile}
>
{!isMobile ? (
routes?.map((item, index) => (
<Item key={`${item.title}${item.slug}`}>
{index > 0 && (
<IconAngleRight size="s" aria-hidden className="angleRight" />
)}
{item.slug ? (
<Link {...(linkComponent && { href: item.slug, passHref: true })}>
<Anchor
{...(!linkComponent && { href: item.slug })}
$current={index === routes.length - 1}
>
{index === routes.length - 1
? item.title.length > limits.current
? `${item.title.slice(0, limits.current)}...`
: item.title
: item.title.length > limits.default
? `${item.title.slice(0, limits.default)}...`
: item.title}
</Anchor>
</Link>
) : (
<Slug $current={index === routes.length - 1}>
{index === routes.length - 1
? item.title.length > limits.current
? `${item.title.slice(0, limits.current)}...`
: item.title
: item.title.length > limits.default
? `${item.title.slice(0, limits.default)}...`
: item.title}
</Slug>
)}
</Item>
))
) : isMobileEnabled ? (
<Item>
<IconAngleLeft size="s" aria-hidden className="angleLeft" />
<Link
{...(linkComponent && {
href: lastRouteWithSlug?.slug,
passHref: true,
})}
>
<Anchor
{...(!linkComponent && { href: lastRouteWithSlug?.slug })}
$isMobile
>
{lastRouteWithSlug.title}
<>
{routes.map((item, index) => (
<Item key={`${item.title}${item.slug}`}>
{index > 0 && <IconAngleRight size="s" aria-hidden="true" />}
{item.slug ? (
<Anchor href={item.slug} $current={index === routes.length - 1}>
{item.title}
</Anchor>
</Link>
) : (
<Slug $current={index === routes.length - 1}>{item.title}</Slug>
)}
</Item>
) : null}
))}
</>
);
}

export function Breadcrumb({ routes }: Props): JSX.Element {
const { t } = useTranslation();
const isMobile = useMedia(`(max-width: ${breakpoints.m})`, false);

const routesWithFrontPage = [
{ title: t("breadcrumb:frontpage"), slug: "/" },
...routes,
];
return (
<Nav data-testid="breadcrumb__wrapper" $isMobile={isMobile}>
<BreadcrumbImpl routes={routesWithFrontPage} isMobile={isMobile} />
</Nav>
);
}
43 changes: 0 additions & 43 deletions apps/ui/components/common/BreadcrumbWrapper.tsx

This file was deleted.

8 changes: 7 additions & 1 deletion apps/ui/modules/applicationRound.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { type ApplicationRoundNode } from "common/gql/gql-types";
import { getTranslation } from "./util";
import { Maybe } from "@/gql/gql-types";

export function getApplicationRoundName(
applicationRound: Pick<ApplicationRoundNode, "nameFi" | "nameSv" | "nameEn">
applicationRound?:
| Maybe<Pick<ApplicationRoundNode, "nameFi" | "nameSv" | "nameEn">>
| undefined
): string {
if (applicationRound == null) {
return "-";
}
return getTranslation(applicationRound, "name");
}
57 changes: 29 additions & 28 deletions apps/ui/pages/applications/[id]/sent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,60 @@ import { IconAngleRight } from "hds-react";
import React from "react";
import { useTranslation } from "next-i18next";
import styled from "styled-components";
import { breakpoints } from "common/src/common/style";
import { fontMedium, H1 } from "common";
import { H1 } from "common";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import type { GetServerSidePropsContext } from "next";
import { applicationsPath } from "@/modules/urls";
import { applicationsPath, applicationsPrefix } from "@/modules/urls";
import { getCommonServerSideProps } from "@/modules/serverUtils";
import BreadcrumbWrapper from "@/components/common/BreadcrumbWrapper";
import { Breadcrumb } from "@/components/common/Breadcrumb";
import { ButtonLikeLink } from "@/components/common/ButtonLikeLink";

const FontMedium = styled.div`
${fontMedium}
`;
import { toNumber } from "common/src/helpers";

const Paragraph = styled.p`
white-space: pre-wrap;
margin-bottom: var(--spacing-xl);
@media (min-width: ${breakpoints.m}) {
max-width: 60%;
}
max-width: var(--prose-width);
`;

function Sent(): JSX.Element {
const { t } = useTranslation();

const routes = [
{
slug: applicationsPrefix,
title: t("breadcrumb:applications"),
},
{
title: t("application:sent.heading"),
},
] as const;

return (
<>
<BreadcrumbWrapper route={["/applications", "application"]} />
<H1>{t("application:sent.heading")}</H1>
<FontMedium as="p">{t("application:sent.subHeading")}</FontMedium>
<Breadcrumb routes={routes} />
<div>
<H1 $noMargin>{t("application:sent.heading")}</H1>
<Paragraph>{t("application:sent.subHeading")}</Paragraph>
</div>
<Paragraph>{t("application:sent.body")}</Paragraph>
<ButtonLikeLink href={applicationsPath}>
{t("navigation:Item.applications")}
<IconAngleRight aria-hidden="true" />
</ButtonLikeLink>
<div>
<ButtonLikeLink href={applicationsPath}>
{t("navigation:Item.applications")}
<IconAngleRight aria-hidden="true" />
</ButtonLikeLink>
</div>
</>
);
}

export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { locale } = ctx;

// TODO should fetch on SSR but we need authentication for it
const { query } = ctx;
const { locale, query } = ctx;
const { id } = query;
// TODO should fetch the application here to check it's actually sent
const pkstring = Array.isArray(id) ? id[0] : id;
const pk = Number.isNaN(Number(pkstring)) ? undefined : Number(pkstring);
const pk = toNumber(pkstring);
return {
notFound: pk == null,
props: {
...getCommonServerSideProps(),
key: locale ?? "fi",
id: pk ?? null,
...(await serverSideTranslations(locale ?? "fi")),
},
};
Expand Down
Loading

0 comments on commit 9c0a853

Please sign in to comment.