diff --git a/docker/auth/realm.json b/docker/auth/realm.json index 683f42c5..ce18a4fc 100644 --- a/docker/auth/realm.json +++ b/docker/auth/realm.json @@ -69,7 +69,7 @@ }, { "id": "1bfe401a-08fc-4d94-80e0-86c4f5195f99", - "name": "default-roles-people", + "name": "default-roles-impress", "description": "${role_default-roles}", "composite": true, "composites": { @@ -404,7 +404,7 @@ "groups": [], "defaultRole": { "id": "1bfe401a-08fc-4d94-80e0-86c4f5195f99", - "name": "default-roles-people", + "name": "default-roles-impress", "description": "${role_default-roles}", "composite": true, "clientRole": false, @@ -501,12 +501,12 @@ "clientId": "account-console", "name": "${client_account-console}", "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/people/account/", + "baseUrl": "/realms/impress/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/people/account/*"], + "redirectUris": ["/realms/impress/account/*"], "webOrigins": [], "notBefore": 0, "bearerOnly": false, diff --git a/src/frontend/apps/impress/.env.development b/src/frontend/apps/impress/.env.development new file mode 100644 index 00000000..96383c2e --- /dev/null +++ b/src/frontend/apps/impress/.env.development @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=http://localhost:8071/api/v1.0/ diff --git a/src/frontend/apps/impress/.env.production b/src/frontend/apps/impress/.env.production new file mode 100644 index 00000000..ba16e0cb --- /dev/null +++ b/src/frontend/apps/impress/.env.production @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=https://desk-staging.beta.numerique.gouv.fr/api/v1.0/ diff --git a/src/frontend/apps/impress/.env.test b/src/frontend/apps/impress/.env.test new file mode 100644 index 00000000..eda49a1b --- /dev/null +++ b/src/frontend/apps/impress/.env.test @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=/api/ diff --git a/src/frontend/apps/impress/.eslintrc.js b/src/frontend/apps/impress/.eslintrc.js new file mode 100644 index 00000000..41f13b7c --- /dev/null +++ b/src/frontend/apps/impress/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + root: true, + extends: ['impress/next'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + settings: { + next: { + rootDir: __dirname, + }, + }, + ignorePatterns: ['node_modules', '.eslintrc.js'], +}; diff --git a/src/frontend/apps/impress/.gitignore b/src/frontend/apps/impress/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/src/frontend/apps/impress/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/src/frontend/apps/impress/README.md b/src/frontend/apps/impress/README.md new file mode 100644 index 00000000..c4033664 --- /dev/null +++ b/src/frontend/apps/impress/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/src/frontend/apps/impress/conf/default.conf b/src/frontend/apps/impress/conf/default.conf new file mode 100644 index 00000000..fe8f1cb9 --- /dev/null +++ b/src/frontend/apps/impress/conf/default.conf @@ -0,0 +1,15 @@ +server { + listen 8080; + server_name localhost; + + root /usr/share/nginx/html; + + location / { + try_files $uri index.html $uri/ =404; + } + + error_page 404 /404.html; + location = /404.html { + internal; + } +} diff --git a/src/frontend/apps/impress/cunningham.ts b/src/frontend/apps/impress/cunningham.ts new file mode 100644 index 00000000..a179392f --- /dev/null +++ b/src/frontend/apps/impress/cunningham.ts @@ -0,0 +1,373 @@ +const config = { + themes: { + default: { + theme: { + colors: { + 'card-border': '#DDDDDD', + 'primary-bg': '#FAFAFA', + 'primary-100': '#EDF5FA', + 'primary-150': '#E5EEFA', + 'info-150': '#E5EEFA', + }, + font: { + sizes: { + ml: '0.938rem', + xl: '1.50rem', + t: '0.6875rem', + s: '0.75rem', + h1: '2.2rem', + h2: '1.7rem', + h3: '1.37rem', + h4: '1.15rem', + h5: '1rem', + h6: '0.87rem', + }, + weights: { + thin: 100, + extrabold: 800, + black: 900, + }, + }, + spacings: { + '0': '0', + none: '0', + auto: 'auto', + bx: '2.2rem', + full: '100%', + }, + breakpoints: { + xxs: '320px', + xs: '480px', + }, + }, + components: { + datagrid: { + header: { + weight: 'var(--c--theme--font--weights--extrabold)', + size: 'var(--c--theme--font--sizes--ml)', + }, + cell: { + color: 'var(--c--theme--colors--primary-500)', + size: 'var(--c--theme--font--sizes--ml)', + }, + }, + 'forms-checkbox': { + 'background-color': { + hover: '#055fd214', + }, + color: 'var(--c--theme--colors--primary-500)', + 'font-size': 'var(--c--theme--font--sizes--ml)', + }, + 'forms-datepicker': { + 'border-color': 'var(--c--theme--colors--primary-500)', + 'value-color': 'var(--c--theme--colors--primary-500)', + 'border-radius': { + hover: 'var(--c--components--forms-datepicker--border-radius)', + focus: 'var(--c--components--forms-datepicker--border-radius)', + }, + }, + 'forms-field': { + color: 'var(--c--theme--colors--primary-500)', + 'value-color': 'var(--c--theme--colors--primary-500)', + width: 'auto', + }, + 'forms-input': { + 'value-color': 'var(--c--theme--colors--primary-500)', + 'border-color': 'var(--c--theme--colors--primary-500)', + color: { + error: 'var(--c--theme--colors--danger-500)', + 'error-hover': 'var(--c--theme--colors--danger-500)', + 'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)', + }, + }, + 'forms-labelledbox': { + 'label-color': { + small: 'var(--c--theme--colors--primary-500)', + 'small-disabled': 'var(--c--theme--colors--greyscale-400)', + big: { + disabled: 'var(--c--theme--colors--greyscale-400)', + }, + }, + }, + 'forms-select': { + 'border-color': 'var(--c--theme--colors--primary-500)', + 'border-color-disabled-hover': + 'var(--c--theme--colors--greyscale-200)', + 'border-radius': { + hover: 'var(--c--components--forms-select--border-radius)', + focus: 'var(--c--components--forms-select--border-radius)', + }, + 'font-size': 'var(--c--theme--font--sizes--ml)', + 'menu-background-color': '#ffffff', + 'item-background-color': { + hover: 'var(--c--theme--colors--primary-300)', + }, + }, + 'forms-switch': { + 'accent-color': 'var(--c--theme--colors--primary-400)', + }, + 'forms-textarea': { + 'border-color': 'var(--c--components--forms-textarea--border-color)', + 'border-color-hover': + 'var(--c--components--forms-textarea--border-color)', + 'border-radius': { + hover: 'var(--c--components--forms-textarea--border-radius)', + focus: 'var(--c--components--forms-textarea--border-radius)', + }, + color: 'var(--c--theme--colors--primary-500)', + disabled: { + 'border-color-hover': 'var(--c--theme--colors--greyscale-200)', + }, + }, + modal: { + 'background-color': '#ffffff', + }, + button: { + 'border-radius': { + active: 'var(--c--components--button--border-radius)', + }, + 'medium-height': 'auto', + 'small-height': 'auto', + success: { + color: 'white', + 'color-disabled': 'white', + 'color-hover': 'white', + background: { + color: 'var(--c--theme--colors--success-600)', + 'color-disabled': 'var(--c--theme--colors--greyscale-300)', + 'color-hover': 'var(--c--theme--colors--success-800)', + }, + }, + danger: { + 'color-hover': 'white', + background: { + color: 'var(--c--theme--colors--danger-400)', + 'color-hover': 'var(--c--theme--colors--danger-500)', + 'color-disabled': 'var(--c--theme--colors--danger-100)', + }, + }, + primary: { + color: 'var(--c--theme--colors--primary-text)', + 'color-active': 'var(--c--theme--colors--primary-text)', + background: { + color: 'var(--c--theme--colors--primary-400)', + 'color-active': 'var(--c--theme--colors--primary-500)', + }, + border: { + 'color-active': 'transparent', + }, + }, + secondary: { + color: 'var(--c--theme--colors--primary-500)', + 'color-hover': 'var(--c--theme--colors--primary-text)', + background: { + color: 'white', + 'color-hover': 'var(--c--theme--colors--primary-700)', + }, + border: { + color: 'var(--c--theme--colors--primary-200)', + }, + }, + tertiary: { + color: 'var(--c--theme--colors--primary-text)', + 'color-disabled': 'var(--c--theme--colors--greyscale-600)', + background: { + 'color-hover': 'var(--c--theme--colors--primary-100)', + 'color-disabled': 'var(--c--theme--colors--greyscale-200)', + }, + }, + disabled: { + color: 'white', + background: { + color: '#b3cef0', + }, + }, + }, + }, + }, + dsfr: { + theme: { + colors: { + 'card-border': '#DDDDDD', + 'primary-text': '#000091', + 'primary-100': '#f5f5fe', + 'primary-150': '#F4F4FD', + 'primary-200': '#ececfe', + 'primary-300': '#e3e3fd', + 'primary-400': '#cacafb', + 'primary-500': '#6a6af4', + 'primary-600': '#000091', + 'primary-700': '#272747', + 'primary-800': '#21213f', + 'primary-900': '#1c1a36', + 'secondary-text': '#FFFFFF', + 'secondary-100': '#fee9ea', + 'secondary-200': '#fedfdf', + 'secondary-300': '#fdbfbf', + 'secondary-400': '#e1020f', + 'secondary-500': '#c91a1f', + 'secondary-600': '#5e2b2b', + 'secondary-700': '#3b2424', + 'secondary-800': '#341f1f', + 'secondary-900': '#2b1919', + 'greyscale-text': '#303C4B', + 'greyscale-000': '#f6f6f6', + 'greyscale-100': '#eeeeee', + 'greyscale-200': '#e5e5e5', + 'greyscale-300': '#e1e1e1', + 'greyscale-400': '#dddddd', + 'greyscale-500': '#cecece', + 'greyscale-600': '#7b7b7b', + 'greyscale-700': '#666666', + 'greyscale-800': '#2a2a2a', + 'greyscale-900': '#1e1e1e', + 'success-text': '#1f8d49', + 'success-100': '#dffee6', + 'success-200': '#b8fec9', + 'success-300': '#88fdaa', + 'success-400': '#3bea7e', + 'success-500': '#1f8d49', + 'success-600': '#18753c', + 'success-700': '#204129', + 'success-800': '#1e2e22', + 'success-900': '#19281d', + 'info-text': '#0078f3', + 'info-100': '#f4f6ff', + 'info-200': '#e8edff', + 'info-300': '#dde5ff', + 'info-400': '#bdcdff', + 'info-500': '#0078f3', + 'info-600': '#0063cb', + 'info-700': '#f4f6ff', + 'info-800': '#222a3f', + 'info-900': '#1d2437', + 'warning-text': '#d64d00', + 'warning-100': '#fff4f3', + 'warning-200': '#ffe9e6', + 'warning-300': '#ffded9', + 'warning-400': '#ffbeb4', + 'warning-500': '#d64d00', + 'warning-600': '#b34000', + 'warning-700': '#5e2c21', + 'warning-800': '#3e241e', + 'warning-900': '#361e19', + 'danger-text': '#e1000f', + 'danger-100': '#fef4f4', + 'danger-200': '#fee9e9', + 'danger-300': '#fddede', + 'danger-400': '#fcbfbf', + 'danger-500': '#e1000f', + 'danger-600': '#c9191e', + 'danger-700': '#642727', + 'danger-800': '#412121', + 'danger-900': '#3a1c1c', + }, + font: { + families: { + accent: 'Marianne', + base: 'Marianne', + }, + }, + }, + components: { + alert: { + 'border-radius': '0', + }, + button: { + 'medium-height': '48px', + 'border-radius': '4px', + primary: { + background: { + color: 'var(--c--theme--colors--primary-text)', + 'color-hover': '#1212ff', + 'color-active': '#2323ff', + }, + color: '#ffffff', + 'color-hover': '#ffffff', + 'color-active': '#ffffff', + }, + 'primary-text': { + background: { + 'color-hover': 'var(--c--theme--colors--primary-100)', + 'color-active': 'var(--c--theme--colors--primary-100)', + }, + 'color-hover': 'var(--c--theme--colors--primary-text)', + }, + secondary: { + background: { + 'color-hover': '#F6F6F6', + 'color-active': '#EDEDED', + }, + border: { + color: 'var(--c--theme--colors--primary-600)', + 'color-hover': 'var(--c--theme--colors--primary-600)', + }, + color: 'var(--c--theme--colors--primary-text)', + }, + 'tertiary-text': { + background: { + 'color-hover': 'var(--c--theme--colors--primary-100)', + }, + 'color-hover': 'var(--c--theme--colors--primary-text)', + }, + }, + datagrid: { + header: { + color: 'var(--c--theme--colors--primary-text)', + size: 'var(--c--theme--font--sizes--s)', + }, + body: { + 'background-color': 'transparent', + 'background-color-hover': '#F4F4FD', + }, + pagination: { + 'background-color': 'transparent', + 'background-color-active': 'var(--c--theme--colors--primary-300)', + }, + }, + 'forms-checkbox': { + 'border-radius': '0', + color: 'var(--c--theme--colors--primary-text)', + }, + 'forms-datepicker': { + 'border-radius': '0', + }, + 'forms-fileuploader': { + 'border-radius': '0', + }, + 'forms-field': { + color: 'var(--c--theme--colors--primary-text)', + }, + 'forms-input': { + 'border-radius': '4px', + 'background-color': '#ffffff', + 'border-color': 'var(--c--theme--colors--primary-text)', + 'box-shadow-color': 'var(--c--theme--colors--primary-text)', + 'value-color': 'var(--c--theme--colors--primary-text)', + }, + 'forms-labelledbox': { + 'label-color': { + big: 'var(--c--theme--colors--primary-text)', + }, + }, + 'forms-select': { + 'border-radius': '4px', + 'border-radius-hover': '4px', + 'background-color': '#ffffff', + 'border-color': 'var(--c--theme--colors--primary-text)', + 'border-color-hover': 'var(--c--theme--colors--primary-text)', + 'box-shadow-color': 'var(--c--theme--colors--primary-text)', + }, + 'forms-switch': { + 'handle-border-radius': '2px', + 'rail-border-radius': '4px', + }, + 'forms-textarea': { + 'border-radius': '0', + }, + }, + }, + }, +}; + +export default config; diff --git a/src/frontend/apps/impress/jest.config.ts b/src/frontend/apps/impress/jest.config.ts new file mode 100644 index 00000000..e77e0b1a --- /dev/null +++ b/src/frontend/apps/impress/jest.config.ts @@ -0,0 +1,30 @@ +import type { Config } from 'jest'; +import nextJest from 'next/jest.js'; + +const createJestConfig = nextJest({ + dir: './', +}); + +// Add any custom config to be passed to Jest +const config: Config = { + coverageProvider: 'v8', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + setupFilesAfterEnv: ['/jest.setup.ts'], + testEnvironment: 'jsdom', +}; + +const jestConfig = async () => { + const nextJestConfig = await createJestConfig(config)(); + return { + ...nextJestConfig, + moduleNameMapper: { + '\\.svg$': '/jest/mocks/svg.js', + '^.+\\.svg\\?url$': `/jest/mocks/fileMock.js`, + ...nextJestConfig.moduleNameMapper, + }, + }; +}; + +export default jestConfig; diff --git a/src/frontend/apps/impress/jest.setup.ts b/src/frontend/apps/impress/jest.setup.ts new file mode 100644 index 00000000..3aa7aec9 --- /dev/null +++ b/src/frontend/apps/impress/jest.setup.ts @@ -0,0 +1,3 @@ +import * as dotenv from 'dotenv'; + +dotenv.config({ path: './.env.test' }); diff --git a/src/frontend/apps/impress/jest/mocks/fileMock.js b/src/frontend/apps/impress/jest/mocks/fileMock.js new file mode 100644 index 00000000..28a24984 --- /dev/null +++ b/src/frontend/apps/impress/jest/mocks/fileMock.js @@ -0,0 +1,16 @@ +module.exports = { + src: '/img.jpg', + height: 40, + width: 40, + blurDataURL: 'data:image/png;base64,imagedata', +}; + +if ( + (typeof exports.default === 'function' || + (typeof exports.default === 'object' && exports.default !== null)) && + typeof exports.default.__esModule === 'undefined' +) { + Object.defineProperty(exports.default, '__esModule', { value: true }); + Object.assign(exports.default, exports); + module.exports = exports.default; +} diff --git a/src/frontend/apps/impress/jest/mocks/svg.js b/src/frontend/apps/impress/jest/mocks/svg.js new file mode 100644 index 00000000..0b7fc5b8 --- /dev/null +++ b/src/frontend/apps/impress/jest/mocks/svg.js @@ -0,0 +1,3 @@ +const nameMock = 'svg'; +export default nameMock; +export const ReactComponent = 'svg'; diff --git a/src/frontend/apps/impress/next.config.js b/src/frontend/apps/impress/next.config.js new file mode 100644 index 00000000..d8ac8ba8 --- /dev/null +++ b/src/frontend/apps/impress/next.config.js @@ -0,0 +1,40 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + images: { + unoptimized: true, + }, + compiler: { + // Enables the styled-components SWC transform + styledComponents: true, + }, + webpack(config) { + // Grab the existing rule that handles SVG imports + const fileLoaderRule = config.module.rules.find((rule) => + rule.test?.test?.('.svg'), + ); + + config.module.rules.push( + // Reapply the existing rule, but only for svg imports ending in ?url + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, // *.svg?url + }, + // Convert all other *.svg imports to React components + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url + use: ['@svgr/webpack'], + }, + ); + + // Modify the file loader rule to ignore *.svg, since we have it handled now. + fileLoaderRule.exclude = /\.svg$/i; + + return config; + }, +}; + +module.exports = nextConfig; diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json new file mode 100644 index 00000000..0d3ed039 --- /dev/null +++ b/src/frontend/apps/impress/package.json @@ -0,0 +1,56 @@ +{ + "name": "app-impress", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "prettier --check . && yarn stylelint && next build", + "build:ci": "cp .env.development .env.local && yarn build", + "build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes", + "start": "npx -y serve@latest out", + "lint": "next lint", + "prettier": "prettier --write .", + "stylelint": "stylelint \"**/*.css\"", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@openfun/cunningham-react": "2.7.0", + "@tanstack/react-query": "5.28.9", + "i18next": "23.10.1", + "lodash": "4.17.21", + "luxon": "3.4.4", + "next": "14.1.4", + "react": "18.2.0", + "react-aria-components": "1.1.1", + "react-dom": "18.2.0", + "react-i18next": "14.1.0", + "react-select": "5.8.0", + "styled-components": "6.1.8", + "zustand": "4.5.2" + }, + "devDependencies": { + "@svgr/webpack": "8.1.0", + "@tanstack/react-query-devtools": "5.28.10", + "@testing-library/jest-dom": "6.4.2", + "@testing-library/react": "14.2.2", + "@testing-library/user-event": "14.5.2", + "@types/jest": "29.5.12", + "@types/lodash": "4.17.0", + "@types/luxon": "3.4.2", + "@types/node": "*", + "@types/react": "18.2.73", + "@types/react-dom": "*", + "dotenv": "16.4.5", + "eslint-config-impress": "*", + "fetch-mock": "9.11.0", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", + "node-fetch": "2.7.0", + "prettier": "3.2.5", + "stylelint": "16.3.1", + "stylelint-config-standard": "36.0.0", + "stylelint-prettier": "5.0.0", + "typescript": "*" + } +} diff --git a/src/frontend/apps/impress/public/favicon.ico b/src/frontend/apps/impress/public/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/src/frontend/apps/impress/public/favicon.ico differ diff --git a/src/frontend/apps/impress/src/__tests__/pages.test.tsx b/src/frontend/apps/impress/src/__tests__/pages.test.tsx new file mode 100644 index 00000000..ce4573e9 --- /dev/null +++ b/src/frontend/apps/impress/src/__tests__/pages.test.tsx @@ -0,0 +1,18 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; + +import { AppWrapper } from '@/tests/utils'; + +import Page from '../pages'; + +describe('Page', () => { + it('checks Page rendering', () => { + render(, { wrapper: AppWrapper }); + + expect( + screen.getByRole('button', { + name: /Create a new team/i, + }), + ).toBeInTheDocument(); + }); +}); diff --git a/src/frontend/apps/impress/src/api/APIError.ts b/src/frontend/apps/impress/src/api/APIError.ts new file mode 100644 index 00000000..2aaf715c --- /dev/null +++ b/src/frontend/apps/impress/src/api/APIError.ts @@ -0,0 +1,20 @@ +interface IAPIError { + status: number; + cause?: string[]; + data?: T; +} + +export class APIError extends Error implements IAPIError { + public status: IAPIError['status']; + public cause?: IAPIError['cause']; + public data?: IAPIError['data']; + + constructor(message: string, { status, cause, data }: IAPIError) { + super(message); + + this.name = 'APIError'; + this.status = status; + this.cause = cause; + this.data = data; + } +} diff --git a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx new file mode 100644 index 00000000..72077d6e --- /dev/null +++ b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx @@ -0,0 +1,58 @@ +import fetchMock from 'fetch-mock'; + +import { fetchAPI } from '@/api'; +import { useAuthStore } from '@/core/auth'; + +describe('fetchAPI', () => { + beforeEach(() => { + process.env.NEXT_PUBLIC_API_URL = 'http://some.api.url/api/v1.0/'; + fetchMock.restore(); + }); + + it('adds correctly the basename', () => { + fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200); + + void fetchAPI('some/url'); + + expect(fetchMock.lastUrl()).toEqual( + 'http://some.api.url/api/v1.0/some/url', + ); + }); + + it('adds the credentials automatically', () => { + fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200); + + void fetchAPI('some/url', { body: 'some body' }); + + expect(fetchMock.lastOptions()).toEqual({ + body: 'some body', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + it('logout if 401 response', async () => { + const mockReplace = jest.fn(); + Object.defineProperty(window, 'location', { + configurable: true, + enumerable: true, + value: { + replace: mockReplace, + }, + }); + + useAuthStore.setState({ userData: { email: 'test@test.com' } }); + + fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401); + + await fetchAPI('some/url'); + + expect(useAuthStore.getState().userData).toBeUndefined(); + + expect(mockReplace).toHaveBeenCalledWith( + 'http://some.api.url/api/v1.0/authenticate/', + ); + }); +}); diff --git a/src/frontend/apps/impress/src/api/fetchApi.ts b/src/frontend/apps/impress/src/api/fetchApi.ts new file mode 100644 index 00000000..c2e2e2b8 --- /dev/null +++ b/src/frontend/apps/impress/src/api/fetchApi.ts @@ -0,0 +1,41 @@ +import { login, useAuthStore } from '@/core/auth'; + +/** + * Retrieves the CSRF token from the document's cookies. + * + * @returns {string|null} The CSRF token if found in the cookies, or null if not present. + */ +function getCSRFToken() { + return document.cookie + .split(';') + .filter((cookie) => cookie.trim().startsWith('csrftoken=')) + .map((cookie) => cookie.split('=')[1]) + .pop(); +} + +export const fetchAPI = async (input: string, init?: RequestInit) => { + const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}${input}`; + const { logout } = useAuthStore.getState(); + + const csrfToken = getCSRFToken(); + + const response = await fetch(apiUrl, { + ...init, + credentials: 'include', + headers: { + ...init?.headers, + 'Content-Type': 'application/json', + ...(csrfToken && { 'X-CSRFToken': csrfToken }), + }, + }); + + // todo - handle 401, redirect to login screen + // todo - please have a look to this documentation page https://mozilla-django-oidc.readthedocs.io/en/stable/xhr.html + if (response.status === 401) { + logout(); + // Fix - force re-logging the user, will be refactored + login(); + } + + return response; +}; diff --git a/src/frontend/apps/impress/src/api/index.ts b/src/frontend/apps/impress/src/api/index.ts new file mode 100644 index 00000000..c8c14e47 --- /dev/null +++ b/src/frontend/apps/impress/src/api/index.ts @@ -0,0 +1,4 @@ +export * from './APIError'; +export * from './fetchApi'; +export * from './types'; +export * from './utils'; diff --git a/src/frontend/apps/impress/src/api/types.ts b/src/frontend/apps/impress/src/api/types.ts new file mode 100644 index 00000000..5c48a797 --- /dev/null +++ b/src/frontend/apps/impress/src/api/types.ts @@ -0,0 +1,6 @@ +export interface APIList { + count: number; + next?: string | null; + previous?: string | null; + results: T[]; +} diff --git a/src/frontend/apps/impress/src/api/utils.ts b/src/frontend/apps/impress/src/api/utils.ts new file mode 100644 index 00000000..5a4370e1 --- /dev/null +++ b/src/frontend/apps/impress/src/api/utils.ts @@ -0,0 +1,18 @@ +export const errorCauses = async (response: Response, data?: unknown) => { + const errorsBody = (await response.json()) as Record< + string, + string | string[] + > | null; + + const causes = errorsBody + ? Object.entries(errorsBody) + .map(([, value]) => value) + .flat() + : undefined; + + return { + status: response.status, + cause: causes, + data, + }; +}; diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Bold.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Bold.woff new file mode 100644 index 00000000..335e3488 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Bold.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff new file mode 100644 index 00000000..72878fca Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-ExtraBold.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-ExtraBold.woff new file mode 100644 index 00000000..ad85a552 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-ExtraBold.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff new file mode 100644 index 00000000..af92ea7c Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Light.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Light.woff new file mode 100644 index 00000000..9ef49df3 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Light.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Light_Italic.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Light_Italic.woff new file mode 100644 index 00000000..5ae0dbea Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Light_Italic.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Medium.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Medium.woff new file mode 100644 index 00000000..692476eb Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Medium.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Medium_Italic.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Medium_Italic.woff new file mode 100644 index 00000000..8e32b88f Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Medium_Italic.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Regular.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Regular.woff new file mode 100644 index 00000000..a95a2d04 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Regular.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff new file mode 100644 index 00000000..1880c5d7 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Thin.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Thin.woff new file mode 100644 index 00000000..6c8bc5a0 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Thin.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Thin_Italic.woff b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Thin_Italic.woff new file mode 100644 index 00000000..4c23d6b9 Binary files /dev/null and b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-Thin_Italic.woff differ diff --git a/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-font.css b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-font.css new file mode 100644 index 00000000..f8183f9e --- /dev/null +++ b/src/frontend/apps/impress/src/assets/fonts/Marianne/Marianne-font.css @@ -0,0 +1,77 @@ +@font-face { + font-family: Marianne; + src: url('Marianne-Thin.woff') format('truetype'); + font-weight: 100; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Thin_Italic.woff') format('truetype'); + font-weight: 100; + font-style: italic; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Light.woff') format('truetype'); + font-weight: 300; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Light_Italic.woff') format('truetype'); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Regular.woff') format('truetype'); + font-weight: 400; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Regular_Italic.woff') format('truetype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Medium.woff') format('truetype'); + font-weight: 500; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Medium_Italic.woff') format('truetype'); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Bold.woff') format('truetype'); + font-weight: 700; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-Bold_Italic.woff') format('truetype'); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-ExtraBold.woff') format('truetype'); + font-weight: 800; +} + +@font-face { + font-family: Marianne; + src: url('Marianne-ExtraBold_Italic.woff') format('truetype'); + font-weight: 800; + font-style: italic; +} diff --git a/src/frontend/apps/impress/src/assets/icons/icon-404.svg b/src/frontend/apps/impress/src/assets/icons/icon-404.svg new file mode 100644 index 00000000..fe705719 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/icon-404.svg @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/src/frontend/apps/impress/src/assets/icons/icon-group.svg b/src/frontend/apps/impress/src/assets/icons/icon-group.svg new file mode 100644 index 00000000..c2a7d3c3 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/icon-group.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/icon-group2.svg b/src/frontend/apps/impress/src/assets/icons/icon-group2.svg new file mode 100644 index 00000000..845da482 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/icon-group2.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/icon-user.svg b/src/frontend/apps/impress/src/assets/icons/icon-user.svg new file mode 100644 index 00000000..ff518b51 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/icon-user.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/frontend/apps/impress/src/components/Box.tsx b/src/frontend/apps/impress/src/components/Box.tsx new file mode 100644 index 00000000..dd82c5f7 --- /dev/null +++ b/src/frontend/apps/impress/src/components/Box.tsx @@ -0,0 +1,46 @@ +import { ComponentPropsWithRef, ReactHTML } from 'react'; +import styled from 'styled-components'; +import { CSSProperties } from 'styled-components/dist/types'; + +export interface BoxProps { + as?: keyof ReactHTML; + $align?: CSSProperties['alignItems']; + $background?: CSSProperties['background']; + $color?: CSSProperties['color']; + $css?: string; + $direction?: CSSProperties['flexDirection']; + $display?: CSSProperties['display']; + $flex?: boolean; + $gap?: CSSProperties['gap']; + $height?: CSSProperties['height']; + $justify?: CSSProperties['justifyContent']; + $overflow?: CSSProperties['overflow']; + $position?: CSSProperties['position']; + $radius?: CSSProperties['borderRadius']; + $width?: CSSProperties['width']; + $maxWidth?: CSSProperties['maxWidth']; + $minWidth?: CSSProperties['minWidth']; +} + +export type BoxType = ComponentPropsWithRef; + +export const Box = styled('div')` + display: flex; + flex-direction: column; + ${({ $align }) => $align && `align-items: ${$align};`} + ${({ $background }) => $background && `background: ${$background};`} + ${({ $color }) => $color && `color: ${$color};`} + ${({ $direction }) => $direction && `flex-direction: ${$direction};`} + ${({ $display }) => $display && `display: ${$display};`} + ${({ $flex }) => $flex === false && `display: block;`} + ${({ $gap }) => $gap && `gap: ${$gap};`} + ${({ $height }) => $height && `height: ${$height};`} + ${({ $justify }) => $justify && `justify-content: ${$justify};`} + ${({ $overflow }) => $overflow && `overflow: ${$overflow};`} + ${({ $position }) => $position && `position: ${$position};`} + ${({ $radius }) => $radius && `border-radius: ${$radius};`} + ${({ $width }) => $width && `width: ${$width};`} + ${({ $maxWidth }) => $maxWidth && `max-width: ${$maxWidth};`} + ${({ $minWidth }) => $minWidth && `min-width: ${$minWidth};`} + ${({ $css }) => $css && `${$css};`} +`; diff --git a/src/frontend/apps/impress/src/components/BoxButton.tsx b/src/frontend/apps/impress/src/components/BoxButton.tsx new file mode 100644 index 00000000..893af9d9 --- /dev/null +++ b/src/frontend/apps/impress/src/components/BoxButton.tsx @@ -0,0 +1,41 @@ +import { ComponentPropsWithRef, forwardRef } from 'react'; + +import { Box, BoxType } from './Box'; + +export type BoxButtonType = ComponentPropsWithRef; + +/** + * Styleless button that extends the Box component. + * Good to wrap around SVGs or other elements that need to be clickable. + * @param props - @see BoxType props + * @param ref + * @see Box + * @example + * ```tsx + * console.log('clicked')}> + * Click me + * + * ``` + */ +const BoxButton = forwardRef( + ({ $css, ...props }, ref) => { + return ( + + ); + }, +); + +BoxButton.displayName = 'BoxButton'; +export { BoxButton }; diff --git a/src/frontend/apps/impress/src/components/Card.tsx b/src/frontend/apps/impress/src/components/Card.tsx new file mode 100644 index 00000000..e59ed745 --- /dev/null +++ b/src/frontend/apps/impress/src/components/Card.tsx @@ -0,0 +1,28 @@ +import { PropsWithChildren } from 'react'; + +import { useCunninghamTheme } from '@/cunningham'; + +import { Box, BoxType } from '.'; + +export const Card = ({ + children, + $css, + ...props +}: PropsWithChildren) => { + const { colorsTokens } = useCunninghamTheme(); + + return ( + + {children} + + ); +}; diff --git a/src/frontend/apps/impress/src/components/DropButton.tsx b/src/frontend/apps/impress/src/components/DropButton.tsx new file mode 100644 index 00000000..2493c9c9 --- /dev/null +++ b/src/frontend/apps/impress/src/components/DropButton.tsx @@ -0,0 +1,67 @@ +import React, { + PropsWithChildren, + ReactNode, + useEffect, + useState, +} from 'react'; +import { Button, DialogTrigger, Popover } from 'react-aria-components'; +import styled from 'styled-components'; + +const StyledPopover = styled(Popover)` + background-color: white; + border-radius: 4px; + box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); + padding: 0.5rem; + border: 1px solid #dddddd; + opacity: 0; + transition: opacity 0.2s ease-in-out; +`; + +const StyledButton = styled(Button)` + cursor: pointer; + border: none; + background: none; + outline: none; + transition: all 0.2s ease-in-out; +`; + +interface DropButtonProps { + button: ReactNode; + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; +} + +export const DropButton = ({ + button, + isOpen = false, + onOpenChange, + children, +}: PropsWithChildren) => { + const [opacity, setOpacity] = useState(false); + const [isLocalOpen, setIsLocalOpen] = useState(isOpen); + + useEffect(() => { + setIsLocalOpen(isOpen); + }, [isOpen]); + + const onOpenChangeHandler = (isOpen: boolean) => { + setIsLocalOpen(isOpen); + onOpenChange?.(isOpen); + setTimeout(() => { + setOpacity(isOpen); + }, 10); + }; + + return ( + + {button} + + {children} + + + ); +}; diff --git a/src/frontend/apps/impress/src/components/IconOptions.tsx b/src/frontend/apps/impress/src/components/IconOptions.tsx new file mode 100644 index 00000000..a6b26569 --- /dev/null +++ b/src/frontend/apps/impress/src/components/IconOptions.tsx @@ -0,0 +1,22 @@ +import { Text } from '@/components'; + +interface IconOptionsProps { + isOpen: boolean; + 'aria-label': string; +} + +export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => { + return ( + + more_vert + + ); +}; diff --git a/src/frontend/apps/impress/src/components/InfiniteScroll.tsx b/src/frontend/apps/impress/src/components/InfiniteScroll.tsx new file mode 100644 index 00000000..112a1055 --- /dev/null +++ b/src/frontend/apps/impress/src/components/InfiniteScroll.tsx @@ -0,0 +1,55 @@ +import { PropsWithChildren, useEffect, useRef } from 'react'; + +import { Box, BoxType } from '@/components'; + +interface InfiniteScrollProps extends BoxType { + hasMore: boolean; + isLoading: boolean; + next: () => void; + scrollContainer: HTMLElement | null; +} + +export const InfiniteScroll = ({ + children, + hasMore, + isLoading, + next, + scrollContainer, + ...boxProps +}: PropsWithChildren) => { + const timeout = useRef>(); + + useEffect(() => { + if (!scrollContainer) { + return; + } + + const nextHandle = () => { + if (!hasMore || isLoading) { + return; + } + + // To not wait until the end of the scroll to load more data + const heightFromBottom = 150; + + const { scrollTop, clientHeight, scrollHeight } = scrollContainer; + if (scrollTop + clientHeight >= scrollHeight - heightFromBottom) { + next(); + } + }; + + const handleScroll = () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + timeout.current = setTimeout(nextHandle, 50); + }; + + scrollContainer.addEventListener('scroll', handleScroll); + return () => { + scrollContainer.removeEventListener('scroll', handleScroll); + }; + }, [hasMore, isLoading, next, scrollContainer]); + + return {children}; +}; diff --git a/src/frontend/apps/impress/src/components/Link.tsx b/src/frontend/apps/impress/src/components/Link.tsx new file mode 100644 index 00000000..8081db2c --- /dev/null +++ b/src/frontend/apps/impress/src/components/Link.tsx @@ -0,0 +1,11 @@ +import Link from 'next/link'; +import styled from 'styled-components'; + +export const StyledLink = styled(Link)` + text-decoration: none; + color: #ffffff33; + &[aria-current='page'] { + color: #ffffff; + } + display: flex; +`; diff --git a/src/frontend/apps/impress/src/components/Text.tsx b/src/frontend/apps/impress/src/components/Text.tsx new file mode 100644 index 00000000..e2cce1a4 --- /dev/null +++ b/src/frontend/apps/impress/src/components/Text.tsx @@ -0,0 +1,60 @@ +import { CSSProperties, ComponentPropsWithRef, ReactHTML } from 'react'; +import styled from 'styled-components'; + +import { tokens } from '@/cunningham'; + +import { Box, BoxProps } from './Box'; + +const { sizes } = tokens.themes.default.theme.font; +type TextSizes = keyof typeof sizes; + +export interface TextProps extends BoxProps { + as?: keyof Pick< + ReactHTML, + 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' + >; + $weight?: CSSProperties['fontWeight']; + $textAlign?: CSSProperties['textAlign']; + // eslint-disable-next-line @typescript-eslint/ban-types + $size?: TextSizes | (string & {}); + $theme?: + | 'primary' + | 'secondary' + | 'info' + | 'success' + | 'warning' + | 'danger' + | 'greyscale'; + $variation?: + | 'text' + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900'; +} + +export type TextType = ComponentPropsWithRef; + +export const TextStyled = styled(Box)` + ${({ $textAlign }) => $textAlign && `text-align: ${$textAlign};`} + ${({ $weight }) => $weight && `font-weight: ${$weight};`} + ${({ $size }) => + $size && + `font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`} + ${({ $theme, $variation }) => + `color: var(--c--theme--colors--${$theme}-${$variation});`} + ${({ $color }) => $color && `color: ${$color};`} +`; + +export const Text = ({ + ...props +}: ComponentPropsWithRef) => { + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/components/TextErrors.tsx b/src/frontend/apps/impress/src/components/TextErrors.tsx new file mode 100644 index 00000000..6c1d0c17 --- /dev/null +++ b/src/frontend/apps/impress/src/components/TextErrors.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; + +import { Box, Text, TextType } from '@/components'; + +interface TextErrorsProps extends TextType { + causes?: string[]; + defaultMessage?: string; +} + +export const TextErrors = ({ + causes, + defaultMessage, + ...textProps +}: TextErrorsProps) => { + const { t } = useTranslation(); + + return ( + + {causes && + causes.map((cause, i) => ( + + {cause} + + ))} + + {!causes && ( + + {defaultMessage || t('Something bad happens, please retry.')} + + )} + + ); +}; diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts new file mode 100644 index 00000000..fa6ac7b1 --- /dev/null +++ b/src/frontend/apps/impress/src/components/index.ts @@ -0,0 +1,8 @@ +export * from './Box'; +export * from './BoxButton'; +export * from './Card'; +export * from './DropButton'; +export * from './IconOptions'; +export * from './Link'; +export * from './Text'; +export * from './TextErrors'; diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx new file mode 100644 index 00000000..1d5fbe87 --- /dev/null +++ b/src/frontend/apps/impress/src/core/AppProvider.tsx @@ -0,0 +1,36 @@ +import { CunninghamProvider } from '@openfun/cunningham-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +import { useCunninghamTheme } from '@/cunningham'; +import '@/i18n/initI18n'; + +import { Auth } from './auth/Auth'; + +/** + * QueryClient: + * - defaultOptions: + * - staleTime: + * - global cache duration - we decided 3 minutes + * - It can be overridden to each query + */ +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 3, + }, + }, +}); + +export function AppProvider({ children }: { children: React.ReactNode }) { + const { theme } = useCunninghamTheme(); + + return ( + + + + {children} + + + ); +} diff --git a/src/frontend/apps/impress/src/core/MainLayout.tsx b/src/frontend/apps/impress/src/core/MainLayout.tsx new file mode 100644 index 00000000..39638e6f --- /dev/null +++ b/src/frontend/apps/impress/src/core/MainLayout.tsx @@ -0,0 +1,17 @@ +import { Box } from '@/components'; +import { HEADER_HEIGHT, Header } from '@/features/header'; +import { Menu } from '@/features/menu'; + +export function MainLayout({ children }: { children: React.ReactNode }) { + return ( + +
+ + + + {children} + + + + ); +} diff --git a/src/frontend/apps/impress/src/core/auth/Auth.tsx b/src/frontend/apps/impress/src/core/auth/Auth.tsx new file mode 100644 index 00000000..865a815f --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/Auth.tsx @@ -0,0 +1,24 @@ +import { Loader } from '@openfun/cunningham-react'; +import { PropsWithChildren, useEffect } from 'react'; + +import { Box } from '@/components'; + +import { useAuthStore } from './useAuthStore'; + +export const Auth = ({ children }: PropsWithChildren) => { + const { authenticated, initAuth } = useAuthStore(); + + useEffect(() => { + initAuth(); + }, [initAuth]); + + if (!authenticated) { + return ( + + + + ); + } + + return children; +}; diff --git a/src/frontend/apps/impress/src/core/auth/api/getMe.tsx b/src/frontend/apps/impress/src/core/auth/api/getMe.tsx new file mode 100644 index 00000000..4c02c27b --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/api/getMe.tsx @@ -0,0 +1,21 @@ +import { fetchAPI } from '@/api'; + +import { User } from './types'; + +/** + * Asynchronously retrieves the current user's data from the API. + * This function is called during frontend initialization to check + * the user's authentication status through a session cookie. + * + * @async + * @function getMe + * @throws {Error} Throws an error if the API request fails. + * @returns {Promise} A promise that resolves to the user data. + */ +export const getMe = async (): Promise => { + const response = await fetchAPI(`users/me/`); + if (!response.ok) { + throw new Error(`Couldn't fetch user data: ${response.statusText}`); + } + return response.json() as Promise; +}; diff --git a/src/frontend/apps/impress/src/core/auth/api/index.ts b/src/frontend/apps/impress/src/core/auth/api/index.ts new file mode 100644 index 00000000..d81520d1 --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/api/index.ts @@ -0,0 +1,2 @@ +export * from './getMe'; +export * from './types'; diff --git a/src/frontend/apps/impress/src/core/auth/api/types.ts b/src/frontend/apps/impress/src/core/auth/api/types.ts new file mode 100644 index 00000000..b3836215 --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/api/types.ts @@ -0,0 +1,12 @@ +/** + * Represents user retrieved from the API. + * @interface User + * @property {string} id - The id of the user. + * @property {string} email - The email of the user. + * @property {string} name - The name of the user. + */ +export interface User { + id: string; + email: string; + name?: string; +} diff --git a/src/frontend/apps/impress/src/core/auth/index.ts b/src/frontend/apps/impress/src/core/auth/index.ts new file mode 100644 index 00000000..b04f47a6 --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/index.ts @@ -0,0 +1,3 @@ +export * from './Auth'; +export * from './useAuthStore'; +export * from './api/types'; diff --git a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx new file mode 100644 index 00000000..e2a2f3a1 --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx @@ -0,0 +1,40 @@ +import { create } from 'zustand'; + +import { User, getMe } from './api'; + +export const login = () => { + window.location.replace( + new URL('authenticate/', process.env.NEXT_PUBLIC_API_URL).href, + ); +}; + +interface AuthStore { + authenticated: boolean; + initAuth: () => void; + logout: () => void; + userData?: User; +} + +const initialState = { + authenticated: false, + userData: undefined, +}; + +export const useAuthStore = create((set) => ({ + authenticated: initialState.authenticated, + userData: initialState.userData, + + initAuth: () => { + getMe() + .then((data: User) => { + set({ authenticated: true, userData: data }); + }) + .catch(() => { + // todo - implement a proper login screen to prevent automatic navigation. + login(); + }); + }, + logout: () => { + set(initialState); + }, +})); diff --git a/src/frontend/apps/impress/src/core/index.ts b/src/frontend/apps/impress/src/core/index.ts new file mode 100644 index 00000000..6af29680 --- /dev/null +++ b/src/frontend/apps/impress/src/core/index.ts @@ -0,0 +1,2 @@ +export * from './AppProvider'; +export * from './MainLayout'; diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-custom-tokens.css b/src/frontend/apps/impress/src/cunningham/cunningham-custom-tokens.css new file mode 100644 index 00000000..885bd8ac --- /dev/null +++ b/src/frontend/apps/impress/src/cunningham/cunningham-custom-tokens.css @@ -0,0 +1,28 @@ +:root { + /** + * Input + */ + --c--components--forms-input--border-radius--hover: var( + --c--components--forms-input--border-radius + ); + --c--components--forms-input--border-radius--focus: var( + --c--components--forms-input--border-radius + ); + --c--components--forms-input--border-color--hover: var( + --c--components--forms-input--border-color + ); + + /** + * Datepicker + **/ + --c--components--forms-datepicker--border-color--hover: var( + --c--components--forms-datepicker--border-color + ); + + /** + * Select + **/ + --c--components--forms-select--value-color--disabled: var( + --c--theme--colors--greyscale-400 + ); +} diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css new file mode 100644 index 00000000..bcfa7dc3 --- /dev/null +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -0,0 +1,466 @@ +@import url('@openfun/cunningham-react/icons'); +@import url('@openfun/cunningham-react/style'); +@import url('@openfun/cunningham-react/fonts'); +@import url('./cunningham-tokens.css'); +@import url('./cunningham-custom-tokens.css'); +@import url('../assets/fonts/Marianne/Marianne-font.css'); + +.c__input, +.c__field, +.c__select, +.c__datagrid { + font-family: var(--c--theme--font--families--base); +} + +.c__field { + line-height: initial; +} + +.labelled-box label { + color: var(--c--theme--colors--primary-text); +} + +.labelled-box--disabled label { + color: var(--c--components--forms-labelledbox--label-color--small-disabled); +} + +.c__field :not(.c__textarea__wrapper, div) .labelled-box label.placeholder { + top: 50%; + transform: translateY(-50%); +} + +/** + * Input + * TextArea +*/ +.c__input__wrapper, +.c__textarea__wrapper { + transition: all var(--c--theme--transitions--duration) + var(--c--theme--transitions--ease-out); +} + +.c__input__wrapper:has(input[readonly]), +.c__input__wrapper:has(input[readonly]) * { + cursor: default; +} + +.c__textarea__wrapper:has(input.border-none), +.c__textarea__wrapper:has(input.border-none) *, +.c__input__wrapper:has(input.border-none), +.c__input__wrapper:has(input.border-none) * { + border: none; +} + +.c__input__wrapper:hover, +.c__textarea__wrapper:hover { + box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px; +} + +.c__textarea__wrapper--disabled:hover, +.c__input__wrapper--disabled:hover, +.c__input__wrapper:hover:has(input[readonly]) { + box-shadow: var(--c--theme--colors--primary-500) 0 0 0 0; +} + +.c__input__wrapper--disabled { + color: var(--c--components--forms-input--value-color--disabled); +} + +.c__input__wrapper .labelled-box__label.placeholder { + cursor: inherit; +} + +.c__input__wrapper .c__input, +.c__textarea__wrapper .c__textarea { + width: 100%; +} + +.c__input__wrapper--disabled .c__input { + color: var(--c--components--forms-input--value-color--disabled); +} + +.c__input__wrapper--error .c__input { + color: var(--c--components--forms-input--color--error); +} + +.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover { + border-color: var(--c--components--forms-input--border--color-error-hover); + color: var(--c--components--forms-input--color--error-hover); +} + +.c__input__wrapper--error:hover { + box-shadow: var(--c--components--forms-input--color--box-shadow-error-hover) 0 + 0 0 2px; +} + +.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label { + color: var(--c--components--forms-input--border--color-error-hover); +} + +input:-webkit-autofill, +input:-webkit-autofill:focus { + transition: + background-color 0s 600000s, + color 0s 600000s; +} + +.c__textarea__wrapper .c__textarea { + color: var(--c--components--forms-textarea--color); +} + +.c__textarea__wrapper:hover { + border-color: var(--c--components--forms-textarea--border-color-hover); +} + +.c__textarea__wrapper--disabled:hover { + border-color: var( + --c--components--forms-textarea--disabled--border-color-hover + ); +} + +/** + * Select +*/ +.c_select__no_border .c__select .c__select__wrapper, +.c_select__no_border .c__select .c__select__wrapper:hover, +.c_select__no_border + .c__select:not(.c__select--disabled) + .c__select__wrapper:hover { + border: none; + box-shadow: none; +} + +.c__select__wrapper { + transition: all var(--c--theme--transitions--duration) + var(--c--theme--transitions--ease-out); + min-height: var(--c--components--forms-select--height); + height: auto; +} + +.c__select:not(.c__select--disabled) .c__select__wrapper:hover { + box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px; +} + +.c__select__wrapper:hover { + border-radius: var(--c--components--forms-select--border-radius-hover); + border-color: var(--c--components--forms-select--border-color-hover); +} + +.c__select--disabled .c__select__wrapper:hover { + border-color: var(--c--components--forms-select--border-color-disabled-hover); +} + +.c__select__menu__item { + transition: all var(--c--theme--transitions--duration) + var(--c--theme--transitions--ease-out); +} + +.c__select--disabled .c__select__wrapper label, +.c__select--disabled .c__select__wrapper input, +.c_select__no_bg .c__select__wrapper { + background: none; +} + +.c__select__wrapper:focus-within .labelled-box--disabled label { + color: var(--c--components--forms-labelledbox--label-color--small-disabled); +} + +.c__select__wrapper .labelled-box { + display: flex; + gap: 0.6rem; + flex-direction: column; + align-items: flex-start; +} + +.c__select__wrapper .labelled-box .labelled-box__children { + padding: unset; + padding-right: 5rem; +} + +.c__select__wrapper .labelled-box .c__select__inner__actions { + right: 0; + top: 50%; + position: absolute; +} + +.c__select__wrapper label { + position: relative; + padding-right: 5rem; + max-width: none; +} + +.c__select__wrapper .c__select__inner__actions__open:focus { + outline: none; +} + +.c__select__wrapper .labelled-box__label.c__offscreen { + display: none; +} + +/** + * DataGrid +*/ +.c__datagrid__table__container { + overflow: auto; +} + +.c__datagrid__table__container > table th .c__datagrid__header { + color: var(--c--components--datagrid--header--color); + font-weight: var(--c--components--datagrid--header--weight); + font-size: var(--c--components--datagrid--header--size); + padding-block: 2rem; +} + +.c__datagrid__table__container > table tbody tr { + border: none; + border-top: 1px var(--c--theme--colors--greyscale-300) solid; + border-bottom: 1px var(--c--theme--colors--greyscale-300) solid; +} + +.c__datagrid__table__container > table tbody { + background-color: var(--c--components--datagrid--body--background-color); +} + +.c__datagrid__table__container > table tbody tr:hover { + background-color: var( + --c--components--datagrid--body--background-color-hover + ); +} + +.c__datagrid__table__container > table th:first-child, +.c__datagrid__table__container > table td:first-child { + padding-left: 2rem; +} + +.c__datagrid > .c__pagination { + padding-right: 1rem; + justify-content: flex-end; +} + +.c__pagination__list { + gap: 3px; + border-radius: 4px; + background: var(--c--components--datagrid--pagination--background-color); +} + +.c__pagination__list .c__button--tertiary-text.c__button--active { + background-color: var( + --c--components--datagrid--pagination--background-color-active + ); + color: var(--c--theme--colors--greyscale-800); +} + +.c__pagination__list .c__button--tertiary-text:disabled { + display: none; +} + +@media (width <= 380px) { + .c__datagrid > .c__pagination { + flex-direction: column; + align-items: center; + gap: 1rem; + } +} + +/** + * Date picker +*/ +.c__popover.c__popover--borderless { + z-index: 3; +} + +.c__date-picker__wrapper { + transition: all var(--c--theme--transitions--duration) + var(--c--theme--transitions--ease-out); +} + +.c__date-picker:not(.c__date-picker--disabled):hover .c__date-picker__wrapper { + box-shadow: var(--c--theme--colors--primary-500) 0 0 0 2px; +} + +.c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover + .c__date-picker__wrapper { + box-shadow: var(--c--theme--colors--danger-300) 0 0 0 2px; +} + +.c__date-picker__wrapper button[aria-label='Clear date'], +.c__date-picker.c__date-picker--invalid .c__date-picker__wrapper * { + color: var(--c--theme--colors--danger-300); +} + +/** + * Others +*/ +.c__checkbox:focus-within { + border-color: transparent; + background-color: transparent; +} + +.c__checkbox { + transition: all 0.8s ease-in-out; +} + +/** + * Button +*/ +.c__button { + text-decoration: none; +} + +.c__button:hover.c__button-no-bg, +.c__button.c__button-no-bg, +.c__button:disabled.c__button-no-bg { + background-color: transparent; +} + +.c__button--medium { + padding: 0.9rem var(--c--theme--spacings--s); +} + +.c__button--small { + padding: 0.6rem 0.75rem; +} + +.c__button--with-icon--right { + padding: 0.7rem var(--c--theme--spacings--t) 0.7rem + var(--c--theme--spacings--s); +} + +.c__button--primary { + background-color: var(--c--components--button--primary--background--color); + color: var(--c--components--button--primary--color); +} + +.c__button--primary:hover { + background-color: var( + --c--components--button--primary--background--color-hover + ); + color: var(--c--components--button--primary--color-hover); +} + +.c__button--primary:active, +.c__button--primary.c__button--active { + background-color: var( + --c--components--button--primary--background--color-active + ); + color: var(--c--components--button--primary--color-active); + border-color: var(--c--components--button--primary--border--color-active); +} + +.c__button--primary-text:active, +.c__button--primary-text.c__button--active { + border: none; + background-color: var( + --c--components--button--primary-text--background--color-active + ); +} + +.c__button--primary-text:hover, +.c__button--primary-text:focus-visible { + background-color: var( + --c--components--button--primary-text--background--color-hover + ); + color: var(--c--components--button--primary-text--color-hover); +} + +.c__button:disabled { + background-color: var(--c--components--button--disabled--background--color); + color: var(--c--components--button--disabled--color); +} + +.c__button--success { + background-color: var(--c--components--button--success--background--color); + color: var(--c--components--button--success--color); +} + +.c__button--success:hover, +.c__button--success:focus-visible { + background-color: var( + --c--components--button--success--background--color-hover + ); + color: var(--c--components--button--success--color-hover); +} + +.c__button--success:disabled { + background-color: var( + --c--components--button--success--background--color-disabled + ); + color: var(--c--components--button--success--color-disabled); +} + +.c__button--secondary { + background-color: var(--c--components--button--secondary--background--color); + color: var(--c--components--button--secondary--color); + border: 1px solid var(--c--components--button--secondary--border--color); +} + +.c__button--secondary:hover, +.c__button--secondary:focus-visible { + background-color: var( + --c--components--button--secondary--background--color-hover + ); + color: var(--c--components--button--secondary--color-hover); + border: 1px solid var(--c--components--button--secondary--border--color-hover); +} + +.c__button--tertiary { + color: var(--c--components--button--tertiary--color); + border: none; +} + +.c__button--tertiary:hover, +.c__button--tertiary:focus-visible { + background-color: var( + --c--components--button--tertiary--background--color-hover + ); + color: var(--c--components--button--tertiary--color); +} + +.c__button--tertiary:disabled { + background-color: var( + --c--components--button--tertiary--background--color-disabled + ); + color: var(--c--components--button--tertiary--color-disabled); +} + +.c__button--tertiary-text { + border: none; +} + +.c__button--tertiary-text:hover, +.c__button--tertiary-text:focus-visible { + background-color: var( + --c--components--button--tertiary-text--background--color-hover + ); + color: var(--c--components--button--tertiary-text--color-hover); +} + +.c__button--danger { + background-color: var(--c--components--button--danger--background--color); +} + +.c__button--danger:hover, +.c__button--danger:focus-visible { + background-color: var( + --c--components--button--danger--background--color-hover + ); + color: var(--c--components--button--danger--color-hover); +} + +.c__button--danger:disabled { + background-color: var( + --c--components--button--danger--background--color-disabled + ); +} + +/** + * Modal +*/ +.c__modal__backdrop { + z-index: 1000; +} + +.c__modal__close .c__button--tertiary-text:hover, +.c__modal__close .c__button--tertiary-text:focus-visible { + box-shadow: none; +} diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css new file mode 100644 index 00000000..0653c764 --- /dev/null +++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css @@ -0,0 +1,1618 @@ +:root { + --c--theme--colors--secondary-text: var(--c--theme--colors--greyscale-700); + --c--theme--colors--secondary-100: #f2f7fc; + --c--theme--colors--secondary-200: #ebf3fa; + --c--theme--colors--secondary-300: #e2eef8; + --c--theme--colors--secondary-400: #ddeaf7; + --c--theme--colors--secondary-500: #d4e5f5; + --c--theme--colors--secondary-600: #c1d0df; + --c--theme--colors--secondary-700: #97a3ae; + --c--theme--colors--secondary-800: #757e87; + --c--theme--colors--secondary-900: #596067; + --c--theme--colors--info-text: var(--c--theme--colors--greyscale-000); + --c--theme--colors--info-100: #ebf2fc; + --c--theme--colors--info-200: #8cb5ea; + --c--theme--colors--info-300: #5894e1; + --c--theme--colors--info-400: #377fdb; + --c--theme--colors--info-500: #055fd2; + --c--theme--colors--info-600: #0556bf; + --c--theme--colors--info-700: #044395; + --c--theme--colors--info-800: #033474; + --c--theme--colors--info-900: #022858; + --c--theme--colors--greyscale-100: #fafafb; + --c--theme--colors--greyscale-200: #f3f4f4; + --c--theme--colors--greyscale-300: #e7e8ea; + --c--theme--colors--greyscale-400: #c2c6ca; + --c--theme--colors--greyscale-500: #9ea3aa; + --c--theme--colors--greyscale-600: #79818a; + --c--theme--colors--greyscale-700: #555f6b; + --c--theme--colors--greyscale-800: #303c4b; + --c--theme--colors--greyscale-900: #0c1a2b; + --c--theme--colors--greyscale-000: #fff; + --c--theme--colors--primary-100: #edf5fa; + --c--theme--colors--primary-200: #8cb5ea; + --c--theme--colors--primary-300: #5894e1; + --c--theme--colors--primary-400: #377fdb; + --c--theme--colors--primary-500: #055fd2; + --c--theme--colors--primary-600: #0556bf; + --c--theme--colors--primary-700: #044395; + --c--theme--colors--primary-800: #033474; + --c--theme--colors--primary-900: #022858; + --c--theme--colors--success-100: #effcd3; + --c--theme--colors--success-200: #dbfaa9; + --c--theme--colors--success-300: #bef27c; + --c--theme--colors--success-400: #a0e659; + --c--theme--colors--success-500: #76d628; + --c--theme--colors--success-600: #5ab81d; + --c--theme--colors--success-700: #419a14; + --c--theme--colors--success-800: #2c7c0c; + --c--theme--colors--success-900: #1d6607; + --c--theme--colors--warning-100: #fff8cd; + --c--theme--colors--warning-200: #ffef9b; + --c--theme--colors--warning-300: #ffe469; + --c--theme--colors--warning-400: #ffda43; + --c--theme--colors--warning-500: #ffc805; + --c--theme--colors--warning-600: #dba603; + --c--theme--colors--warning-700: #b78702; + --c--theme--colors--warning-800: #936901; + --c--theme--colors--warning-900: #7a5400; + --c--theme--colors--danger-100: #f4b0b0; + --c--theme--colors--danger-200: #ee8a8a; + --c--theme--colors--danger-300: #e65454; + --c--theme--colors--danger-400: #e13333; + --c--theme--colors--danger-500: #da0000; + --c--theme--colors--danger-600: #c60000; + --c--theme--colors--danger-700: #9b0000; + --c--theme--colors--danger-800: #780000; + --c--theme--colors--danger-900: #5c0000; + --c--theme--colors--primary-text: var(--c--theme--colors--greyscale-000); + --c--theme--colors--success-text: var(--c--theme--colors--greyscale-000); + --c--theme--colors--warning-text: var(--c--theme--colors--greyscale-000); + --c--theme--colors--danger-text: var(--c--theme--colors--greyscale-000); + --c--theme--colors--card-border: #ddd; + --c--theme--colors--primary-bg: #fafafa; + --c--theme--colors--primary-150: #e5eefa; + --c--theme--colors--info-150: #e5eefa; + --c--theme--font--sizes--h1: 2.2rem; + --c--theme--font--sizes--h2: 1.7rem; + --c--theme--font--sizes--h3: 1.37rem; + --c--theme--font--sizes--h4: 1.15rem; + --c--theme--font--sizes--h5: 1rem; + --c--theme--font--sizes--h6: 0.87rem; + --c--theme--font--sizes--l: 1rem; + --c--theme--font--sizes--m: 0.8125rem; + --c--theme--font--sizes--s: 0.75rem; + --c--theme--font--sizes--ml: 0.938rem; + --c--theme--font--sizes--xl: 1.5rem; + --c--theme--font--sizes--t: 0.6875rem; + --c--theme--font--weights--thin: 100; + --c--theme--font--weights--light: 300; + --c--theme--font--weights--regular: 400; + --c--theme--font--weights--medium: 500; + --c--theme--font--weights--bold: 600; + --c--theme--font--weights--extrabold: 800; + --c--theme--font--weights--black: 900; + --c--theme--font--families--base: 'Roboto Flex Variable', sans-serif; + --c--theme--font--families--accent: 'Roboto Flex Variable', sans-serif; + --c--theme--font--letterspacings--h1: normal; + --c--theme--font--letterspacings--h2: normal; + --c--theme--font--letterspacings--h3: normal; + --c--theme--font--letterspacings--h4: normal; + --c--theme--font--letterspacings--h5: 1px; + --c--theme--font--letterspacings--h6: normal; + --c--theme--font--letterspacings--l: normal; + --c--theme--font--letterspacings--m: normal; + --c--theme--font--letterspacings--s: normal; + --c--theme--spacings--0: 0; + --c--theme--spacings--xl: 4rem; + --c--theme--spacings--l: 3rem; + --c--theme--spacings--b: 1.625rem; + --c--theme--spacings--s: 1rem; + --c--theme--spacings--t: 0.5rem; + --c--theme--spacings--st: 0.25rem; + --c--theme--spacings--none: 0; + --c--theme--spacings--auto: auto; + --c--theme--spacings--bx: 2.2rem; + --c--theme--spacings--full: 100%; + --c--theme--transitions--ease-in: cubic-bezier(0.32, 0, 0.67, 0); + --c--theme--transitions--ease-out: cubic-bezier(0.33, 1, 0.68, 1); + --c--theme--transitions--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --c--theme--transitions--duration: 250ms; + --c--theme--breakpoints--xs: 480px; + --c--theme--breakpoints--sm: 576px; + --c--theme--breakpoints--md: 768px; + --c--theme--breakpoints--lg: 992px; + --c--theme--breakpoints--xl: 1200px; + --c--theme--breakpoints--xxl: 1400px; + --c--theme--breakpoints--xxs: 320px; + --c--components--datagrid--header--weight: var( + --c--theme--font--weights--extrabold + ); + --c--components--datagrid--header--size: var(--c--theme--font--sizes--ml); + --c--components--datagrid--cell--color: var(--c--theme--colors--primary-500); + --c--components--datagrid--cell--size: var(--c--theme--font--sizes--ml); + --c--components--forms-checkbox--background-color--hover: #055fd214; + --c--components--forms-checkbox--color: var(--c--theme--colors--primary-500); + --c--components--forms-checkbox--font-size: var(--c--theme--font--sizes--ml); + --c--components--forms-datepicker--border-color: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-datepicker--value-color: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-datepicker--border-radius--hover: var( + --c--components--forms-datepicker--border-radius + ); + --c--components--forms-datepicker--border-radius--focus: var( + --c--components--forms-datepicker--border-radius + ); + --c--components--forms-field--color: var(--c--theme--colors--primary-500); + --c--components--forms-field--value-color: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-field--width: auto; + --c--components--forms-input--value-color: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-input--border-color: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-input--color--error: var( + --c--theme--colors--danger-500 + ); + --c--components--forms-input--color--error-hover: var( + --c--theme--colors--danger-500 + ); + --c--components--forms-input--color--box-shadow-error-hover: var( + --c--theme--colors--danger-500 + ); + --c--components--forms-labelledbox--label-color--small: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-labelledbox--label-color--small-disabled: var( + --c--theme--colors--greyscale-400 + ); + --c--components--forms-labelledbox--label-color--big--disabled: var( + --c--theme--colors--greyscale-400 + ); + --c--components--forms-select--border-color: var( + --c--theme--colors--primary-500 + ); + --c--components--forms-select--border-color-disabled-hover: var( + --c--theme--colors--greyscale-200 + ); + --c--components--forms-select--border-radius--hover: var( + --c--components--forms-select--border-radius + ); + --c--components--forms-select--border-radius--focus: var( + --c--components--forms-select--border-radius + ); + --c--components--forms-select--font-size: var(--c--theme--font--sizes--ml); + --c--components--forms-select--menu-background-color: #fff; + --c--components--forms-select--item-background-color--hover: var( + --c--theme--colors--primary-300 + ); + --c--components--forms-switch--accent-color: var( + --c--theme--colors--primary-400 + ); + --c--components--forms-textarea--border-color: var( + --c--components--forms-textarea--border-color + ); + --c--components--forms-textarea--border-color-hover: var( + --c--components--forms-textarea--border-color + ); + --c--components--forms-textarea--border-radius--hover: var( + --c--components--forms-textarea--border-radius + ); + --c--components--forms-textarea--border-radius--focus: var( + --c--components--forms-textarea--border-radius + ); + --c--components--forms-textarea--color: var(--c--theme--colors--primary-500); + --c--components--forms-textarea--disabled--border-color-hover: var( + --c--theme--colors--greyscale-200 + ); + --c--components--modal--background-color: #fff; + --c--components--button--border-radius--active: var( + --c--components--button--border-radius + ); + --c--components--button--medium-height: auto; + --c--components--button--small-height: auto; + --c--components--button--success--color: white; + --c--components--button--success--color-disabled: white; + --c--components--button--success--color-hover: white; + --c--components--button--success--background--color: var( + --c--theme--colors--success-600 + ); + --c--components--button--success--background--color-disabled: var( + --c--theme--colors--greyscale-300 + ); + --c--components--button--success--background--color-hover: var( + --c--theme--colors--success-800 + ); + --c--components--button--danger--color-hover: white; + --c--components--button--danger--background--color: var( + --c--theme--colors--danger-400 + ); + --c--components--button--danger--background--color-hover: var( + --c--theme--colors--danger-500 + ); + --c--components--button--danger--background--color-disabled: var( + --c--theme--colors--danger-100 + ); + --c--components--button--primary--color: var( + --c--theme--colors--primary-text + ); + --c--components--button--primary--color-active: var( + --c--theme--colors--primary-text + ); + --c--components--button--primary--background--color: var( + --c--theme--colors--primary-400 + ); + --c--components--button--primary--background--color-active: var( + --c--theme--colors--primary-500 + ); + --c--components--button--primary--border--color-active: transparent; + --c--components--button--secondary--color: var( + --c--theme--colors--primary-500 + ); + --c--components--button--secondary--color-hover: var( + --c--theme--colors--primary-text + ); + --c--components--button--secondary--background--color: white; + --c--components--button--secondary--background--color-hover: var( + --c--theme--colors--primary-700 + ); + --c--components--button--secondary--border--color: var( + --c--theme--colors--primary-200 + ); + --c--components--button--tertiary--color: var( + --c--theme--colors--primary-text + ); + --c--components--button--tertiary--color-disabled: var( + --c--theme--colors--greyscale-600 + ); + --c--components--button--tertiary--background--color-hover: var( + --c--theme--colors--primary-100 + ); + --c--components--button--tertiary--background--color-disabled: var( + --c--theme--colors--greyscale-200 + ); + --c--components--button--disabled--color: white; + --c--components--button--disabled--background--color: #b3cef0; +} + +.cunningham-theme--dark { + --c--theme--colors--greyscale-100: #182536; + --c--theme--colors--greyscale-200: #303c4b; + --c--theme--colors--greyscale-300: #555f6b; + --c--theme--colors--greyscale-400: #79818a; + --c--theme--colors--greyscale-500: #9ea3aa; + --c--theme--colors--greyscale-600: #c2c6ca; + --c--theme--colors--greyscale-700: #e7e8ea; + --c--theme--colors--greyscale-800: #f3f4f4; + --c--theme--colors--greyscale-900: #fafafb; + --c--theme--colors--greyscale-000: #0c1a2b; + --c--theme--colors--primary-100: #3b4c62; + --c--theme--colors--primary-200: #4d6481; + --c--theme--colors--primary-300: #6381a6; + --c--theme--colors--primary-400: #7fa5d5; + --c--theme--colors--primary-500: #8cb5ea; + --c--theme--colors--primary-600: #a3c4ee; + --c--theme--colors--primary-700: #c3d8f4; + --c--theme--colors--primary-800: #dde9f8; + --c--theme--colors--primary-900: #f4f8fd; + --c--theme--colors--success-100: #eef8d7; + --c--theme--colors--success-200: #d9f1b2; + --c--theme--colors--success-300: #bde985; + --c--theme--colors--success-400: #a0e25d; + --c--theme--colors--success-500: #76d628; + --c--theme--colors--success-600: #5bb520; + --c--theme--colors--success-700: #43941a; + --c--theme--colors--success-800: #307414; + --c--theme--colors--success-900: #225d10; + --c--theme--colors--warning-100: #f7f3d5; + --c--theme--colors--warning-200: #f0e5aa; + --c--theme--colors--warning-300: #e8d680; + --c--theme--colors--warning-400: #e3c95f; + --c--theme--colors--warning-500: #d9b32b; + --c--theme--colors--warning-600: #bd9721; + --c--theme--colors--warning-700: #9d7b1c; + --c--theme--colors--warning-800: #7e6016; + --c--theme--colors--warning-900: #684d12; + --c--theme--colors--danger-100: #f8d0d0; + --c--theme--colors--danger-200: #f09898; + --c--theme--colors--danger-300: #f09898; + --c--theme--colors--danger-400: #ed8585; + --c--theme--colors--danger-500: #e96666; + --c--theme--colors--danger-600: #d66; + --c--theme--colors--danger-700: #c36666; + --c--theme--colors--danger-800: #ae6666; + --c--theme--colors--danger-900: #9d6666; +} + +.cunningham-theme--dsfr { + --c--theme--colors--card-border: #ddd; + --c--theme--colors--primary-text: #000091; + --c--theme--colors--primary-100: #f5f5fe; + --c--theme--colors--primary-150: #f4f4fd; + --c--theme--colors--primary-200: #ececfe; + --c--theme--colors--primary-300: #e3e3fd; + --c--theme--colors--primary-400: #cacafb; + --c--theme--colors--primary-500: #6a6af4; + --c--theme--colors--primary-600: #000091; + --c--theme--colors--primary-700: #272747; + --c--theme--colors--primary-800: #21213f; + --c--theme--colors--primary-900: #1c1a36; + --c--theme--colors--secondary-text: #fff; + --c--theme--colors--secondary-100: #fee9ea; + --c--theme--colors--secondary-200: #fedfdf; + --c--theme--colors--secondary-300: #fdbfbf; + --c--theme--colors--secondary-400: #e1020f; + --c--theme--colors--secondary-500: #c91a1f; + --c--theme--colors--secondary-600: #5e2b2b; + --c--theme--colors--secondary-700: #3b2424; + --c--theme--colors--secondary-800: #341f1f; + --c--theme--colors--secondary-900: #2b1919; + --c--theme--colors--greyscale-text: #303c4b; + --c--theme--colors--greyscale-000: #f6f6f6; + --c--theme--colors--greyscale-100: #eee; + --c--theme--colors--greyscale-200: #e5e5e5; + --c--theme--colors--greyscale-300: #e1e1e1; + --c--theme--colors--greyscale-400: #ddd; + --c--theme--colors--greyscale-500: #cecece; + --c--theme--colors--greyscale-600: #7b7b7b; + --c--theme--colors--greyscale-700: #666; + --c--theme--colors--greyscale-800: #2a2a2a; + --c--theme--colors--greyscale-900: #1e1e1e; + --c--theme--colors--success-text: #1f8d49; + --c--theme--colors--success-100: #dffee6; + --c--theme--colors--success-200: #b8fec9; + --c--theme--colors--success-300: #88fdaa; + --c--theme--colors--success-400: #3bea7e; + --c--theme--colors--success-500: #1f8d49; + --c--theme--colors--success-600: #18753c; + --c--theme--colors--success-700: #204129; + --c--theme--colors--success-800: #1e2e22; + --c--theme--colors--success-900: #19281d; + --c--theme--colors--info-text: #0078f3; + --c--theme--colors--info-100: #f4f6ff; + --c--theme--colors--info-200: #e8edff; + --c--theme--colors--info-300: #dde5ff; + --c--theme--colors--info-400: #bdcdff; + --c--theme--colors--info-500: #0078f3; + --c--theme--colors--info-600: #0063cb; + --c--theme--colors--info-700: #f4f6ff; + --c--theme--colors--info-800: #222a3f; + --c--theme--colors--info-900: #1d2437; + --c--theme--colors--warning-text: #d64d00; + --c--theme--colors--warning-100: #fff4f3; + --c--theme--colors--warning-200: #ffe9e6; + --c--theme--colors--warning-300: #ffded9; + --c--theme--colors--warning-400: #ffbeb4; + --c--theme--colors--warning-500: #d64d00; + --c--theme--colors--warning-600: #b34000; + --c--theme--colors--warning-700: #5e2c21; + --c--theme--colors--warning-800: #3e241e; + --c--theme--colors--warning-900: #361e19; + --c--theme--colors--danger-text: #e1000f; + --c--theme--colors--danger-100: #fef4f4; + --c--theme--colors--danger-200: #fee9e9; + --c--theme--colors--danger-300: #fddede; + --c--theme--colors--danger-400: #fcbfbf; + --c--theme--colors--danger-500: #e1000f; + --c--theme--colors--danger-600: #c9191e; + --c--theme--colors--danger-700: #642727; + --c--theme--colors--danger-800: #412121; + --c--theme--colors--danger-900: #3a1c1c; + --c--theme--font--families--accent: marianne; + --c--theme--font--families--base: marianne; + --c--components--alert--border-radius: 0; + --c--components--button--medium-height: 48px; + --c--components--button--border-radius: 4px; + --c--components--button--primary--background--color: var( + --c--theme--colors--primary-text + ); + --c--components--button--primary--background--color-hover: #1212ff; + --c--components--button--primary--background--color-active: #2323ff; + --c--components--button--primary--color: #fff; + --c--components--button--primary--color-hover: #fff; + --c--components--button--primary--color-active: #fff; + --c--components--button--primary-text--background--color-hover: var( + --c--theme--colors--primary-100 + ); + --c--components--button--primary-text--background--color-active: var( + --c--theme--colors--primary-100 + ); + --c--components--button--primary-text--color-hover: var( + --c--theme--colors--primary-text + ); + --c--components--button--secondary--background--color-hover: #f6f6f6; + --c--components--button--secondary--background--color-active: #ededed; + --c--components--button--secondary--border--color: var( + --c--theme--colors--primary-600 + ); + --c--components--button--secondary--border--color-hover: var( + --c--theme--colors--primary-600 + ); + --c--components--button--secondary--color: var( + --c--theme--colors--primary-text + ); + --c--components--button--tertiary-text--background--color-hover: var( + --c--theme--colors--primary-100 + ); + --c--components--button--tertiary-text--color-hover: var( + --c--theme--colors--primary-text + ); + --c--components--datagrid--header--color: var( + --c--theme--colors--primary-text + ); + --c--components--datagrid--header--size: var(--c--theme--font--sizes--s); + --c--components--datagrid--body--background-color: transparent; + --c--components--datagrid--body--background-color-hover: #f4f4fd; + --c--components--datagrid--pagination--background-color: transparent; + --c--components--datagrid--pagination--background-color-active: var( + --c--theme--colors--primary-300 + ); + --c--components--forms-checkbox--border-radius: 0; + --c--components--forms-checkbox--color: var(--c--theme--colors--primary-text); + --c--components--forms-datepicker--border-radius: 0; + --c--components--forms-fileuploader--border-radius: 0; + --c--components--forms-field--color: var(--c--theme--colors--primary-text); + --c--components--forms-input--border-radius: 4px; + --c--components--forms-input--background-color: #fff; + --c--components--forms-input--border-color: var( + --c--theme--colors--primary-text + ); + --c--components--forms-input--box-shadow-color: var( + --c--theme--colors--primary-text + ); + --c--components--forms-input--value-color: var( + --c--theme--colors--primary-text + ); + --c--components--forms-labelledbox--label-color--big: var( + --c--theme--colors--primary-text + ); + --c--components--forms-select--border-radius: 4px; + --c--components--forms-select--border-radius-hover: 4px; + --c--components--forms-select--background-color: #fff; + --c--components--forms-select--border-color: var( + --c--theme--colors--primary-text + ); + --c--components--forms-select--border-color-hover: var( + --c--theme--colors--primary-text + ); + --c--components--forms-select--box-shadow-color: var( + --c--theme--colors--primary-text + ); + --c--components--forms-switch--handle-border-radius: 2px; + --c--components--forms-switch--rail-border-radius: 4px; + --c--components--forms-textarea--border-radius: 0; +} + +.clr-secondary-text { + color: var(--c--theme--colors--secondary-text); +} + +.clr-secondary-100 { + color: var(--c--theme--colors--secondary-100); +} + +.clr-secondary-200 { + color: var(--c--theme--colors--secondary-200); +} + +.clr-secondary-300 { + color: var(--c--theme--colors--secondary-300); +} + +.clr-secondary-400 { + color: var(--c--theme--colors--secondary-400); +} + +.clr-secondary-500 { + color: var(--c--theme--colors--secondary-500); +} + +.clr-secondary-600 { + color: var(--c--theme--colors--secondary-600); +} + +.clr-secondary-700 { + color: var(--c--theme--colors--secondary-700); +} + +.clr-secondary-800 { + color: var(--c--theme--colors--secondary-800); +} + +.clr-secondary-900 { + color: var(--c--theme--colors--secondary-900); +} + +.clr-info-text { + color: var(--c--theme--colors--info-text); +} + +.clr-info-100 { + color: var(--c--theme--colors--info-100); +} + +.clr-info-200 { + color: var(--c--theme--colors--info-200); +} + +.clr-info-300 { + color: var(--c--theme--colors--info-300); +} + +.clr-info-400 { + color: var(--c--theme--colors--info-400); +} + +.clr-info-500 { + color: var(--c--theme--colors--info-500); +} + +.clr-info-600 { + color: var(--c--theme--colors--info-600); +} + +.clr-info-700 { + color: var(--c--theme--colors--info-700); +} + +.clr-info-800 { + color: var(--c--theme--colors--info-800); +} + +.clr-info-900 { + color: var(--c--theme--colors--info-900); +} + +.clr-greyscale-100 { + color: var(--c--theme--colors--greyscale-100); +} + +.clr-greyscale-200 { + color: var(--c--theme--colors--greyscale-200); +} + +.clr-greyscale-300 { + color: var(--c--theme--colors--greyscale-300); +} + +.clr-greyscale-400 { + color: var(--c--theme--colors--greyscale-400); +} + +.clr-greyscale-500 { + color: var(--c--theme--colors--greyscale-500); +} + +.clr-greyscale-600 { + color: var(--c--theme--colors--greyscale-600); +} + +.clr-greyscale-700 { + color: var(--c--theme--colors--greyscale-700); +} + +.clr-greyscale-800 { + color: var(--c--theme--colors--greyscale-800); +} + +.clr-greyscale-900 { + color: var(--c--theme--colors--greyscale-900); +} + +.clr-greyscale-000 { + color: var(--c--theme--colors--greyscale-000); +} + +.clr-primary-100 { + color: var(--c--theme--colors--primary-100); +} + +.clr-primary-200 { + color: var(--c--theme--colors--primary-200); +} + +.clr-primary-300 { + color: var(--c--theme--colors--primary-300); +} + +.clr-primary-400 { + color: var(--c--theme--colors--primary-400); +} + +.clr-primary-500 { + color: var(--c--theme--colors--primary-500); +} + +.clr-primary-600 { + color: var(--c--theme--colors--primary-600); +} + +.clr-primary-700 { + color: var(--c--theme--colors--primary-700); +} + +.clr-primary-800 { + color: var(--c--theme--colors--primary-800); +} + +.clr-primary-900 { + color: var(--c--theme--colors--primary-900); +} + +.clr-success-100 { + color: var(--c--theme--colors--success-100); +} + +.clr-success-200 { + color: var(--c--theme--colors--success-200); +} + +.clr-success-300 { + color: var(--c--theme--colors--success-300); +} + +.clr-success-400 { + color: var(--c--theme--colors--success-400); +} + +.clr-success-500 { + color: var(--c--theme--colors--success-500); +} + +.clr-success-600 { + color: var(--c--theme--colors--success-600); +} + +.clr-success-700 { + color: var(--c--theme--colors--success-700); +} + +.clr-success-800 { + color: var(--c--theme--colors--success-800); +} + +.clr-success-900 { + color: var(--c--theme--colors--success-900); +} + +.clr-warning-100 { + color: var(--c--theme--colors--warning-100); +} + +.clr-warning-200 { + color: var(--c--theme--colors--warning-200); +} + +.clr-warning-300 { + color: var(--c--theme--colors--warning-300); +} + +.clr-warning-400 { + color: var(--c--theme--colors--warning-400); +} + +.clr-warning-500 { + color: var(--c--theme--colors--warning-500); +} + +.clr-warning-600 { + color: var(--c--theme--colors--warning-600); +} + +.clr-warning-700 { + color: var(--c--theme--colors--warning-700); +} + +.clr-warning-800 { + color: var(--c--theme--colors--warning-800); +} + +.clr-warning-900 { + color: var(--c--theme--colors--warning-900); +} + +.clr-danger-100 { + color: var(--c--theme--colors--danger-100); +} + +.clr-danger-200 { + color: var(--c--theme--colors--danger-200); +} + +.clr-danger-300 { + color: var(--c--theme--colors--danger-300); +} + +.clr-danger-400 { + color: var(--c--theme--colors--danger-400); +} + +.clr-danger-500 { + color: var(--c--theme--colors--danger-500); +} + +.clr-danger-600 { + color: var(--c--theme--colors--danger-600); +} + +.clr-danger-700 { + color: var(--c--theme--colors--danger-700); +} + +.clr-danger-800 { + color: var(--c--theme--colors--danger-800); +} + +.clr-danger-900 { + color: var(--c--theme--colors--danger-900); +} + +.clr-primary-text { + color: var(--c--theme--colors--primary-text); +} + +.clr-success-text { + color: var(--c--theme--colors--success-text); +} + +.clr-warning-text { + color: var(--c--theme--colors--warning-text); +} + +.clr-danger-text { + color: var(--c--theme--colors--danger-text); +} + +.clr-card-border { + color: var(--c--theme--colors--card-border); +} + +.clr-primary-bg { + color: var(--c--theme--colors--primary-bg); +} + +.clr-primary-150 { + color: var(--c--theme--colors--primary-150); +} + +.clr-info-150 { + color: var(--c--theme--colors--info-150); +} + +.bg-secondary-text { + background-color: var(--c--theme--colors--secondary-text); +} + +.bg-secondary-100 { + background-color: var(--c--theme--colors--secondary-100); +} + +.bg-secondary-200 { + background-color: var(--c--theme--colors--secondary-200); +} + +.bg-secondary-300 { + background-color: var(--c--theme--colors--secondary-300); +} + +.bg-secondary-400 { + background-color: var(--c--theme--colors--secondary-400); +} + +.bg-secondary-500 { + background-color: var(--c--theme--colors--secondary-500); +} + +.bg-secondary-600 { + background-color: var(--c--theme--colors--secondary-600); +} + +.bg-secondary-700 { + background-color: var(--c--theme--colors--secondary-700); +} + +.bg-secondary-800 { + background-color: var(--c--theme--colors--secondary-800); +} + +.bg-secondary-900 { + background-color: var(--c--theme--colors--secondary-900); +} + +.bg-info-text { + background-color: var(--c--theme--colors--info-text); +} + +.bg-info-100 { + background-color: var(--c--theme--colors--info-100); +} + +.bg-info-200 { + background-color: var(--c--theme--colors--info-200); +} + +.bg-info-300 { + background-color: var(--c--theme--colors--info-300); +} + +.bg-info-400 { + background-color: var(--c--theme--colors--info-400); +} + +.bg-info-500 { + background-color: var(--c--theme--colors--info-500); +} + +.bg-info-600 { + background-color: var(--c--theme--colors--info-600); +} + +.bg-info-700 { + background-color: var(--c--theme--colors--info-700); +} + +.bg-info-800 { + background-color: var(--c--theme--colors--info-800); +} + +.bg-info-900 { + background-color: var(--c--theme--colors--info-900); +} + +.bg-greyscale-100 { + background-color: var(--c--theme--colors--greyscale-100); +} + +.bg-greyscale-200 { + background-color: var(--c--theme--colors--greyscale-200); +} + +.bg-greyscale-300 { + background-color: var(--c--theme--colors--greyscale-300); +} + +.bg-greyscale-400 { + background-color: var(--c--theme--colors--greyscale-400); +} + +.bg-greyscale-500 { + background-color: var(--c--theme--colors--greyscale-500); +} + +.bg-greyscale-600 { + background-color: var(--c--theme--colors--greyscale-600); +} + +.bg-greyscale-700 { + background-color: var(--c--theme--colors--greyscale-700); +} + +.bg-greyscale-800 { + background-color: var(--c--theme--colors--greyscale-800); +} + +.bg-greyscale-900 { + background-color: var(--c--theme--colors--greyscale-900); +} + +.bg-greyscale-000 { + background-color: var(--c--theme--colors--greyscale-000); +} + +.bg-primary-100 { + background-color: var(--c--theme--colors--primary-100); +} + +.bg-primary-200 { + background-color: var(--c--theme--colors--primary-200); +} + +.bg-primary-300 { + background-color: var(--c--theme--colors--primary-300); +} + +.bg-primary-400 { + background-color: var(--c--theme--colors--primary-400); +} + +.bg-primary-500 { + background-color: var(--c--theme--colors--primary-500); +} + +.bg-primary-600 { + background-color: var(--c--theme--colors--primary-600); +} + +.bg-primary-700 { + background-color: var(--c--theme--colors--primary-700); +} + +.bg-primary-800 { + background-color: var(--c--theme--colors--primary-800); +} + +.bg-primary-900 { + background-color: var(--c--theme--colors--primary-900); +} + +.bg-success-100 { + background-color: var(--c--theme--colors--success-100); +} + +.bg-success-200 { + background-color: var(--c--theme--colors--success-200); +} + +.bg-success-300 { + background-color: var(--c--theme--colors--success-300); +} + +.bg-success-400 { + background-color: var(--c--theme--colors--success-400); +} + +.bg-success-500 { + background-color: var(--c--theme--colors--success-500); +} + +.bg-success-600 { + background-color: var(--c--theme--colors--success-600); +} + +.bg-success-700 { + background-color: var(--c--theme--colors--success-700); +} + +.bg-success-800 { + background-color: var(--c--theme--colors--success-800); +} + +.bg-success-900 { + background-color: var(--c--theme--colors--success-900); +} + +.bg-warning-100 { + background-color: var(--c--theme--colors--warning-100); +} + +.bg-warning-200 { + background-color: var(--c--theme--colors--warning-200); +} + +.bg-warning-300 { + background-color: var(--c--theme--colors--warning-300); +} + +.bg-warning-400 { + background-color: var(--c--theme--colors--warning-400); +} + +.bg-warning-500 { + background-color: var(--c--theme--colors--warning-500); +} + +.bg-warning-600 { + background-color: var(--c--theme--colors--warning-600); +} + +.bg-warning-700 { + background-color: var(--c--theme--colors--warning-700); +} + +.bg-warning-800 { + background-color: var(--c--theme--colors--warning-800); +} + +.bg-warning-900 { + background-color: var(--c--theme--colors--warning-900); +} + +.bg-danger-100 { + background-color: var(--c--theme--colors--danger-100); +} + +.bg-danger-200 { + background-color: var(--c--theme--colors--danger-200); +} + +.bg-danger-300 { + background-color: var(--c--theme--colors--danger-300); +} + +.bg-danger-400 { + background-color: var(--c--theme--colors--danger-400); +} + +.bg-danger-500 { + background-color: var(--c--theme--colors--danger-500); +} + +.bg-danger-600 { + background-color: var(--c--theme--colors--danger-600); +} + +.bg-danger-700 { + background-color: var(--c--theme--colors--danger-700); +} + +.bg-danger-800 { + background-color: var(--c--theme--colors--danger-800); +} + +.bg-danger-900 { + background-color: var(--c--theme--colors--danger-900); +} + +.bg-primary-text { + background-color: var(--c--theme--colors--primary-text); +} + +.bg-success-text { + background-color: var(--c--theme--colors--success-text); +} + +.bg-warning-text { + background-color: var(--c--theme--colors--warning-text); +} + +.bg-danger-text { + background-color: var(--c--theme--colors--danger-text); +} + +.bg-card-border { + background-color: var(--c--theme--colors--card-border); +} + +.bg-primary-bg { + background-color: var(--c--theme--colors--primary-bg); +} + +.bg-primary-150 { + background-color: var(--c--theme--colors--primary-150); +} + +.bg-info-150 { + background-color: var(--c--theme--colors--info-150); +} + +.fw-thin { + font-weight: var(--c--theme--font--weights--thin); +} + +.fw-light { + font-weight: var(--c--theme--font--weights--light); +} + +.fw-regular { + font-weight: var(--c--theme--font--weights--regular); +} + +.fw-medium { + font-weight: var(--c--theme--font--weights--medium); +} + +.fw-bold { + font-weight: var(--c--theme--font--weights--bold); +} + +.fw-extrabold { + font-weight: var(--c--theme--font--weights--extrabold); +} + +.fw-black { + font-weight: var(--c--theme--font--weights--black); +} + +.fs-h1 { + font-size: var(--c--theme--font--sizes--h1); + letter-spacing: var(--c--theme--font--letterspacings--h1); +} + +.fs-h2 { + font-size: var(--c--theme--font--sizes--h2); + letter-spacing: var(--c--theme--font--letterspacings--h2); +} + +.fs-h3 { + font-size: var(--c--theme--font--sizes--h3); + letter-spacing: var(--c--theme--font--letterspacings--h3); +} + +.fs-h4 { + font-size: var(--c--theme--font--sizes--h4); + letter-spacing: var(--c--theme--font--letterspacings--h4); +} + +.fs-h5 { + font-size: var(--c--theme--font--sizes--h5); + letter-spacing: var(--c--theme--font--letterspacings--h5); +} + +.fs-h6 { + font-size: var(--c--theme--font--sizes--h6); + letter-spacing: var(--c--theme--font--letterspacings--h6); +} + +.fs-l { + font-size: var(--c--theme--font--sizes--l); + letter-spacing: var(--c--theme--font--letterspacings--l); +} + +.fs-m { + font-size: var(--c--theme--font--sizes--m); + letter-spacing: var(--c--theme--font--letterspacings--m); +} + +.fs-s { + font-size: var(--c--theme--font--sizes--s); + letter-spacing: var(--c--theme--font--letterspacings--s); +} + +.fs-ml { + font-size: var(--c--theme--font--sizes--ml); + letter-spacing: var(--c--theme--font--letterspacings--ml); +} + +.fs-xl { + font-size: var(--c--theme--font--sizes--xl); + letter-spacing: var(--c--theme--font--letterspacings--xl); +} + +.fs-t { + font-size: var(--c--theme--font--sizes--t); + letter-spacing: var(--c--theme--font--letterspacings--t); +} + +.f-base { + font-family: var(--c--theme--font--families--base); +} + +.f-accent { + font-family: var(--c--theme--font--families--accent); +} + +.m-0 { + margin: var(--c--theme--spacings--0); +} + +.mb-0 { + margin-bottom: var(--c--theme--spacings--0); +} + +.mt-0 { + margin-top: var(--c--theme--spacings--0); +} + +.ml-0 { + margin-left: var(--c--theme--spacings--0); +} + +.mr-0 { + margin-right: var(--c--theme--spacings--0); +} + +.m-xl { + margin: var(--c--theme--spacings--xl); +} + +.mb-xl { + margin-bottom: var(--c--theme--spacings--xl); +} + +.mt-xl { + margin-top: var(--c--theme--spacings--xl); +} + +.ml-xl { + margin-left: var(--c--theme--spacings--xl); +} + +.mr-xl { + margin-right: var(--c--theme--spacings--xl); +} + +.m-l { + margin: var(--c--theme--spacings--l); +} + +.mb-l { + margin-bottom: var(--c--theme--spacings--l); +} + +.mt-l { + margin-top: var(--c--theme--spacings--l); +} + +.ml-l { + margin-left: var(--c--theme--spacings--l); +} + +.mr-l { + margin-right: var(--c--theme--spacings--l); +} + +.m-b { + margin: var(--c--theme--spacings--b); +} + +.mb-b { + margin-bottom: var(--c--theme--spacings--b); +} + +.mt-b { + margin-top: var(--c--theme--spacings--b); +} + +.ml-b { + margin-left: var(--c--theme--spacings--b); +} + +.mr-b { + margin-right: var(--c--theme--spacings--b); +} + +.m-s { + margin: var(--c--theme--spacings--s); +} + +.mb-s { + margin-bottom: var(--c--theme--spacings--s); +} + +.mt-s { + margin-top: var(--c--theme--spacings--s); +} + +.ml-s { + margin-left: var(--c--theme--spacings--s); +} + +.mr-s { + margin-right: var(--c--theme--spacings--s); +} + +.m-t { + margin: var(--c--theme--spacings--t); +} + +.mb-t { + margin-bottom: var(--c--theme--spacings--t); +} + +.mt-t { + margin-top: var(--c--theme--spacings--t); +} + +.ml-t { + margin-left: var(--c--theme--spacings--t); +} + +.mr-t { + margin-right: var(--c--theme--spacings--t); +} + +.m-st { + margin: var(--c--theme--spacings--st); +} + +.mb-st { + margin-bottom: var(--c--theme--spacings--st); +} + +.mt-st { + margin-top: var(--c--theme--spacings--st); +} + +.ml-st { + margin-left: var(--c--theme--spacings--st); +} + +.mr-st { + margin-right: var(--c--theme--spacings--st); +} + +.m-none { + margin: var(--c--theme--spacings--none); +} + +.mb-none { + margin-bottom: var(--c--theme--spacings--none); +} + +.mt-none { + margin-top: var(--c--theme--spacings--none); +} + +.ml-none { + margin-left: var(--c--theme--spacings--none); +} + +.mr-none { + margin-right: var(--c--theme--spacings--none); +} + +.m-auto { + margin: var(--c--theme--spacings--auto); +} + +.mb-auto { + margin-bottom: var(--c--theme--spacings--auto); +} + +.mt-auto { + margin-top: var(--c--theme--spacings--auto); +} + +.ml-auto { + margin-left: var(--c--theme--spacings--auto); +} + +.mr-auto { + margin-right: var(--c--theme--spacings--auto); +} + +.m-bx { + margin: var(--c--theme--spacings--bx); +} + +.mb-bx { + margin-bottom: var(--c--theme--spacings--bx); +} + +.mt-bx { + margin-top: var(--c--theme--spacings--bx); +} + +.ml-bx { + margin-left: var(--c--theme--spacings--bx); +} + +.mr-bx { + margin-right: var(--c--theme--spacings--bx); +} + +.m-full { + margin: var(--c--theme--spacings--full); +} + +.mb-full { + margin-bottom: var(--c--theme--spacings--full); +} + +.mt-full { + margin-top: var(--c--theme--spacings--full); +} + +.ml-full { + margin-left: var(--c--theme--spacings--full); +} + +.mr-full { + margin-right: var(--c--theme--spacings--full); +} + +.p-0 { + padding: var(--c--theme--spacings--0); +} + +.pb-0 { + padding-bottom: var(--c--theme--spacings--0); +} + +.pt-0 { + padding-top: var(--c--theme--spacings--0); +} + +.pl-0 { + padding-left: var(--c--theme--spacings--0); +} + +.pr-0 { + padding-right: var(--c--theme--spacings--0); +} + +.p-xl { + padding: var(--c--theme--spacings--xl); +} + +.pb-xl { + padding-bottom: var(--c--theme--spacings--xl); +} + +.pt-xl { + padding-top: var(--c--theme--spacings--xl); +} + +.pl-xl { + padding-left: var(--c--theme--spacings--xl); +} + +.pr-xl { + padding-right: var(--c--theme--spacings--xl); +} + +.p-l { + padding: var(--c--theme--spacings--l); +} + +.pb-l { + padding-bottom: var(--c--theme--spacings--l); +} + +.pt-l { + padding-top: var(--c--theme--spacings--l); +} + +.pl-l { + padding-left: var(--c--theme--spacings--l); +} + +.pr-l { + padding-right: var(--c--theme--spacings--l); +} + +.p-b { + padding: var(--c--theme--spacings--b); +} + +.pb-b { + padding-bottom: var(--c--theme--spacings--b); +} + +.pt-b { + padding-top: var(--c--theme--spacings--b); +} + +.pl-b { + padding-left: var(--c--theme--spacings--b); +} + +.pr-b { + padding-right: var(--c--theme--spacings--b); +} + +.p-s { + padding: var(--c--theme--spacings--s); +} + +.pb-s { + padding-bottom: var(--c--theme--spacings--s); +} + +.pt-s { + padding-top: var(--c--theme--spacings--s); +} + +.pl-s { + padding-left: var(--c--theme--spacings--s); +} + +.pr-s { + padding-right: var(--c--theme--spacings--s); +} + +.p-t { + padding: var(--c--theme--spacings--t); +} + +.pb-t { + padding-bottom: var(--c--theme--spacings--t); +} + +.pt-t { + padding-top: var(--c--theme--spacings--t); +} + +.pl-t { + padding-left: var(--c--theme--spacings--t); +} + +.pr-t { + padding-right: var(--c--theme--spacings--t); +} + +.p-st { + padding: var(--c--theme--spacings--st); +} + +.pb-st { + padding-bottom: var(--c--theme--spacings--st); +} + +.pt-st { + padding-top: var(--c--theme--spacings--st); +} + +.pl-st { + padding-left: var(--c--theme--spacings--st); +} + +.pr-st { + padding-right: var(--c--theme--spacings--st); +} + +.p-none { + padding: var(--c--theme--spacings--none); +} + +.pb-none { + padding-bottom: var(--c--theme--spacings--none); +} + +.pt-none { + padding-top: var(--c--theme--spacings--none); +} + +.pl-none { + padding-left: var(--c--theme--spacings--none); +} + +.pr-none { + padding-right: var(--c--theme--spacings--none); +} + +.p-auto { + padding: var(--c--theme--spacings--auto); +} + +.pb-auto { + padding-bottom: var(--c--theme--spacings--auto); +} + +.pt-auto { + padding-top: var(--c--theme--spacings--auto); +} + +.pl-auto { + padding-left: var(--c--theme--spacings--auto); +} + +.pr-auto { + padding-right: var(--c--theme--spacings--auto); +} + +.p-bx { + padding: var(--c--theme--spacings--bx); +} + +.pb-bx { + padding-bottom: var(--c--theme--spacings--bx); +} + +.pt-bx { + padding-top: var(--c--theme--spacings--bx); +} + +.pl-bx { + padding-left: var(--c--theme--spacings--bx); +} + +.pr-bx { + padding-right: var(--c--theme--spacings--bx); +} + +.p-full { + padding: var(--c--theme--spacings--full); +} + +.pb-full { + padding-bottom: var(--c--theme--spacings--full); +} + +.pt-full { + padding-top: var(--c--theme--spacings--full); +} + +.pl-full { + padding-left: var(--c--theme--spacings--full); +} + +.pr-full { + padding-right: var(--c--theme--spacings--full); +} diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts new file mode 100644 index 00000000..18a7942f --- /dev/null +++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts @@ -0,0 +1,495 @@ +export const tokens = { + themes: { + default: { + theme: { + colors: { + 'secondary-text': '#555F6B', + 'secondary-100': '#F2F7FC', + 'secondary-200': '#EBF3FA', + 'secondary-300': '#E2EEF8', + 'secondary-400': '#DDEAF7', + 'secondary-500': '#D4E5F5', + 'secondary-600': '#C1D0DF', + 'secondary-700': '#97A3AE', + 'secondary-800': '#757E87', + 'secondary-900': '#596067', + 'info-text': '#FFFFFF', + 'info-100': '#EBF2FC', + 'info-200': '#8CB5EA', + 'info-300': '#5894E1', + 'info-400': '#377FDB', + 'info-500': '#055FD2', + 'info-600': '#0556BF', + 'info-700': '#044395', + 'info-800': '#033474', + 'info-900': '#022858', + 'greyscale-100': '#FAFAFB', + 'greyscale-200': '#F3F4F4', + 'greyscale-300': '#E7E8EA', + 'greyscale-400': '#C2C6CA', + 'greyscale-500': '#9EA3AA', + 'greyscale-600': '#79818A', + 'greyscale-700': '#555F6B', + 'greyscale-800': '#303C4B', + 'greyscale-900': '#0C1A2B', + 'greyscale-000': '#FFFFFF', + 'primary-100': '#EDF5FA', + 'primary-200': '#8CB5EA', + 'primary-300': '#5894E1', + 'primary-400': '#377FDB', + 'primary-500': '#055FD2', + 'primary-600': '#0556BF', + 'primary-700': '#044395', + 'primary-800': '#033474', + 'primary-900': '#022858', + 'success-100': '#EFFCD3', + 'success-200': '#DBFAA9', + 'success-300': '#BEF27C', + 'success-400': '#A0E659', + 'success-500': '#76D628', + 'success-600': '#5AB81D', + 'success-700': '#419A14', + 'success-800': '#2C7C0C', + 'success-900': '#1D6607', + 'warning-100': '#FFF8CD', + 'warning-200': '#FFEF9B', + 'warning-300': '#FFE469', + 'warning-400': '#FFDA43', + 'warning-500': '#FFC805', + 'warning-600': '#DBA603', + 'warning-700': '#B78702', + 'warning-800': '#936901', + 'warning-900': '#7A5400', + 'danger-100': '#F4B0B0', + 'danger-200': '#EE8A8A', + 'danger-300': '#E65454', + 'danger-400': '#E13333', + 'danger-500': '#DA0000', + 'danger-600': '#C60000', + 'danger-700': '#9B0000', + 'danger-800': '#780000', + 'danger-900': '#5C0000', + 'primary-text': '#FFFFFF', + 'success-text': '#FFFFFF', + 'warning-text': '#FFFFFF', + 'danger-text': '#FFFFFF', + 'card-border': '#DDDDDD', + 'primary-bg': '#FAFAFA', + 'primary-150': '#E5EEFA', + 'info-150': '#E5EEFA', + }, + font: { + sizes: { + h1: '2.2rem', + h2: '1.7rem', + h3: '1.37rem', + h4: '1.15rem', + h5: '1rem', + h6: '0.87rem', + l: '1rem', + m: '0.8125rem', + s: '0.75rem', + ml: '0.938rem', + xl: '1.50rem', + t: '0.6875rem', + }, + weights: { + thin: 100, + light: 300, + regular: 400, + medium: 500, + bold: 600, + extrabold: 800, + black: 900, + }, + families: { + base: '"Roboto Flex Variable", sans-serif', + accent: '"Roboto Flex Variable", sans-serif', + }, + letterSpacings: { + h1: 'normal', + h2: 'normal', + h3: 'normal', + h4: 'normal', + h5: '1px', + h6: 'normal', + l: 'normal', + m: 'normal', + s: 'normal', + }, + }, + spacings: { + '0': '0', + xl: '4rem', + l: '3rem', + b: '1.625rem', + s: '1rem', + t: '0.5rem', + st: '0.25rem', + none: '0', + auto: 'auto', + bx: '2.2rem', + full: '100%', + }, + transitions: { + 'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)', + 'ease-out': 'cubic-bezier(0.33, 1, 0.68, 1)', + 'ease-in-out': 'cubic-bezier(0.65, 0, 0.35, 1)', + duration: '250ms', + }, + breakpoints: { + xs: '480px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + xxl: '1400px', + xxs: '320px', + }, + }, + components: { + datagrid: { + header: { + weight: 'var(--c--theme--font--weights--extrabold)', + size: 'var(--c--theme--font--sizes--ml)', + }, + cell: { + color: 'var(--c--theme--colors--primary-500)', + size: 'var(--c--theme--font--sizes--ml)', + }, + }, + 'forms-checkbox': { + 'background-color': { hover: '#055fd214' }, + color: 'var(--c--theme--colors--primary-500)', + 'font-size': 'var(--c--theme--font--sizes--ml)', + }, + 'forms-datepicker': { + 'border-color': 'var(--c--theme--colors--primary-500)', + 'value-color': 'var(--c--theme--colors--primary-500)', + 'border-radius': { + hover: 'var(--c--components--forms-datepicker--border-radius)', + focus: 'var(--c--components--forms-datepicker--border-radius)', + }, + }, + 'forms-field': { + color: 'var(--c--theme--colors--primary-500)', + 'value-color': 'var(--c--theme--colors--primary-500)', + width: 'auto', + }, + 'forms-input': { + 'value-color': 'var(--c--theme--colors--primary-500)', + 'border-color': 'var(--c--theme--colors--primary-500)', + color: { + error: 'var(--c--theme--colors--danger-500)', + 'error-hover': 'var(--c--theme--colors--danger-500)', + 'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)', + }, + }, + 'forms-labelledbox': { + 'label-color': { + small: 'var(--c--theme--colors--primary-500)', + 'small-disabled': 'var(--c--theme--colors--greyscale-400)', + big: { disabled: 'var(--c--theme--colors--greyscale-400)' }, + }, + }, + 'forms-select': { + 'border-color': 'var(--c--theme--colors--primary-500)', + 'border-color-disabled-hover': + 'var(--c--theme--colors--greyscale-200)', + 'border-radius': { + hover: 'var(--c--components--forms-select--border-radius)', + focus: 'var(--c--components--forms-select--border-radius)', + }, + 'font-size': 'var(--c--theme--font--sizes--ml)', + 'menu-background-color': '#ffffff', + 'item-background-color': { + hover: 'var(--c--theme--colors--primary-300)', + }, + }, + 'forms-switch': { + 'accent-color': 'var(--c--theme--colors--primary-400)', + }, + 'forms-textarea': { + 'border-color': 'var(--c--components--forms-textarea--border-color)', + 'border-color-hover': + 'var(--c--components--forms-textarea--border-color)', + 'border-radius': { + hover: 'var(--c--components--forms-textarea--border-radius)', + focus: 'var(--c--components--forms-textarea--border-radius)', + }, + color: 'var(--c--theme--colors--primary-500)', + disabled: { + 'border-color-hover': 'var(--c--theme--colors--greyscale-200)', + }, + }, + modal: { 'background-color': '#ffffff' }, + button: { + 'border-radius': { + active: 'var(--c--components--button--border-radius)', + }, + 'medium-height': 'auto', + 'small-height': 'auto', + success: { + color: 'white', + 'color-disabled': 'white', + 'color-hover': 'white', + background: { + color: 'var(--c--theme--colors--success-600)', + 'color-disabled': 'var(--c--theme--colors--greyscale-300)', + 'color-hover': 'var(--c--theme--colors--success-800)', + }, + }, + danger: { + 'color-hover': 'white', + background: { + color: 'var(--c--theme--colors--danger-400)', + 'color-hover': 'var(--c--theme--colors--danger-500)', + 'color-disabled': 'var(--c--theme--colors--danger-100)', + }, + }, + primary: { + color: 'var(--c--theme--colors--primary-text)', + 'color-active': 'var(--c--theme--colors--primary-text)', + background: { + color: 'var(--c--theme--colors--primary-400)', + 'color-active': 'var(--c--theme--colors--primary-500)', + }, + border: { 'color-active': 'transparent' }, + }, + secondary: { + color: 'var(--c--theme--colors--primary-500)', + 'color-hover': 'var(--c--theme--colors--primary-text)', + background: { + color: 'white', + 'color-hover': 'var(--c--theme--colors--primary-700)', + }, + border: { color: 'var(--c--theme--colors--primary-200)' }, + }, + tertiary: { + color: 'var(--c--theme--colors--primary-text)', + 'color-disabled': 'var(--c--theme--colors--greyscale-600)', + background: { + 'color-hover': 'var(--c--theme--colors--primary-100)', + 'color-disabled': 'var(--c--theme--colors--greyscale-200)', + }, + }, + disabled: { color: 'white', background: { color: '#b3cef0' } }, + }, + }, + }, + dark: { + theme: { + colors: { + 'greyscale-100': '#182536', + 'greyscale-200': '#303C4B', + 'greyscale-300': '#555F6B', + 'greyscale-400': '#79818A', + 'greyscale-500': '#9EA3AA', + 'greyscale-600': '#C2C6CA', + 'greyscale-700': '#E7E8EA', + 'greyscale-800': '#F3F4F4', + 'greyscale-900': '#FAFAFB', + 'greyscale-000': '#0C1A2B', + 'primary-100': '#3B4C62', + 'primary-200': '#4D6481', + 'primary-300': '#6381A6', + 'primary-400': '#7FA5D5', + 'primary-500': '#8CB5EA', + 'primary-600': '#A3C4EE', + 'primary-700': '#C3D8F4', + 'primary-800': '#DDE9F8', + 'primary-900': '#F4F8FD', + 'success-100': '#EEF8D7', + 'success-200': '#D9F1B2', + 'success-300': '#BDE985', + 'success-400': '#A0E25D', + 'success-500': '#76D628', + 'success-600': '#5BB520', + 'success-700': '#43941A', + 'success-800': '#307414', + 'success-900': '#225D10', + 'warning-100': '#F7F3D5', + 'warning-200': '#F0E5AA', + 'warning-300': '#E8D680', + 'warning-400': '#E3C95F', + 'warning-500': '#D9B32B', + 'warning-600': '#BD9721', + 'warning-700': '#9D7B1C', + 'warning-800': '#7E6016', + 'warning-900': '#684D12', + 'danger-100': '#F8D0D0', + 'danger-200': '#F09898', + 'danger-300': '#F09898', + 'danger-400': '#ED8585', + 'danger-500': '#E96666', + 'danger-600': '#DD6666', + 'danger-700': '#C36666', + 'danger-800': '#AE6666', + 'danger-900': '#9D6666', + }, + }, + }, + dsfr: { + theme: { + colors: { + 'card-border': '#DDDDDD', + 'primary-text': '#000091', + 'primary-100': '#f5f5fe', + 'primary-150': '#F4F4FD', + 'primary-200': '#ececfe', + 'primary-300': '#e3e3fd', + 'primary-400': '#cacafb', + 'primary-500': '#6a6af4', + 'primary-600': '#000091', + 'primary-700': '#272747', + 'primary-800': '#21213f', + 'primary-900': '#1c1a36', + 'secondary-text': '#FFFFFF', + 'secondary-100': '#fee9ea', + 'secondary-200': '#fedfdf', + 'secondary-300': '#fdbfbf', + 'secondary-400': '#e1020f', + 'secondary-500': '#c91a1f', + 'secondary-600': '#5e2b2b', + 'secondary-700': '#3b2424', + 'secondary-800': '#341f1f', + 'secondary-900': '#2b1919', + 'greyscale-text': '#303C4B', + 'greyscale-000': '#f6f6f6', + 'greyscale-100': '#eeeeee', + 'greyscale-200': '#e5e5e5', + 'greyscale-300': '#e1e1e1', + 'greyscale-400': '#dddddd', + 'greyscale-500': '#cecece', + 'greyscale-600': '#7b7b7b', + 'greyscale-700': '#666666', + 'greyscale-800': '#2a2a2a', + 'greyscale-900': '#1e1e1e', + 'success-text': '#1f8d49', + 'success-100': '#dffee6', + 'success-200': '#b8fec9', + 'success-300': '#88fdaa', + 'success-400': '#3bea7e', + 'success-500': '#1f8d49', + 'success-600': '#18753c', + 'success-700': '#204129', + 'success-800': '#1e2e22', + 'success-900': '#19281d', + 'info-text': '#0078f3', + 'info-100': '#f4f6ff', + 'info-200': '#e8edff', + 'info-300': '#dde5ff', + 'info-400': '#bdcdff', + 'info-500': '#0078f3', + 'info-600': '#0063cb', + 'info-700': '#f4f6ff', + 'info-800': '#222a3f', + 'info-900': '#1d2437', + 'warning-text': '#d64d00', + 'warning-100': '#fff4f3', + 'warning-200': '#ffe9e6', + 'warning-300': '#ffded9', + 'warning-400': '#ffbeb4', + 'warning-500': '#d64d00', + 'warning-600': '#b34000', + 'warning-700': '#5e2c21', + 'warning-800': '#3e241e', + 'warning-900': '#361e19', + 'danger-text': '#e1000f', + 'danger-100': '#fef4f4', + 'danger-200': '#fee9e9', + 'danger-300': '#fddede', + 'danger-400': '#fcbfbf', + 'danger-500': '#e1000f', + 'danger-600': '#c9191e', + 'danger-700': '#642727', + 'danger-800': '#412121', + 'danger-900': '#3a1c1c', + }, + font: { families: { accent: 'Marianne', base: 'Marianne' } }, + }, + components: { + alert: { 'border-radius': '0' }, + button: { + 'medium-height': '48px', + 'border-radius': '4px', + primary: { + background: { + color: 'var(--c--theme--colors--primary-text)', + 'color-hover': '#1212ff', + 'color-active': '#2323ff', + }, + color: '#ffffff', + 'color-hover': '#ffffff', + 'color-active': '#ffffff', + }, + 'primary-text': { + background: { + 'color-hover': 'var(--c--theme--colors--primary-100)', + 'color-active': 'var(--c--theme--colors--primary-100)', + }, + 'color-hover': 'var(--c--theme--colors--primary-text)', + }, + secondary: { + background: { 'color-hover': '#F6F6F6', 'color-active': '#EDEDED' }, + border: { + color: 'var(--c--theme--colors--primary-600)', + 'color-hover': 'var(--c--theme--colors--primary-600)', + }, + color: 'var(--c--theme--colors--primary-text)', + }, + 'tertiary-text': { + background: { + 'color-hover': 'var(--c--theme--colors--primary-100)', + }, + 'color-hover': 'var(--c--theme--colors--primary-text)', + }, + }, + datagrid: { + header: { + color: 'var(--c--theme--colors--primary-text)', + size: 'var(--c--theme--font--sizes--s)', + }, + body: { + 'background-color': 'transparent', + 'background-color-hover': '#F4F4FD', + }, + pagination: { + 'background-color': 'transparent', + 'background-color-active': 'var(--c--theme--colors--primary-300)', + }, + }, + 'forms-checkbox': { + 'border-radius': '0', + color: 'var(--c--theme--colors--primary-text)', + }, + 'forms-datepicker': { 'border-radius': '0' }, + 'forms-fileuploader': { 'border-radius': '0' }, + 'forms-field': { color: 'var(--c--theme--colors--primary-text)' }, + 'forms-input': { + 'border-radius': '4px', + 'background-color': '#ffffff', + 'border-color': 'var(--c--theme--colors--primary-text)', + 'box-shadow-color': 'var(--c--theme--colors--primary-text)', + 'value-color': 'var(--c--theme--colors--primary-text)', + }, + 'forms-labelledbox': { + 'label-color': { big: 'var(--c--theme--colors--primary-text)' }, + }, + 'forms-select': { + 'border-radius': '4px', + 'border-radius-hover': '4px', + 'background-color': '#ffffff', + 'border-color': 'var(--c--theme--colors--primary-text)', + 'border-color-hover': 'var(--c--theme--colors--primary-text)', + 'box-shadow-color': 'var(--c--theme--colors--primary-text)', + }, + 'forms-switch': { + 'handle-border-radius': '2px', + 'rail-border-radius': '4px', + }, + 'forms-textarea': { 'border-radius': '0' }, + }, + }, + }, +}; diff --git a/src/frontend/apps/impress/src/cunningham/index.ts b/src/frontend/apps/impress/src/cunningham/index.ts new file mode 100644 index 00000000..e955768b --- /dev/null +++ b/src/frontend/apps/impress/src/cunningham/index.ts @@ -0,0 +1,4 @@ +import { tokens } from './cunningham-tokens'; +import useCunninghamTheme from './useCunninghamTheme'; + +export { tokens, useCunninghamTheme }; diff --git a/src/frontend/apps/impress/src/cunningham/useCunninghamTheme.tsx b/src/frontend/apps/impress/src/cunningham/useCunninghamTheme.tsx new file mode 100644 index 00000000..e858286f --- /dev/null +++ b/src/frontend/apps/impress/src/cunningham/useCunninghamTheme.tsx @@ -0,0 +1,32 @@ +import merge from 'lodash/merge'; +import { create } from 'zustand'; + +import { tokens } from './cunningham-tokens'; + +type Tokens = typeof tokens.themes.default & Partial; +type ColorsTokens = Tokens['theme']['colors']; +type ComponentTokens = Tokens['components']; +type Theme = 'default' | 'dsfr'; + +interface AuthStore { + theme: Theme; + setTheme: (theme: Theme) => void; + colorsTokens: () => Partial; + componentTokens: () => ComponentTokens; +} + +const useCunninghamTheme = create((set, get) => { + const currentTheme = () => + merge(tokens.themes['default'], tokens.themes[get().theme]) as Tokens; + + return { + theme: 'dsfr', + colorsTokens: () => currentTheme().theme.colors, + componentTokens: () => currentTheme().components, + setTheme: (theme: Theme) => { + set({ theme }); + }, + }; +}); + +export default useCunninghamTheme; diff --git a/src/frontend/apps/impress/src/custom-next.d.ts b/src/frontend/apps/impress/src/custom-next.d.ts new file mode 100644 index 00000000..5919942a --- /dev/null +++ b/src/frontend/apps/impress/src/custom-next.d.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +declare module '*.svg' { + import * as React from 'react'; + + const ReactComponent: React.FunctionComponent< + React.SVGProps & { + title?: string; + } + >; + + export default ReactComponent; +} + +declare module '*.svg?url' { + const content: string; + export default content; +} + +namespace NodeJS { + interface ProcessEnv { + NEXT_PUBLIC_API_URL?: string; + } +} diff --git a/src/frontend/apps/impress/src/features/addMembers/api/index.ts b/src/frontend/apps/impress/src/features/addMembers/api/index.ts new file mode 100644 index 00000000..1b0d23c0 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/api/index.ts @@ -0,0 +1,3 @@ +export * from './useCreateInvitation'; +export * from './useCreateTeamAccess'; +export * from './useUsers'; diff --git a/src/frontend/apps/impress/src/features/addMembers/api/useCreateInvitation.tsx b/src/frontend/apps/impress/src/features/addMembers/api/useCreateInvitation.tsx new file mode 100644 index 00000000..6e3b8bc9 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/api/useCreateInvitation.tsx @@ -0,0 +1,46 @@ +import { useMutation } from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; +import { User } from '@/core/auth'; +import { Invitation } from '@/features/members'; +import { Role, Team } from '@/features/teams'; + +import { OptionType } from '../types'; + +interface CreateInvitationParams { + email: User['email']; + role: Role; + teamId: Team['id']; +} + +export const createInvitation = async ({ + email, + role, + teamId, +}: CreateInvitationParams): Promise => { + const response = await fetchAPI(`teams/${teamId}/invitations/`, { + method: 'POST', + body: JSON.stringify({ + email, + role, + }), + }); + + if (!response.ok) { + throw new APIError( + `Failed to create the invitation for ${email}`, + await errorCauses(response, { + value: email, + type: OptionType.INVITATION, + }), + ); + } + + return response.json() as Promise; +}; + +export function useCreateInvitation() { + return useMutation({ + mutationFn: createInvitation, + }); +} diff --git a/src/frontend/apps/impress/src/features/addMembers/api/useCreateTeamAccess.tsx b/src/frontend/apps/impress/src/features/addMembers/api/useCreateTeamAccess.tsx new file mode 100644 index 00000000..08f8e91d --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/api/useCreateTeamAccess.tsx @@ -0,0 +1,60 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; +import { User } from '@/core/auth'; +import { Access, KEY_LIST_TEAM_ACCESSES } from '@/features/members'; +import { KEY_LIST_TEAM, KEY_TEAM, Role, Team } from '@/features/teams'; + +import { OptionType } from '../types'; + +interface CreateTeamAccessParams { + name: User['name']; + role: Role; + teamId: Team['id']; + userId: User['id']; +} + +export const createTeamAccess = async ({ + userId, + name, + role, + teamId, +}: CreateTeamAccessParams): Promise => { + const response = await fetchAPI(`teams/${teamId}/accesses/`, { + method: 'POST', + body: JSON.stringify({ + user: userId, + role, + }), + }); + + if (!response.ok) { + throw new APIError( + `Failed to add ${name} in the team.`, + await errorCauses(response, { + value: name, + type: OptionType.NEW_MEMBER, + }), + ); + } + + return response.json() as Promise; +}; + +export function useCreateTeamAccess() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createTeamAccess, + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_TEAM], + }); + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_TEAM_ACCESSES], + }); + void queryClient.invalidateQueries({ + queryKey: [KEY_TEAM], + }); + }, + }); +} diff --git a/src/frontend/apps/impress/src/features/addMembers/api/useUsers.tsx b/src/frontend/apps/impress/src/features/addMembers/api/useUsers.tsx new file mode 100644 index 00000000..d5af57d5 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/api/useUsers.tsx @@ -0,0 +1,43 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import { APIError, APIList, errorCauses, fetchAPI } from '@/api'; +import { User } from '@/core/auth'; +import { Team } from '@/features/teams'; + +export type UsersParams = { + query: string; + teamId: Team['id']; +}; + +type UsersResponse = APIList; + +export const getUsers = async ({ + query, + teamId, +}: UsersParams): Promise => { + const queriesParams = []; + queriesParams.push(query ? `q=${query}` : ''); + queriesParams.push(teamId ? `team_id=${teamId}` : ''); + const queryParams = queriesParams.filter(Boolean).join('&'); + + const response = await fetchAPI(`users/?${queryParams}`); + + if (!response.ok) { + throw new APIError('Failed to get the users', await errorCauses(response)); + } + + return response.json() as Promise; +}; + +export const KEY_LIST_USER = 'users'; + +export function useUsers( + param: UsersParams, + queryConfig?: UseQueryOptions, +) { + return useQuery({ + queryKey: [KEY_LIST_USER, param], + queryFn: () => getUsers(param), + ...queryConfig, + }); +} diff --git a/src/frontend/apps/impress/src/features/addMembers/assets/add-member.svg b/src/frontend/apps/impress/src/features/addMembers/assets/add-member.svg new file mode 100644 index 00000000..08de3aae --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/assets/add-member.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/frontend/apps/impress/src/features/addMembers/components/ModalAddMembers.tsx b/src/frontend/apps/impress/src/features/addMembers/components/ModalAddMembers.tsx new file mode 100644 index 00000000..01c16064 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/components/ModalAddMembers.tsx @@ -0,0 +1,186 @@ +import { + Button, + Modal, + ModalSize, + VariantType, + useToastProvider, +} from '@openfun/cunningham-react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { createGlobalStyle } from 'styled-components'; + +import { APIError } from '@/api'; +import { Box, Text } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { ChooseRole } from '@/features/members'; +import { Role, Team } from '@/features/teams'; + +import { useCreateInvitation, useCreateTeamAccess } from '../api'; +import IconAddMember from '../assets/add-member.svg'; +import { + OptionInvitation, + OptionNewMember, + OptionSelect, + OptionType, + isOptionNewMember, +} from '../types'; + +import { OptionsSelect, SearchMembers } from './SearchMembers'; + +const GlobalStyle = createGlobalStyle` + .c__modal { + overflow: visible; + } +`; + +type APIErrorMember = APIError<{ + value: string; + type: OptionType; +}>; + +interface ModalAddMembersProps { + currentRole: Role; + onClose: () => void; + team: Team; +} + +export const ModalAddMembers = ({ + currentRole, + onClose, + team, +}: ModalAddMembersProps) => { + const { colorsTokens } = useCunninghamTheme(); + const { t } = useTranslation(); + const [selectedMembers, setSelectedMembers] = useState([]); + const [selectedRole, setSelectedRole] = useState(Role.MEMBER); + const { toast } = useToastProvider(); + const { mutateAsync: createInvitation } = useCreateInvitation(); + const { mutateAsync: createTeamAccess } = useCreateTeamAccess(); + + const switchActions = (selectedMembers: OptionsSelect) => + selectedMembers.map(async (selectedMember) => { + switch (selectedMember.type) { + case OptionType.INVITATION: + await createInvitation({ + email: selectedMember.value.email, + role: selectedRole, + teamId: team.id, + }); + break; + + case OptionType.NEW_MEMBER: + await createTeamAccess({ + name: selectedMember.value.name, + role: selectedRole, + teamId: team.id, + userId: selectedMember.value.id, + }); + break; + } + + return selectedMember; + }); + + const toastOptions = { + duration: 4000, + }; + + const onError = (dataError: APIErrorMember['data']) => { + const messageError = + dataError?.type === OptionType.INVITATION + ? t(`Failed to create the invitation for {{email}}`, { + email: dataError?.value, + }) + : t(`Failed to add {{name}} in the team`, { + name: dataError?.value, + }); + + toast(messageError, VariantType.ERROR, toastOptions); + }; + + const onSuccess = (option: OptionSelect) => { + const message = !isOptionNewMember(option) + ? t('Invitation sent to {{email}}', { + email: option.value.email, + }) + : t('Member {{name}} added to the team', { + name: option.value.name, + }); + + toast(message, VariantType.SUCCESS, toastOptions); + }; + + const handleValidate = async () => { + const settledPromises = await Promise.allSettled< + OptionInvitation | OptionNewMember + >(switchActions(selectedMembers)); + + onClose(); + settledPromises.forEach((settledPromise) => { + switch (settledPromise.status) { + case 'rejected': + onError((settledPromise.reason as APIErrorMember).data); + break; + + case 'fulfilled': + onSuccess(settledPromise.value); + break; + } + }); + }; + + return ( + + {t('Cancel')} + + } + onClose={onClose} + closeOnClickOutside + hideCloseButton + rightActions={ + + } + size={ModalSize.MEDIUM} + title={ + + + + {t('Add a member')} + + + } + > + + + + {selectedMembers.length > 0 && ( + + + {t('Choose a role')} + + + + )} + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/addMembers/components/SearchMembers.tsx b/src/frontend/apps/impress/src/features/addMembers/components/SearchMembers.tsx new file mode 100644 index 00000000..0b821e46 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/components/SearchMembers.tsx @@ -0,0 +1,120 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Options } from 'react-select'; +import AsyncSelect from 'react-select/async'; + +import { Team } from '@/features/teams'; +import { isValidEmail } from '@/utils'; + +import { KEY_LIST_USER, useUsers } from '../api/useUsers'; +import { OptionSelect, OptionType } from '../types'; + +export type OptionsSelect = Options; + +interface SearchMembersProps { + team: Team; + selectedMembers: OptionsSelect; + setSelectedMembers: (value: OptionsSelect) => void; +} + +export const SearchMembers = ({ + team, + selectedMembers, + setSelectedMembers, +}: SearchMembersProps) => { + const { t } = useTranslation(); + const [input, setInput] = useState(''); + const [userQuery, setUserQuery] = useState(''); + const resolveOptionsRef = useRef<((value: OptionsSelect) => void) | null>( + null, + ); + const { data } = useUsers( + { query: userQuery, teamId: team.id }, + { + enabled: !!userQuery, + queryKey: [KEY_LIST_USER, { query: userQuery }], + }, + ); + + const options = data?.results; + + useEffect(() => { + if (!resolveOptionsRef.current || !options) { + return; + } + + const optionsFiltered = options.filter( + (user) => + !selectedMembers?.find( + (selectedUser) => selectedUser.value.email === user.email, + ), + ); + + let users: OptionsSelect = optionsFiltered.map((user) => ({ + value: user, + label: user.name || user.email, + type: OptionType.NEW_MEMBER, + })); + + if (userQuery && isValidEmail(userQuery)) { + const isFoundUser = !!optionsFiltered.find( + (user) => user.email === userQuery, + ); + const isFoundEmail = !!selectedMembers.find( + (selectedMember) => selectedMember.value.email === userQuery, + ); + + if (!isFoundUser && !isFoundEmail) { + users = [ + { + value: { email: userQuery }, + label: userQuery, + type: OptionType.INVITATION, + }, + ]; + } + } + + resolveOptionsRef.current(users); + resolveOptionsRef.current = null; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [options, selectedMembers]); + + const loadOptions = (): Promise => { + return new Promise((resolve) => { + resolveOptionsRef.current = resolve; + }); + }; + + const timeout = useRef(null); + const onInputChangeHandle = useCallback((newValue: string) => { + setInput(newValue); + if (timeout.current) { + clearTimeout(timeout.current); + } + + timeout.current = setTimeout(() => { + setUserQuery(newValue); + }, 1000); + }, []); + + return ( + + t('Invite new members to {{teamName}}', { teamName: team.name }) + } + onChange={(value) => { + setInput(''); + setUserQuery(''); + setSelectedMembers(value); + }} + /> + ); +}; diff --git a/src/frontend/apps/impress/src/features/addMembers/index.ts b/src/frontend/apps/impress/src/features/addMembers/index.ts new file mode 100644 index 00000000..23fc0dd8 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/index.ts @@ -0,0 +1 @@ +export * from './components/ModalAddMembers'; diff --git a/src/frontend/apps/impress/src/features/addMembers/types.tsx b/src/frontend/apps/impress/src/features/addMembers/types.tsx new file mode 100644 index 00000000..d33c83c2 --- /dev/null +++ b/src/frontend/apps/impress/src/features/addMembers/types.tsx @@ -0,0 +1,26 @@ +import { User } from '@/core/auth'; + +export enum OptionType { + INVITATION = 'invitation', + NEW_MEMBER = 'new_member', +} + +export const isOptionNewMember = ( + data: OptionSelect, +): data is OptionNewMember => { + return 'id' in data.value; +}; + +export interface OptionInvitation { + value: { email: string }; + label: string; + type: OptionType.INVITATION; +} + +export interface OptionNewMember { + value: User; + label: string; + type: OptionType.NEW_MEMBER; +} + +export type OptionSelect = OptionNewMember | OptionInvitation; diff --git a/src/frontend/apps/impress/src/features/header/ApplicationsMenu.tsx b/src/frontend/apps/impress/src/features/header/ApplicationsMenu.tsx new file mode 100644 index 00000000..ca777561 --- /dev/null +++ b/src/frontend/apps/impress/src/features/header/ApplicationsMenu.tsx @@ -0,0 +1,34 @@ +import Script from 'next/script'; +import React from 'react'; + +/** + * ApplicationsMenu Component + * + * This component is a work in progress (WIP) and serves as a proof of concept (POC) to showcase a future advanced feature. + * The purpose of this component is to render an applications menu for La Suite, allowing users to switch between different La Suite apps seamlessly. + * To ensure synchronized content across applications, it utilizes an iframe hosted on Scalingo. + * + * This PoC has been created by @manuhabitela. + * + * It includes external CSS and JavaScript files for styling and functionality. + * + * Style has to be included as well: https://suite-numerique-gaufre.osc-fr1.scalingo.io/public/styles/gaufre-vanilla.css \ + * To respect next.js standards, the css is included using the `_document.ts` component. + * @see https://github.com/numerique-gouv/impress/blob/main/src/frontend/apps/impress/src/pages/_document.tsx#L8 + */ +export const ApplicationsMenu = () => ( + <> +