diff --git a/.env b/.env index 49e954c87..9c54c3073 100644 --- a/.env +++ b/.env @@ -4,5 +4,8 @@ REACT_APP_OIDC_CLIENT_ID="https://api.hel.fi/auth/helsinkiprofile-ui" REACT_APP_PROFILE_AUDIENCE="https://api.hel.fi/auth/helsinkiprofile" REACT_APP_PROFILE_GRAPHQL= REACT_APP_OIDC_SCOPE="openid profile $REACT_APP_PROFILE_AUDIENCE" -REACT_APP_SENTRY_DSN= +REACT_APP_SENTRY_DSN="https://8b5b23e2171b42cd8617e2b1ad7353b6@sentry.hel.ninja/63" REACT_APP_VERSION=$npm_package_version +TRANSLATION_LANGUAGES=en,fi,sv +TRANSLATIONS_SHEET_ID=1Q5LfG2wC_vxsoK0Ko-J1npWzY-96QQCERqMpvA0s9hg +TRANSLATION_PROJECT_NAME=open-city-profile diff --git a/.env.development b/.env.development index 5900c7942..37812ec1f 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,3 @@ -REACT_APP_OIDC_AUTHORITY="https://tunnistamo.test.kuva.hel.ninja/" +REACT_APP_OIDC_AUTHORITY="https://api.hel.fi/sso-test/" REACT_APP_OIDC_CLIENT_ID="https://api.hel.fi/auth/helsinkiprofile-ui" REACT_APP_PROFILE_GRAPHQL="https://profiili-api.test.kuva.hel.ninja/graphql/" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb0478872..36ead8cca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,6 +11,7 @@ build-review: extends: .build variables: DOCKER_IMAGE_NAME: "$CI_PROJECT_NAME-review" + DOCKER_BUILD_ARG_REACT_APP_ENVIRONMENT: 'review' DOCKER_BUILD_ARG_REACT_APP_OIDC_AUTHORITY: "https://tunnistamo.test.kuva.hel.ninja/" DOCKER_BUILD_ARG_REACT_APP_PROFILE_GRAPHQL: "https://profiili-api.test.kuva.hel.ninja/graphql/" only: @@ -21,7 +22,8 @@ build-staging: extends: .build variables: DOCKER_IMAGE_NAME: "$CI_PROJECT_NAME-staging" - DOCKER_BUILD_ARG_REACT_APP_OIDC_AUTHORITY: "https://tunnistamo.test.kuva.hel.ninja/" + DOCKER_BUILD_ARG_REACT_APP_ENVIRONMENT: 'staging' + DOCKER_BUILD_ARG_REACT_APP_OIDC_AUTHORITY: "https://api.hel.fi/sso-test/" DOCKER_BUILD_ARG_REACT_APP_PROFILE_GRAPHQL: "https://profiili-api.test.kuva.hel.ninja/graphql/" only: refs: @@ -31,6 +33,7 @@ build-production: extends: .build variables: DOCKER_IMAGE_NAME: "$CI_PROJECT_NAME-production" + DOCKER_BUILD_ARG_REACT_APP_ENVIRONMENT: 'production' DOCKER_BUILD_ARG_REACT_APP_OIDC_AUTHORITY: "https://api.hel.fi/sso/" DOCKER_BUILD_ARG_REACT_APP_PROFILE_GRAPHQL: "https://profiili-api.prod.kuva.hel.ninja/graphql/" only: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8188838d8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +Contributions are accepted as pull requests. Please observe our coding +practices at https://github.com/City-of-Helsinki/bestpractice/ . +Please make your pull requests short, elegant and only handling one +issue at a time! + + + + +Our contribution handling guidelines are at +https://github.com/City-of-Helsinki/bestpractice/blob/master/accepting-contributions.md diff --git a/Dockerfile b/Dockerfile index 842f46a06..af4b9d143 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,8 @@ FROM appbase as staticbuilder ARG REACT_APP_PROFILE_GRAPHQL ARG REACT_APP_OIDC_AUTHORITY +ARG REACT_APP_ENVIRONMENT + COPY . /app RUN yarn build diff --git a/README.md b/README.md index 092bbef7c..07aa5bdd7 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,13 @@ UI for citizen-profile - This project was bootstrapped with [Create React App](h ## Environments -Test: https://omahelsinki.test.kuva.hel.ninja/ +Test: https://profiili.test.kuva.hel.ninja/ -Staging: None +Production: https://profiili.prod.kuva.hel.ninja/ -Production: None +## Issues board -## Development - -If running on Linux or MacOS, easiest way is to just run the app without docker. Any semi-new version of node should probably work, the docker-image is set to use node 12. - -Run `yarn` to install dependencies, start app with `yarn start`. - -The graphql-backend for development is located at https://helsinkiprofile.test.kuva.hel.ninja/graphql/, it has graphiql installed so you can browse it in your browser! +https://helsinkisolutionoffice.atlassian.net/projects/OM/issues/?filter=allissues&= ## CI @@ -49,13 +43,16 @@ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. -## Running with docker +### `yarn codegen` + +Generate static types for GraphQL queries by using the schema from the backend server. -If you really must, you can run this app with docker locally. +### `yarn update-translations` -Run `docker-compose up` to start the app in docker. +Fetches translation data from our Google Spreadsheet and updates translation files. See `.env` for configuration. + +You still need to update tests and add the translation files to the git repository manually. -`docker-compose down` stops the container. ## Environment variables @@ -68,12 +65,39 @@ The following envs are used: - REACT_APP_PROFILE_AUDIENCE - name of the api-token that client uses profile-api with - REACT_APP_PROFILE_GRAPHQL - URL to the profile graphql - REACT_APP_OIDC_SCOPE - which scopes the app requires -- REACT_APP_SENTRY_DSN - not yet used +- REACT_APP_SENTRY_DSN - sentry public dns-key + + +## Setting up local development environment with Docker + +### Set tunnistamo hostname + +Add the following line to your hosts file (`/etc/hosts` on mac and linux): + + 127.0.0.1 tunnistamo-backend + +### Create a new OAuth app on GitHub + +Go to https://github.com/settings/developers/ and add a new app with the following settings: + +- Application name: can be anything, e.g. local tunnistamo +- Homepage URL: http://tunnistamo-backend:8000 +- Authorization callback URL: http://tunnistamo-backend:8000/accounts/github/login/callback/ +Save. You'll need the created **Client ID** and **Client Secret** for configuring tunnistamo in the next step. -## Tunnistamo configuration +### Install local tunnistamo -This app uses tunnistamo for authentication. Tunnistamo needs to have the following things set up: +Clone https://github.com/City-of-Helsinki/tunnistamo/. + +Follow the instructions for setting up tunnistamo locally. Before running `docker-compose up` set the following settings in tunnistamo roots `docker-compose.env.yaml`: + +- SOCIAL_AUTH_GITHUB_KEY: **Client ID** from the GitHub OAuth app +- SOCIAL_AUTH_GITHUB_SECRET: **Client Secret** from the GitHub OAuth app + +Run `docker-compose up` + +After container is up and running, few things need to be set up at http://localhost:8000/admin **OIDC client** @@ -89,6 +113,33 @@ Requires the following things: The scopes this app uses are set with the REACT_APP_OIDC_SCOPE environment variable. +### Install local open-city-profile +Clone https://github.com/City-of-Helsinki/open-city-profile/. + +1. Create a `docker-compose.env.yaml` file in the project folder: + * Use `docker-compose.env.yaml.example` as a base, it does not need any changes + for getting the project running. + * Change `DEBUG` and the rest of the Django settings if needed. + * `TOKEN_AUTH_*`, settings for [tunnistamo](https://github.com/City-of-Helsinki/tunnistamo) authentication service + * Set entrypoint/startup variables according to taste. + * `CREATE_SUPERUSER`, creates a superuser with credentials `admin`:`admin` (admin@example.com) + * `APPLY_MIGRATIONS`, applies migrations on startup + * `BOOTSTRAP_DIVISIONS`, bootstrap data import for divisions + +2. Run `docker-compose up` + +### open-city-profile-ui + +If running on Linux or MacOS, easiest way is to just run the app without docker. Any semi-new version of node should probably work, the docker-image is set to use node 12. + +`docker-compose up` starts the container. + +OR + +Run `yarn` to install dependencies, start app with `yarn start`. + +The graphql-backend for development is located at https://profiili-api.test.kuva.hel.ninja/graphql/, it has graphiql installed so you can browse it in your browser! + ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). diff --git a/package.json b/package.json index 100bd2d0c..ea342f56f 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "open-city-profile-ui", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "license": "MIT", "private": true, "dependencies": { "@apollo/react-hooks": "^3.1.3", + "@apollo/react-testing": "^3.1.3", + "@datapunt/matomo-tracker-react": "^0.1.2", + "@sentry/browser": "^5.15.4", "@types/classnames": "^2.2.9", "@types/enzyme": "^3.10.5", "@types/enzyme-adapter-react-16": "^1.0.6", @@ -12,6 +15,7 @@ "@types/node": "12.11.5", "@types/react": "16.9.9", "@types/react-dom": "16.9.2", + "@types/react-helmet": "^6.0.0", "@types/react-modal": "^3.10.1", "@types/react-redux": "^7.1.5", "@types/react-router-dom": "^5.1.0", @@ -25,13 +29,16 @@ "formik": "^2.0.4", "graphql": "^14.5.8", "graphql.macro": "^1.4.2", - "hds-core": "^0.4.0", - "hds-react": "^0.5.3", + "hds-core": "0.6.3", + "hds-design-tokens": "0.2.0", + "hds-react": "0.7.2", + "i18n-iso-countries": "^5.3.0", "i18next": "^17.3.0", "i18next-browser-languagedetector": "^4.0.1", "oidc-client": "^1.9.1", "react": "^16.11.0", "react-dom": "^16.11.0", + "react-helmet": "^6.0.0", "react-i18next": "^10.13.1", "react-modal": "^3.11.1", "react-redux": "^7.1.1", @@ -49,7 +56,8 @@ "test": "react-scripts test", "ci": "CI=true yarn test --coverage", "lint": "eslint --ext js,ts,tsx src", - "codegen": "apollo client:codegen ./src/graphql/generatedTypes.ts --outputFlat --includes=./src/**/*.graphql --target=typescript --endpoint=https://helsinkiprofile.test.kuva.hel.ninja/graphql/ --useReadOnlyTypes --addTypename" + "codegen": "apollo client:codegen ./src/graphql/generatedTypes.ts --outputFlat --includes=./src/**/*.graphql --target=typescript --endpoint=https://profiili-api.test.kuva.hel.ninja/graphql/ --useReadOnlyTypes --addTypename", + "update-translations": "ts-node -P ./scripts/tsconfig.json -r dotenv/config --files scripts/update-translations.ts" }, "eslintConfig": { "extends": "react-app" @@ -70,7 +78,9 @@ "apollo": "^2.21.1", "eslint-config-prettier": "^6.4.0", "eslint-plugin-prettier": "^3.1.1", + "helsinki-utils": "City-of-Helsinki/helsinki-utils-js#0.1.0", "jest-fetch-mock": "^2.1.2", - "prettier": "^1.18.2" + "prettier": "^1.18.2", + "ts-node": "^8.8.2" } } diff --git a/public/index.html b/public/index.html index ff4219a90..d57aa728f 100644 --- a/public/index.html +++ b/public/index.html @@ -1,41 +1,18 @@ - + - - - Open city profile app + Profiili
- diff --git a/public/robots.txt b/public/robots.txt index 01b0f9a10..b21f0887a 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,3 @@ # https://www.robotstxt.org/robotstxt.html User-agent: * +Disallow: / diff --git a/scripts/helsinki-utils.d.ts b/scripts/helsinki-utils.d.ts new file mode 100644 index 000000000..1b674459e --- /dev/null +++ b/scripts/helsinki-utils.d.ts @@ -0,0 +1,9 @@ +declare module 'helsinki-utils/scripts/fetch-translations' { + function fetchTranslations( + sheetId: string, + languages: string[], + output: string, + debug?: boolean + ): Promise; + export = fetchTranslations; +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000..5b66bde8d --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "outDir": "./dist", + "sourceMap": true, + "esModuleInterop": true + }, + "include": [ + "*" + ], + } \ No newline at end of file diff --git a/scripts/update-translations.ts b/scripts/update-translations.ts new file mode 100644 index 000000000..cd9a5c418 --- /dev/null +++ b/scripts/update-translations.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env ts-node-script +import * as path from 'path'; + +/// +import fetchTranslations from 'helsinki-utils/scripts/fetch-translations'; + +const languages = process.env.TRANSLATION_LANGUAGES.split(','); +const sheetId = process.env.TRANSLATIONS_SHEET_ID; + +const pathToLocales: string = path.join(__dirname, '../src/i18n'); + +const start = async () => { + try { + await fetchTranslations(sheetId, languages, pathToLocales); + console.log('Done'); // eslint-disable-line + } catch (err) { + console.error(err.message); // eslint-disable-line + process.exit(1); + } +}; + +start(); diff --git a/src/App.tsx b/src/App.tsx index a3ca57661..54f8db3ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,11 @@ import { Switch, Route } from 'react-router'; import { ApolloProvider } from '@apollo/react-hooks'; import { Provider as ReduxProvider } from 'react-redux'; import { OidcProvider, loadUser } from 'redux-oidc'; +import { MatomoProvider, createInstance } from '@datapunt/matomo-tracker-react'; +import countries from 'i18n-iso-countries'; +import fi from 'i18n-iso-countries/langs/fi.json'; +import en from 'i18n-iso-countries/langs/en.json'; +import sv from 'i18n-iso-countries/langs/sv.json'; import graphqlClient from './graphql/client'; import store from './redux/store'; @@ -12,8 +17,14 @@ import Login from './auth/components/login/Login'; import OidcCallback from './auth/components/oidcCallback/OidcCallback'; import Profile from './profile/components/profile/Profile'; import { fetchApiTokenThunk } from './auth/redux'; -import TermsOfService from './tos/components/termsOfService/TermsOfService'; import ProfileDeleted from './profile/components/profileDeleted/ProfileDeleted'; +import { MAIN_CONTENT_ID } from './common/constants'; +import AccessibilityShortcuts from './common/accessibilityShortcuts/AccessibilityShortcuts'; +import AppMeta from './AppMeta'; + +countries.registerLocale(fi); +countries.registerLocale(en); +countries.registerLocale(sv); if (process.env.NODE_ENV !== 'production') { enableOidcLogging(); @@ -25,6 +36,18 @@ loadUser(store, userManager).then(async user => { } }); +const instance = createInstance({ + urlBase: 'https://analytics.hel.ninja/', + siteId: 60, +}); + +// Prevent non-production data from being submitted to Matomo +// by pretending to require consent to process analytics data and never ask for it. +// https://developer.matomo.org/guides/tracking-javascript-guide#step-1-require-consent +if (process.env.REACT_APP_ENVIRONMENT !== 'production') { + window._paq.push(['requireConsent']); +} + type Props = {}; function App(props: Props) { @@ -32,31 +55,36 @@ function App(props: Props) { - - { - userManager.signinSilentCallback(); - return null; - }} - /> - - - - - - - - - - - - - - - - 404 - not found - + + + {/* This should be the first focusable element */} + + + { + userManager.signinSilentCallback(); + return null; + }} + /> + + + + + + + + + + + + + 404 - not found + + diff --git a/src/AppMeta.tsx b/src/AppMeta.tsx new file mode 100644 index 000000000..ad4d3d7c5 --- /dev/null +++ b/src/AppMeta.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { useTranslation } from 'react-i18next'; + +function AppMeta() { + const { i18n } = useTranslation(); + + return ( + + + + ); +} + +export default AppMeta; diff --git a/src/auth/authenticate.ts b/src/auth/authenticate.ts index 1218d92c7..73383aef3 100644 --- a/src/auth/authenticate.ts +++ b/src/auth/authenticate.ts @@ -1,9 +1,14 @@ +import * as Sentry from '@sentry/browser'; + import userManager from './userManager'; import store from '../redux/store'; import { apiError } from './redux'; export default function(): void { userManager.signinRedirect().catch(error => { + if (error.message !== 'Network Error') { + Sentry.captureException(error); + } store.dispatch(apiError(error.toString())); }); } diff --git a/src/auth/components/login/Login.module.css b/src/auth/components/login/Login.module.css index 58cfd9b72..22daea664 100644 --- a/src/auth/components/login/Login.module.css +++ b/src/auth/components/login/Login.module.css @@ -1,5 +1,5 @@ .wrapper { - background-color: var(--hds-brand-color-bus); + background-color: var(--color-bus); display: flex; justify-content: center; align-items: center; @@ -11,7 +11,7 @@ margin: 20px 10px; width: 95%; text-align: center; - color: var(--hds-ui-color-white); + color: var(--color-white); } .logo { @@ -20,13 +20,17 @@ } .logo g { - fill: var(--hds-ui-color-white); + fill: var(--color-white); } .button { margin-top: 50px; } +.content h2 { + font-size: var(--fontsize-h-5); +} + .content button { min-width: auto; } diff --git a/src/auth/components/login/Login.tsx b/src/auth/components/login/Login.tsx index 97b282484..74e67e157 100644 --- a/src/auth/components/login/Login.tsx +++ b/src/auth/components/login/Login.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { useMatomo } from '@datapunt/matomo-tracker-react'; import { RootState } from '../../../redux/rootReducer'; import { AuthState, resetApiError } from '../../redux'; @@ -18,18 +19,22 @@ type Props = { function Home(props: Props) { const { t } = useTranslation(); + const { trackEvent } = useMatomo(); return ( - +

