Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 2a37340

Browse files
authored
Merge pull request #285 from City-of-Helsinki/TILA-1463
TILA-1463 common breadcumb component
2 parents ab92263 + 02e93a7 commit 2a37340

20 files changed

Lines changed: 571 additions & 131 deletions

File tree

admin-ui/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ USER default
2323
# Install dependencies
2424
RUN yarn --frozen-lockfile
2525

26-
WORKDIR /opt/app-root/src/admin-ui
27-
2826
# Copy all files
27+
WORKDIR /opt/app-root/src/common
28+
COPY common .
29+
WORKDIR /opt/app-root/src/admin-ui
2930
COPY admin-ui .
3031

3132
# Build application

admin-ui/config-overrides.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
var path = require("path");
2+
3+
const { override, babelInclude } = require("customize-cra");
4+
5+
module.exports = function (config, env) {
6+
return Object.assign(
7+
config,
8+
override(
9+
babelInclude([
10+
/* transpile (converting to es5) code in src/ and shared component library */
11+
path.resolve("src"),
12+
path.resolve("../common"),
13+
])
14+
)(config, env)
15+
);
16+
};

admin-ui/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@tilavarauspalvelu/admin-ui",
2+
"name": "admin-ui",
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
@@ -26,6 +26,8 @@
2626
"axios-auth-refresh": "^3.1.0",
2727
"axios-case-converter": "^0.8.1",
2828
"classnames": "^2.2.6",
29+
"common": "^0.1.0",
30+
"customize-cra": "^1.0.0",
2931
"date-fns": "^2.16.1",
3032
"focus-trap-react": "^8.7.1",
3133
"graphql": "^15.5.1",
@@ -41,6 +43,7 @@
4143
"oidc-client": "^1.11.5",
4244
"query-string": "^7.0.0",
4345
"react": "^17.0.1",
46+
"react-app-rewired": "^2.2.1",
4447
"react-dom": "^17.0.1",
4548
"react-i18next": "^11.8.5",
4649
"react-map-gl": "5.3.16",
@@ -53,9 +56,9 @@
5356
"web-vitals": "^0.2.4"
5457
},
5558
"scripts": {
56-
"start": "HOST=local-tilavaraus.hel.fi HTTPS=true SSL_CRT_FILE=../common/certificates/local-tilavaraus.crt SSL_KEY_FILE=../common/certificates/local-tilavaraus.key react-scripts start",
57-
"build": "react-scripts build",
58-
"test": "react-scripts test",
59+
"start": "HOST=local-tilavaraus.hel.fi HTTPS=true SSL_CRT_FILE=../common/certificates/local-tilavaraus.crt SSL_KEY_FILE=../common/certificates/local-tilavaraus.key react-app-rewired start",
60+
"build": "react-app-rewired build",
61+
"test": "react-app-rewired test",
5962
"test:e2e-local": "testcafe --skip-js-errors \"chrome --window-size='1920,1080'\" e2e/tests",
6063
"lint": "eslint --ext ts,tsx src",
6164
"lint:css": "stylelint './src/**/*.tsx'",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
import { useMedia } from "react-use";
4+
import { Breadcrumb } from "common";
5+
import { useTranslation } from "react-i18next";
6+
import { trim } from "lodash";
7+
import { breakpoints } from "../styles/util";
8+
9+
type Alias = {
10+
slug: string;
11+
title: string;
12+
};
13+
14+
type Props = {
15+
route: string[];
16+
aliases?: Alias[];
17+
};
18+
19+
const Wrapper = styled.div`
20+
display: block;
21+
background-color: var(--color-white);
22+
`;
23+
24+
const BreadcrumbWrapper = ({ route, aliases }: Props): JSX.Element => {
25+
const { t } = useTranslation();
26+
const isMobile = useMedia(`(max-width: ${breakpoints.s})`, true);
27+
28+
const routes =
29+
route?.map((n) => ({
30+
title:
31+
aliases?.find((alias) => alias.slug === n)?.title ||
32+
t(`breadcrumb:${trim(n, "/")}`) ||
33+
"",
34+
slug: n.includes("/") ? n : "",
35+
})) || [];
36+
37+
return (
38+
<Wrapper>
39+
<Breadcrumb
40+
routes={[{ title: t("breadcrumb:frontpage"), slug: "/" }, ...routes]}
41+
className={isMobile ? "isMobile" : ""}
42+
/>
43+
</Wrapper>
44+
);
45+
};
46+
47+
export default BreadcrumbWrapper;

common/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Breadcrumb from "./src/breadcrumb/Breadcrumb";
12
import ExampleCommonComponent from "./src/ExampleCommonComponent";
23

3-
export { ExampleCommonComponent };
4+
export { Breadcrumb, ExampleCommonComponent };

common/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
2-
"name": "@tilavarauspalvelu/common",
2+
"name": "common",
33
"version": "0.1.0",
44
"private": true,
55
"module": "index.ts",
66
"main": "index.ts",
77
"dependencies": {
8+
"hds-react": "^1.12.0",
89
"react": "^17.0.2",
910
"react-dom": "^17.0.2",
10-
"react-scripts": "4.0.3"
11+
"react-scripts": "4.0.3",
12+
"styled-components": "^5.3.5"
1113
},
1214
"devDependencies": {
1315
"@testing-library/jest-dom": "^5.11.4",
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import React, { ElementType, Fragment } from "react";
2+
import styled from "styled-components";
3+
import { IconAngleLeft, IconAngleRight } from "hds-react";
4+
5+
export type RouteItem = {
6+
title: string;
7+
slug: string;
8+
};
9+
10+
type Props = {
11+
routes: RouteItem[];
12+
linkComponent?: ElementType;
13+
className?: string;
14+
};
15+
16+
const limits = {
17+
default: 25,
18+
current: 40,
19+
};
20+
21+
const MobileWrapper = styled.div`
22+
display: flex;
23+
align-items: center;
24+
max-width: 100%;
25+
`;
26+
27+
const DesktopWrapper = styled.div`
28+
display: flex;
29+
align-items: center;
30+
`;
31+
32+
const Wrapper = styled.nav`
33+
background-color: var(--color-white);
34+
font-size: var(--fontsize-body-m);
35+
display: flex;
36+
align-items: center;
37+
max-width: var(--container-width-xl);
38+
margin: 0 auto;
39+
line-height: var(--spacing-3-xl);
40+
color: var(--color-black);
41+
padding: 0 var(--spacing-m);
42+
43+
&& > a {
44+
color: var(--color-black);
45+
text-decoration: underline;
46+
}
47+
48+
${MobileWrapper} {
49+
display: none;
50+
}
51+
52+
&.isMobile {
53+
padding-left: var(--spacing-xs);
54+
55+
${MobileWrapper} {
56+
display: flex;
57+
}
58+
${DesktopWrapper} {
59+
display: none;
60+
}
61+
}
62+
63+
svg {
64+
margin: 0 var(--spacing-3-xs);
65+
}
66+
`;
67+
68+
const Anchor = styled.a<{ $current?: boolean }>`
69+
&&& {
70+
${({ $current }) => {
71+
switch ($current) {
72+
case true:
73+
return `
74+
color: var(--color-black);
75+
font-family: HelsinkiGrotesk-Medium, var(--font-default);
76+
font-weight: 500;
77+
`;
78+
case false:
79+
default:
80+
return `
81+
color: var(--color-black);
82+
text-decoration: underline;
83+
`;
84+
}
85+
}}
86+
}
87+
88+
${MobileWrapper} & {
89+
overflow: hidden;
90+
text-overflow: ellipsis;
91+
}
92+
93+
white-space: nowrap;
94+
`;
95+
96+
const Slug = styled.span<{ $current?: boolean }>`
97+
${({ $current }) => {
98+
switch ($current) {
99+
case true:
100+
return `
101+
color: var(--color-black);
102+
font-family: HelsinkiGrotesk-Medium, var(--font-default);
103+
font-weight: 500;
104+
`;
105+
case false:
106+
default:
107+
return ``;
108+
}
109+
}}
110+
111+
white-space: nowrap;
112+
`;
113+
114+
const Breadcrumb = ({
115+
routes = [],
116+
linkComponent,
117+
className,
118+
}: Props): JSX.Element => {
119+
const Link = linkComponent || Fragment;
120+
121+
const routesWithSlug = routes?.filter((n) => n.slug);
122+
const lastRoute = routes[routes.length - 1];
123+
const lastRouteWithSlug = routesWithSlug[routesWithSlug.length - 1];
124+
125+
return (
126+
<Wrapper className={className}>
127+
{routesWithSlug.length > 1 && lastRoute.slug !== lastRouteWithSlug.slug && (
128+
<MobileWrapper>
129+
<IconAngleLeft size="s" aria-hidden className="angleLeft" />
130+
<Link
131+
{...(linkComponent && {
132+
href: lastRouteWithSlug?.slug,
133+
passHref: true,
134+
})}
135+
>
136+
<Anchor
137+
{...(!linkComponent && { href: lastRouteWithSlug?.slug })}
138+
title={lastRouteWithSlug.title}
139+
>
140+
{lastRouteWithSlug.title}
141+
</Anchor>
142+
</Link>
143+
</MobileWrapper>
144+
)}
145+
{routes?.map((item, index) => (
146+
<DesktopWrapper key={`${item.title}${item.slug}`}>
147+
{index > 0 && (
148+
<IconAngleRight size="s" aria-hidden className="angleRight" />
149+
)}
150+
{item.slug ? (
151+
<Link {...(linkComponent && { href: item.slug, passHref: true })}>
152+
<Anchor
153+
{...(!linkComponent && { href: item.slug })}
154+
title={item.title}
155+
$current={index === routes.length - 1}
156+
>
157+
{index === routes.length - 1
158+
? item.title.length > limits.current
159+
? `${item.title.slice(0, limits.current)}...`
160+
: item.title
161+
: item.title.length > limits.default
162+
? `${item.title.slice(0, limits.default)}...`
163+
: item.title}
164+
</Anchor>
165+
</Link>
166+
) : (
167+
<Slug $current={index === routes.length - 1} title={item.title}>
168+
{index === routes.length - 1
169+
? item.title.length > limits.current
170+
? `${item.title.slice(0, limits.current)}...`
171+
: item.title
172+
: item.title.length > limits.default
173+
? `${item.title.slice(0, limits.default)}...`
174+
: item.title}
175+
</Slug>
176+
)}
177+
</DesktopWrapper>
178+
))}
179+
</Wrapper>
180+
);
181+
};
182+
183+
export default Breadcrumb;

common/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"resolveJsonModule": true,
1919
"isolatedModules": true,
2020
"noEmit": true,
21-
"jsx": "react-jsx"
21+
"jsx": "preserve"
2222
},
2323
"include": [
2424
"src",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "tilavarauspalvelu",
2+
"name": "tilavaraus",
33
"private": true,
44
"workspaces": [
55
"common",

ui/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ENV NPM_CONFIG_LOGLEVEL warn
88

99
WORKDIR /app
1010
COPY package.json yarn.lock ./
11+
COPY common/package.json common/package.json common/
1112
COPY ui/package.json ui/package.json ui/
1213
RUN yarn install --frozen-lockfile
1314

@@ -18,12 +19,15 @@ USER root
1819

1920
ENV NPM_CONFIG_LOGLEVEL warn
2021

22+
WORKDIR /app/common
23+
COPY common .
2124
WORKDIR /app/ui
2225
COPY ui .
2326

2427
COPY --from=deps /app/node_modules /app/node_modules
2528
COPY --from=deps /app/yarn.lock /app/yarn.lock
2629
COPY --from=deps /app/package.json /app/package.json
30+
COPY --from=deps /app/common/package.json /app/common/package.json
2731
COPY --from=deps /app/ui/package.json /app/ui/package.json
2832
RUN yarn build
2933

@@ -46,6 +50,7 @@ COPY --from=builder /app/ui/public ./public
4650
COPY --from=builder --chown=default:root /app/ui/.next ./.next
4751
COPY --from=builder /app/node_modules /app/node_modules
4852
COPY --from=builder /app/ui/package.json /app/ui/package.json
53+
COPY --from=builder /app/common/package.json /app/common/package.json
4954
COPY --from=builder /app/package.json /app/package.json
5055

5156
# Add images directory and fix group permissions

0 commit comments

Comments
 (0)