{t('login.title')}

-
{t('login.description')}
+

{t('login.description')}

diff --git a/src/auth/components/oidcCallback/OidcCallback.tsx b/src/auth/components/oidcCallback/OidcCallback.tsx index 2b0a50b03..91bcd6d91 100644 --- a/src/auth/components/oidcCallback/OidcCallback.tsx +++ b/src/auth/components/oidcCallback/OidcCallback.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { CallbackComponent } from 'redux-oidc'; import { useHistory } from 'react-router'; import { useTranslation } from 'react-i18next'; +import * as Sentry from '@sentry/browser'; import userManager from '../../userManager'; @@ -13,9 +14,8 @@ function OidcCallback(props: Props) { history.push('/'); }; const onError = (error: object) => { - // TODO: do something about errors - // eslint-disable-next-line no-console - console.error(error); + Sentry.captureException(error); + history.push('/'); }; const { t } = useTranslation(); return ( diff --git a/src/auth/logout.ts b/src/auth/logout.ts index c57c79a1e..a59bb22cc 100644 --- a/src/auth/logout.ts +++ b/src/auth/logout.ts @@ -1,9 +1,12 @@ +import * as Sentry from '@sentry/browser'; + import userManager from './userManager'; import store from '../redux/store'; import { apiError } from './redux'; export default function(): void { userManager.signoutRedirect().catch(error => { + Sentry.captureException(error); store.dispatch(apiError(error.toString())); }); } diff --git a/src/common/accessibilityShortcuts/AccessibilityShortcuts.module.css b/src/common/accessibilityShortcuts/AccessibilityShortcuts.module.css new file mode 100644 index 000000000..be64126ff --- /dev/null +++ b/src/common/accessibilityShortcuts/AccessibilityShortcuts.module.css @@ -0,0 +1,26 @@ +.wrapper { + display: flex; + justify-content: center; + + font-size: var(--fontsize-body-medium); + + background: var(--color-bus-light-20); +} + +/* Inspired by Bootstrap */ +/* https://github.com/twbs/bootstrap/blob/a4a04cd9ec741050390746f8056cc79a9c04c8df/scss/mixins/_screen-reader.scss */ +.srOnlyFocusable:not(:focus) { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +.accessibilityLink:focus { + padding: var(--spacing-layout-xs) 0; +} diff --git a/src/common/accessibilityShortcuts/AccessibilityShortcuts.tsx b/src/common/accessibilityShortcuts/AccessibilityShortcuts.tsx new file mode 100644 index 000000000..05cdc0edb --- /dev/null +++ b/src/common/accessibilityShortcuts/AccessibilityShortcuts.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; + +import styles from './AccessibilityShortcuts.module.css'; + +interface Props { + mainContentId: string; +} + +function AccessibilityShortcuts({ mainContentId }: Props) { + const mainContentHref = `#${mainContentId}`; + const { t } = useTranslation(); + + return ( + + ); +} + +export default AccessibilityShortcuts; diff --git a/src/common/button/Button.module.css b/src/common/button/Button.module.css index 772b6dc17..bc55f0294 100644 --- a/src/common/button/Button.module.css +++ b/src/common/button/Button.module.css @@ -1,7 +1,7 @@ .button { - background: var(--hds-brand-color-bus); + background: var(--color-bus); border: none; - color: var(--hds-ui-color-white); + color: var(--color-white); display: inline-block; font-weight: 500; font-size: 16px; @@ -11,22 +11,22 @@ } .button:hover { - background: var(--hds-brand-color-bus-dark-50); + background: var(--color-bus-dark-50); } .button:disabled { - background: var(--hds-brand-color-bus-light-20); + background: var(--color-bus-light-20); cursor: not-allowed; } .outlined { - background: var(--hds-ui-color-white); - border: 1px solid var(--hds-brand-color-bus); - color: var(--hds-ui-color-black); + background: var(--color-white); + border: 1px solid var(--color-bus); + color: var(--color-bus); display: inline-block; font-weight: 500; font-size: 16px; text-align: center; - padding: 15px; + padding: 14px; min-width: 230px; } diff --git a/src/common/checkbox/Checkbox.module.css b/src/common/checkbox/Checkbox.module.css deleted file mode 100644 index c778aaecb..000000000 --- a/src/common/checkbox/Checkbox.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.container { - margin: 10px 0; - width: 100%; - display: flex; - align-items: center; -} - -.checkbox { - min-width: 20px; - min-height: 20px; - -webkit-appearance: none; - background-color: var(--hds-ui-color-white); - border: 1px solid var(--hds-brand-color-bus-dark-50); - position: relative; -} - -.checkbox:checked { - background-color: var(--hds-brand-color-bus-dark-50); - color: var(--hds-ui-color-white) -} - -.checkbox:checked:after { - content: '\2714'; - font-size: 16px; - position: absolute; - top: 0; - left: 3px; - color: var(--hds-ui-color-white); -} - -.checkbox:checked:focus { - outline: none; -} - -.label { - margin-left: 10px; - font-size: 16px; -} diff --git a/src/common/checkbox/Checkbox.tsx b/src/common/checkbox/Checkbox.tsx deleted file mode 100644 index b8b287d47..000000000 --- a/src/common/checkbox/Checkbox.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { PropsWithChildren, ReactNode } from 'react'; -import classNames from 'classnames'; - -import styles from './Checkbox.module.css'; - -type Props = PropsWithChildren<{ - onChange: () => void; - label?: string | ReactNode; - className?: string; - name: string; -}>; - -function Checkbox(props: Props) { - return ( -
- - -
- ); -} - -export default Checkbox; diff --git a/src/common/checkedLabel/CheckedLabel.module.css b/src/common/checkedLabel/CheckedLabel.module.css index 6231ded2a..b321f40d5 100644 --- a/src/common/checkedLabel/CheckedLabel.module.css +++ b/src/common/checkedLabel/CheckedLabel.module.css @@ -1,5 +1,10 @@ -.checkedLabel::before { - content: '\2714'; - display: inline-block; +.checkLabel { + display: flex; + align-items: center; +} + +.icon { + width: 16px; + height: 16px; margin-right: 10px; } diff --git a/src/common/checkedLabel/CheckedLabel.tsx b/src/common/checkedLabel/CheckedLabel.tsx index f51ac4c7d..117e02e28 100644 --- a/src/common/checkedLabel/CheckedLabel.tsx +++ b/src/common/checkedLabel/CheckedLabel.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { IconCheck } from 'hds-react'; import styles from './CheckedLabel.module.css'; @@ -10,6 +11,7 @@ type Props = { function CheckedLabel(props: Props) { return ( + {props.value} ); diff --git a/src/common/constants.tsx b/src/common/constants.tsx new file mode 100644 index 000000000..0fef48aaa --- /dev/null +++ b/src/common/constants.tsx @@ -0,0 +1 @@ +export const MAIN_CONTENT_ID = 'main-content'; diff --git a/src/common/dropdown/Dropdown.module.css b/src/common/dropdown/Dropdown.module.css index e70e196ac..2bcf8e28b 100644 --- a/src/common/dropdown/Dropdown.module.css +++ b/src/common/dropdown/Dropdown.module.css @@ -16,11 +16,11 @@ } .navButton { - max-width: 180px; + max-width: 230px; } .label { - max-width: 150px; + max-width: 200px; display: block; white-space: nowrap; overflow: hidden; @@ -36,14 +36,14 @@ .dropdownContent { position: absolute; right: 0; - border: 1px solid var(--hds-ui-color-black-50); - background: var(--hds-ui-color-white); + border: 1px solid var(--color-black-50); + background: var(--color-white); z-index: 1; width: 256px; } .linkButton { - color: var(--hds-ui-color-black); + color: var(--color-black); text-decoration: none; } @@ -58,16 +58,16 @@ .linkButton:hover, .dropdownContentOption:hover { - background-color: var(--hds-ui-color-black-20); + background-color: var(--color-black-20); } .linkButton:not(:last-child), .dropdownContentOption:not(:last-child) { - border-bottom: 1px solid var(--hds-ui-color-black-30); + border-bottom: 1px solid var(--color-black-30); } .linkButton:focus, .dropdownContent button:focus, .dropdownWrapper button:focus { - outline: 1px solid var(--hds-brand-color-coat-of-arms-blue); + outline: 1px solid var(--color-coat-of-arms-blue); } diff --git a/src/common/expandingPanel/ExpandingPanel.module.css b/src/common/expandingPanel/ExpandingPanel.module.css index 649a1ca12..308dc589b 100644 --- a/src/common/expandingPanel/ExpandingPanel.module.css +++ b/src/common/expandingPanel/ExpandingPanel.module.css @@ -1,7 +1,7 @@ .container { width: 100%; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); - background: var(--hds-ui-color-white); + background: var(--color-white); margin: 15px 0; } @@ -17,7 +17,7 @@ .title h2 { margin-top: 11px; font-size: 22px; - color: var(--hds-brand-color-bus-dark-50); + color: var(--color-bus); } .rightSideInformation { @@ -50,22 +50,11 @@ .content { - padding: 0 20px; + padding: 0 20px 20px; width: 80%; overflow: hidden; } -.open { - overflow: auto; - height: 100%; - padding-bottom: 20px; -} - -.closed { - height: 0; -} - - @media (max-width: 400px) { .content { overflow-x: hidden; diff --git a/src/common/expandingPanel/ExpandingPanel.tsx b/src/common/expandingPanel/ExpandingPanel.tsx index b5355fcfc..1849a0db5 100644 --- a/src/common/expandingPanel/ExpandingPanel.tsx +++ b/src/common/expandingPanel/ExpandingPanel.tsx @@ -1,6 +1,5 @@ import React, { useState, PropsWithChildren } from 'react'; -import classNames from 'classnames'; -import IconAngleRight from 'hds-react/lib/icons/IconAngleRight'; +import { IconAngleRight } from 'hds-react'; import { useTranslation } from 'react-i18next'; import styles from './ExpandingPanel.module.css'; @@ -49,14 +48,7 @@ function ExpandingPanel(props: Props) { />
-
- {props.children} -
+ {expanded &&
{props.children}
} ); } diff --git a/src/common/explanation/Explanation.module.css b/src/common/explanation/Explanation.module.css index b80f2a0f8..18df4e87a 100644 --- a/src/common/explanation/Explanation.module.css +++ b/src/common/explanation/Explanation.module.css @@ -1,7 +1,19 @@ +.container.margin { + margin: 0 15px; +} + .main { - font-size: 24px; + font-size: 36px; + margin-bottom: 30px; } .small { - color: var(--hds-ui-color-black-70); + margin-top: 0; + color: var(--color-black-70); font-size: 20px; } + +@media (min-width: 1200px) { + .container.margin { + margin: 0; + } +} diff --git a/src/common/explanation/Explanation.tsx b/src/common/explanation/Explanation.tsx index 4c3b48723..f17b78f76 100644 --- a/src/common/explanation/Explanation.tsx +++ b/src/common/explanation/Explanation.tsx @@ -1,18 +1,37 @@ import React from 'react'; +import classNames from 'classnames'; import styles from './Explanation.module.css'; type Props = { main: string; - small: string; + small?: string; className?: string; + // The explanation content can be with or without a wrapping element. + // When it is not wrapped, it needs to have its own margin on small + // screens. When it is wrapped, the containing element usually has a + // padding that takes care of the whitespace. + // + // In this context margin means that the explanation has horizontal + // margin. Flush means that it does not--that it's flush with the + // other content on that level of the hierarchy. + variant?: 'margin' | 'flush'; }; -function Explanation(props: Props) { +function Explanation({ className, main, small, variant = 'margin' }: Props) { return ( -
-

{props.main}

-

{props.small}

+
+

{main}

+ {small &&

{small}

}
); } diff --git a/src/common/footer/Footer.module.css b/src/common/footer/Footer.module.css index d633aa65f..c0b442a9e 100644 --- a/src/common/footer/Footer.module.css +++ b/src/common/footer/Footer.module.css @@ -1,5 +1,5 @@ .footer { - background: var(--hds-brand-color-bus); + background: var(--color-bus); } .logo { @@ -8,12 +8,12 @@ } .logo g { - fill: var(--hds-ui-color-white); + fill: var(--color-white); } .textContainer { - border-top: 1px solid var(--hds-ui-color-black-50); - color: var(--hds-ui-color-white); + border-top: 1px solid var(--color-black-50); + color: var(--color-white); font-size: 14px; margin: 0 3px; padding: 20px 10px; @@ -24,13 +24,13 @@ .links a { font-weight: bold; - color: var(--hds-ui-color-white); + color: var(--color-white); text-decoration: none; } .feedback { font-weight: bold; - color: var(--hds-ui-color-white); + color: var(--color-white); margin-top: 10px; } diff --git a/src/common/footer/Footer.tsx b/src/common/footer/Footer.tsx index d464cb2a3..187af80ca 100644 --- a/src/common/footer/Footer.tsx +++ b/src/common/footer/Footer.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ReactComponent as HelsinkiLogo } from '../svg/HelsinkiLogo.svg'; @@ -25,9 +24,14 @@ function Footer(props: Props) {
- + {t('footer.feedback')} - + diff --git a/src/common/footerLinks/FooterLinks.tsx b/src/common/footerLinks/FooterLinks.tsx index 884fb0c8a..79b7dc6d7 100644 --- a/src/common/footerLinks/FooterLinks.tsx +++ b/src/common/footerLinks/FooterLinks.tsx @@ -2,23 +2,18 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import tosConstants from '../../tos/constants/tosConstants'; - type Props = { className?: string; }; function FooterLinks(props: Props) { - const { t, i18n } = useTranslation(); - const selectedLanguage = - (i18n.languages && i18n.languages[0].toUpperCase()) || 'FI'; - const link = Object(tosConstants.REGISTER_DESCRIPTION)[selectedLanguage]; + const { t } = useTranslation(); + return ( {' '} - {t('footer.privacy')} |{' '} - {t('footer.accessibility')} |{' '} - {t('footer.terms')} + {t('footer.privacy')} |{' '} + {t('footer.accessibility')} ); } diff --git a/src/common/fullscreenNavigation/FullscreenNavigation.module.css b/src/common/fullscreenNavigation/FullscreenNavigation.module.css index 511a1c492..d02680d98 100644 --- a/src/common/fullscreenNavigation/FullscreenNavigation.module.css +++ b/src/common/fullscreenNavigation/FullscreenNavigation.module.css @@ -24,7 +24,7 @@ } .navItems { - background: var(--hds-ui-color-white); + background: var(--color-white); display: flex; flex: 1; flex-direction: column; @@ -44,7 +44,7 @@ } .navLink { - color: var(--hds-ui-color-black); + color: var(--color-black); cursor: pointer; font-size: 20px; font-weight: bold; diff --git a/src/common/header/Header.module.css b/src/common/header/Header.module.css index 974cb3b02..18ee2f239 100644 --- a/src/common/header/Header.module.css +++ b/src/common/header/Header.module.css @@ -1,5 +1,5 @@ .header { - background: var(--hds-ui-color-white); + background: var(--color-white); height: 64px; position: sticky; z-index: 1; diff --git a/src/common/header/Header.tsx b/src/common/header/Header.tsx index 3a57c0a66..691e36e1b 100644 --- a/src/common/header/Header.tsx +++ b/src/common/header/Header.tsx @@ -16,13 +16,21 @@ function Header() {
{t('appName')} -
+
- - + {/* The language switcher is a navigation element. Because */} + {/* there's a possibility that we have multiple navs on */} + {/* the same page, we need to give the element a label to */} + {/* distinct it from the other navs. */} + +
-
+
); diff --git a/src/common/header/userDropdown/UserDropdown.tsx b/src/common/header/userDropdown/UserDropdown.tsx index 65dcdf84a..c9a30d0ed 100644 --- a/src/common/header/userDropdown/UserDropdown.tsx +++ b/src/common/header/userDropdown/UserDropdown.tsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { useLazyQuery } from '@apollo/react-hooks'; import { useTranslation } from 'react-i18next'; import { loader } from 'graphql.macro'; +import { useMatomo } from '@datapunt/matomo-tracker-react'; import PersonIcon from '../../svg/Person.svg'; import { NameQuery } from '../../../graphql/generatedTypes'; @@ -25,6 +26,7 @@ function UserDropdown(props: Props) { fetchPolicy: 'cache-only', }); const { t } = useTranslation(); + const { trackEvent } = useMatomo(); const isAuthenticated = useSelector(isAuthenticatedSelector); @@ -53,7 +55,10 @@ function UserDropdown(props: Props) { const login = { id: 'loginButton', label: t('nav.signin'), - onClick: () => authenticate(), + onClick: () => { + trackEvent({ category: 'action', action: 'Log in' }); + authenticate(); + }, }; const user = { @@ -68,7 +73,10 @@ function UserDropdown(props: Props) { const logOut = { id: 'logoutButton', label: t('nav.signout'), - onClick: () => logout(), + onClick: () => { + trackEvent({ category: 'action', action: 'Log out' }); + logout(); + }, }; const dropdownOptions = getDropdownOptions(); diff --git a/src/common/labeledValue/LabeledValue.module.css b/src/common/labeledValue/LabeledValue.module.css index 710d5e3c9..30481056b 100644 --- a/src/common/labeledValue/LabeledValue.module.css +++ b/src/common/labeledValue/LabeledValue.module.css @@ -11,4 +11,5 @@ .value { font-size: 18px; -} \ No newline at end of file + white-space: pre-line; +} diff --git a/src/common/pageHeading/PageHeading.module.css b/src/common/pageHeading/PageHeading.module.css index 1eee0de4d..e419194b6 100644 --- a/src/common/pageHeading/PageHeading.module.css +++ b/src/common/pageHeading/PageHeading.module.css @@ -1,10 +1,10 @@ .pageHeading { align-items: center; - color: var(--hds-brand-color-coat-of-arms-blue-dark-50); + color: var(--color-bus); display: flex; min-height: 120px; padding: 20px 10px 10px; - font-size: var(--hds-text-xxxl); + font-size: var(--fontsize-h-1); font-weight: bold; width: 100%; } diff --git a/src/common/pageLayout/PageLayout.module.css b/src/common/pageLayout/PageLayout.module.css index d488e8e95..3525e2d03 100644 --- a/src/common/pageLayout/PageLayout.module.css +++ b/src/common/pageLayout/PageLayout.module.css @@ -5,7 +5,7 @@ } .content { - background: var(--hds-brand-color-coat-of-arms-blue-light-20); + background: var(--color-coat-of-arms-blue-light-20); flex: 1; display: flex; } diff --git a/src/common/pageLayout/PageLayout.tsx b/src/common/pageLayout/PageLayout.tsx index 73128cd88..68242cd27 100644 --- a/src/common/pageLayout/PageLayout.tsx +++ b/src/common/pageLayout/PageLayout.tsx @@ -1,6 +1,9 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import classNames from 'classnames'; +import { useMatomo } from '@datapunt/matomo-tracker-react'; +import { useTranslation } from 'react-i18next'; +import { MAIN_CONTENT_ID } from '../constants'; import Header from '../header/Header'; import Footer from '../footer/Footer'; import styles from './PageLayout.module.css'; @@ -8,13 +11,31 @@ import styles from './PageLayout.module.css'; type Props = React.PropsWithChildren<{ className?: string; hideFooterLogo?: boolean; + title?: string; }>; function PageLayout(props: Props) { + const { trackPageView } = useMatomo(); + const { t } = useTranslation(); + const { title = 'appName' } = props; + + const pageTitle = + props.title !== 'appName' ? `${t(title)} - ${t('appName')}` : t('appName'); + + useEffect(() => { + trackPageView({ + documentTitle: pageTitle, + href: window.location.href, + }); + }, [trackPageView, pageTitle]); + return (
-
+
{props.children}