Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e082c2146 | |||
| f4b18627db | |||
| 70f5b90888 | |||
| 3f88127dc3 | |||
| d2c8fb4408 | |||
| fd49e59c8f | |||
| a72dc4c648 | |||
| 2dc6c92b1a | |||
| 601b285d51 | |||
| f5a45f9ae1 | |||
| b5c5879250 | |||
| e0d844a7e6 | |||
| 8bf6e20d3a | |||
| 4ca38a1cd0 |
@@ -8,11 +8,16 @@ const config = {
|
||||
'primary-100': '#EDF5FA',
|
||||
'primary-150': '#E5EEFA',
|
||||
'info-150': '#E5EEFA',
|
||||
'grey-400': '#929292',
|
||||
'grey-800': '#2A2A2A',
|
||||
},
|
||||
font: {
|
||||
letterSpacings: {
|
||||
h5: 'normal',
|
||||
},
|
||||
sizes: {
|
||||
ml: '0.938rem',
|
||||
xl: '1.50rem',
|
||||
xl: '1.25rem',
|
||||
t: '0.6875rem',
|
||||
s: '0.75rem',
|
||||
h1: '2.2rem',
|
||||
@@ -28,205 +33,40 @@ const config = {
|
||||
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',
|
||||
'primary-50': '#F5F5FE',
|
||||
'primary-100': '#ECECFE',
|
||||
'primary-150': '#410093',
|
||||
'primary-200': '#E3E3FD',
|
||||
'primary-300': '#CACAFB',
|
||||
'primary-400': '#6A6AF4',
|
||||
'primary-500': '#000091',
|
||||
'secondary-050': '#FEF4F4',
|
||||
'secondary-100': '#FEE9E9',
|
||||
'secondary-200': '#FDDEDE',
|
||||
'secondary-300': '#FCBFBF',
|
||||
'secondary-400': '#E1000F',
|
||||
'secondary-500': '#C9191E',
|
||||
'greyscale-000': '#FFFFFF',
|
||||
'greyscale-050': '#F6F6F6',
|
||||
'greyscale-100': '#EEEEEE',
|
||||
'greyscale-200': '#E5E5E5',
|
||||
'greyscale-250': '#DDDDDD',
|
||||
'greyscale-300': '#CECECE',
|
||||
'greyscale-400': '#929292',
|
||||
'greyscale-500': '#666666',
|
||||
'greyscale-700': '#3A3A3A',
|
||||
'greyscale-1000': '#161616',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-100': '#B8FEC9',
|
||||
'success-200': '#b8fec9',
|
||||
'success-300': '#88fdaa',
|
||||
'success-400': '#3bea7e',
|
||||
'success-500': '#1f8d49',
|
||||
'success-500': '#18753C',
|
||||
'success-600': '#18753c',
|
||||
'success-700': '#204129',
|
||||
'success-800': '#1e2e22',
|
||||
@@ -272,13 +112,18 @@ const config = {
|
||||
components: {
|
||||
alert: {
|
||||
'border-radius': '0',
|
||||
'background-color': 'var(--c--theme--colors--greyscale-000)',
|
||||
},
|
||||
modal: {
|
||||
'box-shadow': '0px 6px 18px 0px rgba(0, 0, 18, 0.16);',
|
||||
},
|
||||
button: {
|
||||
'medium-height': '48px',
|
||||
'border-radius': '4px',
|
||||
'border-radius': '0',
|
||||
'medium-text-height': '48px',
|
||||
primary: {
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'color-hover': '#1212ff',
|
||||
'color-active': '#2323ff',
|
||||
},
|
||||
@@ -291,7 +136,8 @@ const config = {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
|
||||
'color-hover': 'var(--c--theme--colors--primary-500)',
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
@@ -299,10 +145,11 @@ const config = {
|
||||
'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-500)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-250)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
['color-hover']: 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
@@ -313,7 +160,7 @@ const config = {
|
||||
},
|
||||
datagrid: {
|
||||
header: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
size: 'var(--c--theme--font--sizes--s)',
|
||||
},
|
||||
body: {
|
||||
@@ -325,45 +172,48 @@ const config = {
|
||||
'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',
|
||||
'background-color': 'var(--c--theme--colors--greyscale-050)',
|
||||
'border-radius': '5px 5px 0 0',
|
||||
|
||||
'border-color': 'var(--c--theme--colors--greyscale-100)',
|
||||
|
||||
'border-color--focus': 'var(--c--theme--colors--greyscale-1000)',
|
||||
'border-width': '0 0 2px 0',
|
||||
'label-color--focus':
|
||||
'var(--c--components--forms-labelledbox--label-color--small)',
|
||||
},
|
||||
'forms-textarea': {
|
||||
'background-color': 'var(--c--theme--colors--greyscale-050)',
|
||||
'border-radius': '5px 5px 0 0',
|
||||
'border-color': 'var(--c--theme--colors--greyscale-100)',
|
||||
'border-color--focus': 'var(--c--theme--colors--greyscale-1000)',
|
||||
'border-width': '0 0 2px 0',
|
||||
'border-color--hover': 'var(--c--theme--colors--greyscale-1000)',
|
||||
'label-color--focus':
|
||||
'var(--c--components--forms-labelledbox--label-color--small)',
|
||||
},
|
||||
'forms-select': {
|
||||
'background-color': 'var(--c--theme--colors--greyscale-100)',
|
||||
'border-radius': '0',
|
||||
'border-color': 'var(--c--theme--colors--greyscale-1000)',
|
||||
'border-width': '0 0 2px 0',
|
||||
'border-color--focus': '#0974F6',
|
||||
'border-color--hover': 'var(--c--theme--colors--greyscale-1000)',
|
||||
'label-color--focus':
|
||||
'var(--c--components--forms-labelledbox--label-color--big)',
|
||||
},
|
||||
'forms-switch': {
|
||||
'accent-color': '#2323ff',
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'accent-color': '#2323ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const path = require('path');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
@@ -9,6 +11,13 @@ const nextConfig = {
|
||||
// Enables the styled-components SWC transform
|
||||
styledComponents: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
sassOptions: {
|
||||
includePaths: [
|
||||
path.join(__dirname, 'styles'),
|
||||
path.join(__dirname, 'components'),
|
||||
],
|
||||
},
|
||||
webpack(config) {
|
||||
// Grab the existing rule that handles SVG imports
|
||||
const fileLoaderRule = config.module.rules.find((rule) =>
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
"dependencies": {
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hookform/resolvers": "3.9.0",
|
||||
"@ngneat/falso": "^7.2.0",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@tanstack/react-query": "5.56.2",
|
||||
"classnames": "^2.5.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"i18next": "23.15.1",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
@@ -30,6 +33,7 @@
|
||||
"react-i18next": "15.0.2",
|
||||
"react-select": "5.8.1",
|
||||
"styled-components": "6.1.13",
|
||||
"use-debounce": "^10.0.3",
|
||||
"zod": "3.23.8",
|
||||
"zustand": "4.5.5"
|
||||
},
|
||||
@@ -54,6 +58,7 @@
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.3.3",
|
||||
"sass": "1.79.1",
|
||||
"stylelint": "16.9.0",
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-prettier": "5.0.2",
|
||||
|
||||
@@ -2,6 +2,11 @@ import { useAuthStore } from '@/core/auth';
|
||||
|
||||
import { baseApiUrl } from './conf';
|
||||
|
||||
interface CheckStatusOptions {
|
||||
fallbackValue: unknown;
|
||||
ignoredErrorStatus: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the CSRF token from the document's cookies.
|
||||
*
|
||||
@@ -41,3 +46,37 @@ export const fetchAPI = async (
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export async function checkStatus<T>(
|
||||
response: Response,
|
||||
options: CheckStatusOptions = { fallbackValue: null, ignoredErrorStatus: [] },
|
||||
): Promise<T> {
|
||||
if (response.ok) {
|
||||
if (response.headers.get('Content-Type') === 'application/json') {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
if (response.headers.get('Content-Type') === 'application/pdf') {
|
||||
return response.blob() as Promise<T>;
|
||||
}
|
||||
return response.text() as Promise<T>;
|
||||
}
|
||||
|
||||
if (options.ignoredErrorStatus.includes(response.status)) {
|
||||
return Promise.resolve(options.fallbackValue) as Promise<T>;
|
||||
}
|
||||
|
||||
const data: T = (await response.json()) as T;
|
||||
|
||||
throw new HttpError(response.status, response.statusText, data);
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
code: number;
|
||||
data: unknown;
|
||||
|
||||
constructor(status: number, statusText: string, data: unknown) {
|
||||
super(statusText);
|
||||
this.code = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg viewBox="0 0 41 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35.7941 20.6945C33.8535 20.6945 32.052 19.4495 31.4319 17.4987C30.6646 15.0855 31.9962 12.5063 34.406 11.7379C34.868 11.5906 35.3361 11.5205 35.7965 11.5205C37.7371 11.5205 39.5386 12.7656 40.1587 14.7161C40.9261 17.1295 39.5944 19.7087 37.1846 20.4772C36.7227 20.6244 36.2544 20.6945 35.7941 20.6945Z" fill="#000091"/>
|
||||
<path d="M34.5271 27.8442C35.25 27.4943 35.7975 26.809 35.9377 25.9554C36.1665 24.5616 35.2238 23.2458 33.832 23.0167C33.0544 22.8887 32.2816 22.8267 31.5179 22.8267C30.0882 22.8267 28.6926 23.0442 27.3699 23.452C26.4866 22.2232 25.8084 20.8654 25.3541 19.4365C24.8882 17.9708 24.6574 16.4304 24.6814 14.8774C26.7391 14.2179 28.6763 13.0776 30.315 11.4558C30.8765 10.9003 31.1266 10.1487 31.0647 9.41846C31.1336 9.9223 31.0533 10.4517 30.8025 10.9377C28.4985 15.4006 30.2496 20.9084 34.7064 23.2161C35.9591 23.8649 36.4499 25.4081 35.8021 26.6625C35.5168 27.2153 35.058 27.6199 34.5271 27.8442Z" fill="#000091"/>
|
||||
<path d="M29.6422 38.9999C28.2165 38.9999 26.8121 38.3354 25.9176 37.0873C24.4433 35.0295 24.9138 32.1644 26.9685 30.6878C27.7768 30.107 28.7102 29.8276 29.6343 29.8276C31.0598 29.8276 32.4644 30.4919 33.3587 31.7402C34.8332 33.798 34.3628 36.6631 32.3081 38.1395C31.4997 38.7205 30.5664 38.9999 29.6422 38.9999Z" fill="#E1000F"/>
|
||||
<path d="M20.0313 37.7184C19.8934 37.7184 19.7541 37.7071 19.6136 37.6842C19.0005 37.583 18.4746 37.2713 18.0977 36.8349C18.5686 37.3268 19.229 37.6241 19.9395 37.6241C20.0673 37.6241 20.1968 37.6145 20.3271 37.5946C21.7217 37.3818 22.6799 36.0774 22.4675 34.681C22.1266 32.4399 21.2783 30.4005 20.0573 28.6611C20.9513 27.4401 22.0312 26.3748 23.2479 25.5005C24.4958 24.6038 25.8874 23.9079 27.3698 23.4512C28.632 25.2071 30.3135 26.6994 32.3601 27.759C32.7353 27.9534 33.1362 28.0454 33.5312 28.0454C33.8743 28.0454 34.2133 27.9757 34.5269 27.8433C34.1886 28.0071 33.8119 28.0976 33.4204 28.0976C33.2828 28.0976 33.1432 28.0862 33.0029 28.0633C32.5052 27.9811 32.0101 27.9416 31.5212 27.9416C27.1486 27.9416 23.2809 31.1162 22.5482 35.5754C22.3423 36.8286 21.2591 37.7184 20.0313 37.7184Z" fill="#E1000F"/>
|
||||
<path d="M33.5248 28.0462C33.1297 28.0462 32.7288 27.9543 32.3537 27.7599C30.307 26.7003 28.6255 25.208 27.3633 23.452C28.686 23.0442 30.0815 22.8267 31.5113 22.8267C32.2749 22.8267 33.0477 22.8887 33.8253 23.0167C35.2172 23.2458 36.1599 24.5616 35.9311 25.9554C35.7908 26.809 35.2433 27.4942 34.5205 27.8442C34.2068 27.9765 33.8679 28.0462 33.5248 28.0462Z" fill="#6D2700"/>
|
||||
<path d="M10.3416 38.7923C9.39936 38.7923 8.44894 38.5023 7.63175 37.9003C5.59448 36.3995 5.15785 33.5291 6.65647 31.489C7.55399 30.2671 8.94222 29.6201 10.3489 29.6201C11.2912 29.6201 12.2416 29.9104 13.0588 30.5124C15.096 32.013 15.5327 34.8836 14.034 36.9237C13.1366 38.1454 11.7483 38.7923 10.3416 38.7923Z" fill="#E1000F"/>
|
||||
<path d="M18.0934 36.8362C17.7418 36.4693 17.4958 35.9937 17.4135 35.4527C16.7284 30.9489 12.8409 27.7125 8.42383 27.7125C7.96983 27.7125 7.51056 27.7468 7.04827 27.8171C6.91806 27.837 6.78862 27.8466 6.6608 27.8466C5.42004 27.8466 4.33134 26.9397 4.13887 25.6739C4.04527 25.0586 4.17885 24.4614 4.47689 23.9673C4.0973 24.6761 4.05657 25.5525 4.4443 26.3257C4.89261 27.2199 5.79338 27.7363 6.72875 27.7363C7.11393 27.7363 7.5051 27.6487 7.87279 27.4637C9.89584 26.4467 11.5707 25.0083 12.8452 23.3081C14.2811 23.7821 15.6267 24.4815 16.8329 25.3701C18.0701 26.2816 19.1611 27.3918 20.053 28.6624C18.7755 30.4072 17.8777 32.4698 17.5038 34.7465C17.3756 35.5265 17.6144 36.2821 18.0934 36.8362Z" fill="#E1000F"/>
|
||||
<path d="M19.9345 37.6241C19.224 37.6241 18.5636 37.3268 18.0926 36.8349C17.6137 36.2808 17.3748 35.5252 17.5031 34.7452C17.8769 32.4685 18.7747 30.4059 20.0522 28.6611C21.2733 30.4005 22.1216 32.4399 22.4625 34.681C22.6749 36.0774 21.7167 37.3819 20.3221 37.5946C20.1918 37.6145 20.0623 37.6241 19.9345 37.6241Z" fill="#6D2700"/>
|
||||
<path d="M4.58127 20.3585C4.1036 20.3585 3.618 20.2832 3.13975 20.1244C0.739113 19.3276 -0.561933 16.7329 0.233815 14.3288C0.871033 12.4037 2.6598 11.1846 4.57985 11.1846C5.05753 11.1846 5.54314 11.26 6.02139 11.4187C8.42203 12.2156 9.72307 14.8105 8.92732 17.2145C8.29011 19.1395 6.50134 20.3585 4.58127 20.3585Z" fill="#000091"/>
|
||||
<path d="M4.48047 23.9669C4.72047 23.5187 5.09596 23.1376 5.58427 22.8921C10.0678 20.6379 11.8839 15.1512 9.63285 10.6609C9.00015 9.39848 9.50889 7.86127 10.7693 7.22763C11.137 7.04273 11.5283 6.95508 11.9135 6.95508C12.11 6.95508 12.305 6.97788 12.4945 7.02216C12.3443 6.99509 12.192 6.98153 12.0397 6.98153C11.3912 6.98153 10.7423 7.22739 10.2446 7.71995C9.2413 8.71285 9.23173 10.3323 10.2233 11.3369C11.8145 12.9493 13.6979 14.1001 15.7066 14.7884C15.7001 16.3025 15.4516 17.8001 14.9805 19.2236C14.4972 20.6835 13.7798 22.0655 12.8488 23.3077C11.4468 22.8447 9.9589 22.5965 8.43031 22.5965C7.72172 22.5965 7.00417 22.6498 6.28274 22.7599C5.50254 22.879 4.85893 23.3396 4.48047 23.9669Z" fill="#000091"/>
|
||||
<path d="M6.73234 27.7346C5.79697 27.7346 4.89619 27.2182 4.44789 26.324C4.06016 25.5508 4.10088 24.6744 4.48048 23.9656C4.85894 23.3383 5.50255 22.8776 6.28274 22.7586C7.00418 22.6485 7.72173 22.5952 8.43032 22.5952C9.95891 22.5952 11.4468 22.8433 12.8488 23.3064C11.5743 25.0067 9.89943 26.445 7.87638 27.462C7.50869 27.647 7.11751 27.7346 6.73234 27.7346Z" fill="#6D2700"/>
|
||||
<path d="M20.3024 9.17157C20.2932 9.17157 20.2841 9.17155 20.2749 9.17148C17.7458 9.1565 15.7077 7.09128 15.7227 4.55868C15.7376 2.03535 17.7851 0 20.3013 0C20.3107 0 20.3196 4.36376e-05 20.329 8.73232e-05C22.8581 0.0150715 24.896 2.08029 24.8812 4.61289C24.8664 7.13624 22.8189 9.17157 20.3024 9.17157Z" fill="#000091"/>
|
||||
<path d="M20.3304 15.5561C20.302 15.5561 20.2741 15.5561 20.2457 15.5559C18.7099 15.5467 17.1755 15.2905 15.7082 14.7877C15.7175 12.6241 15.2333 10.4269 14.1994 8.36499C13.8453 7.65864 13.2089 7.18804 12.4961 7.02148C12.996 7.11156 13.4738 7.3514 13.858 7.74064C15.6347 9.54114 17.9821 10.4433 20.3299 10.4433C22.642 10.4433 24.9547 9.56899 26.7254 7.81682C27.2232 7.32436 27.8721 7.07852 28.5206 7.07852C29.1793 7.07852 29.8376 7.33204 30.3371 7.83827C30.774 8.28092 31.0165 8.84278 31.0654 9.41786C30.9563 8.62108 30.4749 7.88805 29.7074 7.49067C29.3324 7.29651 28.9315 7.20446 28.5365 7.20446C27.6119 7.20446 26.7196 7.70883 26.2656 8.58803C25.2261 10.6017 24.7152 12.751 24.6821 14.8768C23.2695 15.3296 21.7996 15.5561 20.3304 15.5561Z" fill="#000091"/>
|
||||
<path d="M24.6797 14.8774C24.7128 12.7517 25.2237 10.6024 26.2632 8.58864C26.7172 7.70945 27.6095 7.20508 28.5341 7.20508C28.9291 7.20508 29.33 7.29712 29.705 7.49128C30.4725 7.88867 30.9539 8.62169 31.063 9.41847C31.1249 10.1488 30.8748 10.9004 30.3133 11.4558C28.6746 13.0776 26.7374 14.2179 24.6797 14.8774Z" fill="#000091"/>
|
||||
<path d="M15.713 14.7878C13.7042 14.0995 11.8208 12.9487 10.2296 11.3363C9.23809 10.3317 9.24766 8.71228 10.251 7.71937C10.7487 7.22682 11.3976 6.98096 12.0461 6.98096C12.1983 6.98096 12.3507 6.99452 12.5009 7.02158C13.2137 7.18814 13.8501 7.65874 14.2042 8.36509C15.2381 10.427 15.7223 12.6242 15.713 14.7878Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -36,7 +36,7 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
`;
|
||||
|
||||
interface DropButtonProps {
|
||||
export interface DropButtonProps {
|
||||
button: ReactNode;
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
@@ -77,7 +77,7 @@ export const DropButton = ({
|
||||
<DialogTrigger onOpenChange={onOpenChangeHandler} isOpen={isLocalOpen}>
|
||||
<StyledButton>{button}</StyledButton>
|
||||
<StyledPopover
|
||||
style={{ opacity: opacity ? 1 : 0 }}
|
||||
style={{ padding: 0, opacity: opacity ? 1 : 0 }}
|
||||
isOpen={isLocalOpen}
|
||||
onOpenChange={onOpenChangeHandler}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,6 @@ export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
|
||||
<Text
|
||||
aria-label={props['aria-label']}
|
||||
className="material-icons"
|
||||
$theme="primary"
|
||||
$css={`
|
||||
transition: all 0.3s ease-in-out;
|
||||
transform: rotate(${isOpen ? '90' : '0'}deg);
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface LinkProps {
|
||||
|
||||
export const StyledLink = styled(Link)<LinkProps>`
|
||||
text-decoration: none;
|
||||
color: #ffffff33;
|
||||
color: black;
|
||||
&[aria-current='page'] {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
|
||||
export type BasicAvatarProps = {
|
||||
letter?: string;
|
||||
size?: 'small' | 'large';
|
||||
};
|
||||
export const BasicAvatar = ({ size = 'small', letter }: BasicAvatarProps) => {
|
||||
const width = useMemo(() => {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return '20px';
|
||||
case 'large':
|
||||
return '75px';
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$css="border-radius: 50%"
|
||||
className="bg-grey-400"
|
||||
$display="flex"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$width={width}
|
||||
$height={width}
|
||||
>
|
||||
<Box
|
||||
$css="border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.75);"
|
||||
$display="flex"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$width={width}
|
||||
$height={width}
|
||||
$padding={{ bottom: '2px' }}
|
||||
>
|
||||
<Text as="span" $color="white" $size={size === 'small' ? 't' : 'h2'}>
|
||||
{letter ?? ''}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { BasicAvatar, BasicAvatarProps } from '@/components/avatar/BasicAvatar';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
type Props = Omit<BasicAvatarProps, 'letter'> & {
|
||||
contact?: Contact;
|
||||
};
|
||||
|
||||
export const ContactAvatar = ({ contact, ...props }: Props) => {
|
||||
return <BasicAvatar {...props} letter={contact?.firstName.charAt(0)} />;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { Size } from '@/types/utils';
|
||||
|
||||
import style from './avatar.module.scss';
|
||||
|
||||
type Props = {
|
||||
size?: Size;
|
||||
};
|
||||
export const TeamAvatar = ({ size = Size.SMALL }: Props) => {
|
||||
return (
|
||||
<div className={`${style.teamAvatar} ${style[size]}`}>
|
||||
<Icon icon="group" className={`${style.teamIcon} ${style[size]}`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
.teamAvatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(179deg, #929292 -10.75%, #CECECE 126.68%);
|
||||
|
||||
.teamIcon {
|
||||
color: #FFFFFFBF;
|
||||
&.small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
border-radius: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #FFFFFFBF;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
border-radius: 18px;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
border: 4px solid #FFFFFFBF;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Button, ButtonProps } from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { SimpleLoader } from '@/components/loader/SimpleLoader';
|
||||
|
||||
type Props = ButtonProps & {
|
||||
loading?: boolean;
|
||||
};
|
||||
export const LoadingButton = ({ loading, ...props }: Props) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
disabled={loading ? true : props.disabled}
|
||||
icon={loading ? <SimpleLoader size="small" /> : props.icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import style from './styles.module.scss';
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
};
|
||||
export const UnstyledButton = ({
|
||||
children,
|
||||
className = '',
|
||||
onClick,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<button
|
||||
className={`${style.unstyledButton} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
.unstyledButton {
|
||||
width: 100%;
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
+10
-2
@@ -2,17 +2,25 @@ import { PropsWithChildren } from 'react';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { Box, BoxType } from '.';
|
||||
import { Box, BoxType } from '../index';
|
||||
|
||||
import style from './card.module.scss';
|
||||
|
||||
export type CardProps = BoxType & {
|
||||
size?: 'small' | 'medium' | 'large' | 'full';
|
||||
};
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
$css,
|
||||
size = 'medium',
|
||||
...props
|
||||
}: PropsWithChildren<BoxType>) => {
|
||||
}: PropsWithChildren<CardProps>) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={`${style.card} ${style[size]}`}
|
||||
$background="white"
|
||||
$radius="4px"
|
||||
$css={`
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { LoadingButton } from '@/components/button/LoadingButton';
|
||||
import { CardHeaderSection } from '@/components/cards/CardHeaderSection';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
sticky?: boolean;
|
||||
formId?: string;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
export const CardHeaderFormSection = ({
|
||||
title,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isLoading = false,
|
||||
formId,
|
||||
sticky = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CardHeaderSection
|
||||
sticky={sticky}
|
||||
title={title}
|
||||
left={
|
||||
<Button type="button" size="small" onClick={onCancel} color="secondary">
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
right={
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
size="small"
|
||||
form={formId}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{t('Validate')}
|
||||
</LoadingButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { HorizontalSeparator } from '@/components/separator/HorizontalSeparator';
|
||||
|
||||
import style from './card.module.scss';
|
||||
|
||||
type Props = {
|
||||
left?: ReactNode;
|
||||
title: string;
|
||||
right?: ReactNode;
|
||||
sticky?: boolean;
|
||||
};
|
||||
export const CardHeaderSection = ({
|
||||
left,
|
||||
title,
|
||||
right,
|
||||
sticky = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(style.cardSectionHeader, {
|
||||
[style.sticky]: sticky,
|
||||
})}
|
||||
>
|
||||
<div className={`flex justify-between align-items pl-b pr-b `}>
|
||||
{left}
|
||||
<p className="fw-bold fs-h3">{title}</p>
|
||||
{right}
|
||||
</div>
|
||||
<HorizontalSeparator />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
SeparatorVariant,
|
||||
} from '@/components/separator/HorizontalSeparator';
|
||||
|
||||
type Props = {
|
||||
showSeparator?: boolean;
|
||||
classNames?: string;
|
||||
separatorVariant?: SeparatorVariant;
|
||||
};
|
||||
export const CardSection = ({
|
||||
showSeparator = true,
|
||||
classNames = '',
|
||||
children,
|
||||
separatorVariant,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={`gap-t p-b ${classNames}`}>{children}</div>
|
||||
{showSeparator && <HorizontalSeparator variant={separatorVariant} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.medium {
|
||||
max-width: 600px;
|
||||
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.medium {
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
.cardSectionHeader {
|
||||
&.sticky {
|
||||
position: sticky;
|
||||
background-color: white;
|
||||
top: -30px;
|
||||
z-index: 9;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { DropButton, DropButtonProps } from '@/components';
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
icon?: string;
|
||||
label: string;
|
||||
callback?: () => void | Promise<unknown>;
|
||||
danger?: boolean;
|
||||
};
|
||||
|
||||
export type DropdownMenuProps = Omit<DropButtonProps, 'button'> & {
|
||||
options: DropdownMenuOption[];
|
||||
showArrow?: boolean;
|
||||
arrowClassname?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenu = ({
|
||||
options,
|
||||
children,
|
||||
showArrow = false,
|
||||
arrowClassname,
|
||||
...dropButtonProps
|
||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||
const getButton = () => {
|
||||
if (!showArrow) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div>{children}</div>
|
||||
<Icon
|
||||
className={arrowClassname ?? 'clr-primary-500'}
|
||||
icon={dropButtonProps.isOpen ? 'arrow_drop_up' : 'arrow_drop_down'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropButton {...dropButtonProps} button={getButton()}>
|
||||
<div className="flex flex-column ">
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
size="medium"
|
||||
key={option.label}
|
||||
onClick={() => {
|
||||
dropButtonProps.onOpenChange?.(false);
|
||||
void option.callback?.();
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={
|
||||
option.icon ? (
|
||||
<span className="material-icons" aria-hidden="true">
|
||||
{option.icon}
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DropButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.simpleContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--theme--spacings--st);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useDropdownMenu = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onOpenChange = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Input, InputProps } from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
type Props = Omit<InputProps, 'name'> & {
|
||||
name: string;
|
||||
};
|
||||
export const RHFInput = ({ name, ...props }: Props) => {
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Input
|
||||
{...field}
|
||||
name={name}
|
||||
state={error ? 'error' : props.state}
|
||||
text={error ? error.message : props.text}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { ConfirmationModal, useModal } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { MutableRefObject, useEffect, useState } from 'react';
|
||||
import { FieldValues, FormProvider, UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { LoadingButton } from '@/components/button/LoadingButton';
|
||||
|
||||
export interface RHFProviderProps<T extends FieldValues> {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
methods: UseFormReturn<T>;
|
||||
actionButtons?: React.ReactNode;
|
||||
onSubmit?: VoidFunction;
|
||||
formRef?: MutableRefObject<HTMLFormElement | undefined>;
|
||||
isSubmitting?: boolean;
|
||||
showSubmit?: boolean;
|
||||
checkBeforeUnload?: boolean;
|
||||
}
|
||||
|
||||
export function RHFProvider<T extends FieldValues>({
|
||||
children,
|
||||
onSubmit,
|
||||
methods,
|
||||
actionButtons,
|
||||
showSubmit = true,
|
||||
isSubmitting = false,
|
||||
checkBeforeUnload = false,
|
||||
id,
|
||||
}: RHFProviderProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [nextUrl, setNextUrl] = useState<string>();
|
||||
const confirmModal = useModal();
|
||||
useEffect(() => {
|
||||
const beforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (!checkBeforeUnload || !methods.formState.isDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', beforeUnload);
|
||||
};
|
||||
}, [methods.formState.isDirty]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
id={id}
|
||||
onSubmit={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onSubmit?.();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{(showSubmit || actionButtons) && (
|
||||
<Box className="flex justify-end mt-b" $gap={5}>
|
||||
{actionButtons}
|
||||
{showSubmit && (
|
||||
<LoadingButton
|
||||
data-testid={id ? `submit-button-${id}` : 'submit-button'}
|
||||
loading={isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{t('Submit')}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</form>
|
||||
<ConfirmationModal
|
||||
{...confirmModal}
|
||||
data-testid="unsaved-form-modal"
|
||||
onClose={confirmModal.close}
|
||||
onDecide={(decision) => {
|
||||
if (decision === 'yes' && nextUrl) {
|
||||
router.push(nextUrl);
|
||||
} else {
|
||||
setNextUrl(undefined);
|
||||
confirmModal.close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('form.dirty.modal')}
|
||||
</ConfirmationModal>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TextArea, TextAreaProps } from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
type Props = Omit<TextAreaProps, 'name'> & {
|
||||
name: string;
|
||||
};
|
||||
export const RHFTextArea = ({ name, ...props }: Props) => {
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<TextArea
|
||||
{...field}
|
||||
name={name}
|
||||
state={error ? 'error' : props.state}
|
||||
text={error ? error.message : props.text}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import style from './simple-view-field.module.scss';
|
||||
|
||||
type Props = {
|
||||
label: string | ReactNode;
|
||||
right?: ReactNode;
|
||||
};
|
||||
export const SimpleViewField = ({
|
||||
label,
|
||||
right,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<div className={style.left}>
|
||||
{typeof label === 'string' && (
|
||||
<span className="fw-bold fs-t clr-greyscale-500">{label}</span>
|
||||
)}
|
||||
{typeof label !== 'string' && label}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
{right && <div className={style.right}>{right}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
type Props = {
|
||||
icon: string;
|
||||
className?: string;
|
||||
};
|
||||
export const Icon = ({ icon, className }: Props) => {
|
||||
return <span className={`material-icons ${className}`}>{icon}</span>;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './Box';
|
||||
export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './cards/Card';
|
||||
export * from './DropButton';
|
||||
export * from './IconOptions';
|
||||
export * from './Link';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
|
||||
import style from './search-input.module.scss';
|
||||
|
||||
type Props = {
|
||||
onChange?: (str: string) => void;
|
||||
};
|
||||
export const SearchInput = (props: Props) => {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<Icon icon="search" className={style.icon} />
|
||||
<input
|
||||
onChange={(event) => props.onChange?.(event.target.value)}
|
||||
placeholder="Rechercher"
|
||||
className={style.input}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
border-radius: 4px;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
padding: 7px 10px;
|
||||
gap: var(--c--theme--spacings--t);
|
||||
height: 32px;
|
||||
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: none;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { UnstyledButton } from '@/components/button/UnstyledButton';
|
||||
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
|
||||
|
||||
export const FocusOnContent = ({ children }: PropsWithChildren) => {
|
||||
const responsive = useResponsiveLayout();
|
||||
return (
|
||||
<UnstyledButton onClick={responsive.focusOnContent}>
|
||||
{children}
|
||||
</UnstyledButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export const FocusOnLeft = ({ children }: PropsWithChildren) => {
|
||||
const responsive = useResponsiveLayout();
|
||||
return (
|
||||
<button
|
||||
onClick={responsive.focusOnLeft}
|
||||
className={`${styles.focusButton} ${styles.focusOnLeft}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import classNames from 'classnames';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren, ReactNode, useEffect } from 'react';
|
||||
|
||||
import { Breakpoints, useBreakpoint } from '@/hooks/useBreakpoints';
|
||||
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
|
||||
|
||||
import style from './styles.module.scss';
|
||||
|
||||
type Props = {
|
||||
leftContent: ReactNode;
|
||||
};
|
||||
|
||||
export function LeftRightContent({
|
||||
children,
|
||||
leftContent,
|
||||
}: PropsWithChildren<Props>) {
|
||||
const pathname = usePathname();
|
||||
const store = useResponsiveLayout();
|
||||
const isMobile = useBreakpoint(Breakpoints.LG, false);
|
||||
useEffect(() => {
|
||||
const paths = ['/contacts/', '/teams/'];
|
||||
if (paths.includes(pathname)) {
|
||||
store.focusOnLeft();
|
||||
}
|
||||
}, [pathname, isMobile]);
|
||||
|
||||
return (
|
||||
<div className={`${style.leftRightContent}`}>
|
||||
<div
|
||||
className={classNames(style.left, {
|
||||
[style.active]: !store.isFocusOnContent,
|
||||
})}
|
||||
>
|
||||
{leftContent}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(style.right, {
|
||||
[style.active]: store.isFocusOnContent,
|
||||
})}
|
||||
>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { usePathname } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import {
|
||||
HEADER_RESPONSIVE_HEIGHT,
|
||||
ResponsiveLayoutHeader,
|
||||
} from '@/components/layouts/responsive/header/ResponsiveLayoutHeader';
|
||||
import { ResponsiveLayoutMenuMobile } from '@/components/layouts/responsive/menu/ResponsiveLayoutMenuMobile';
|
||||
|
||||
import styles from './responsive-layout.module.scss';
|
||||
|
||||
type Props = {
|
||||
leftContent?: ReactNode;
|
||||
};
|
||||
|
||||
export const ResponsiveLayout = ({ children }: PropsWithChildren) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div className={styles.responsiveLayoutContainer}>
|
||||
<ResponsiveLayoutHeader toggleMenu={() => setIsOpen(!isOpen)} />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
<ResponsiveLayoutMenuMobile isOpen={isOpen} />
|
||||
<Box
|
||||
as="main"
|
||||
$height={`calc(100dvh - ${HEADER_RESPONSIVE_HEIGHT})`}
|
||||
$width="100%"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import HomeIcon from '@/assets/icons/icon-application.svg?url';
|
||||
import { Box, StyledLink } from '@/components';
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { AccountDropdown } from '@/features/header/AccountDropdown';
|
||||
import { LaGaufre } from '@/features/header/LaGaufre';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export const HEADER_RESPONSIVE_HEIGHT = '60px';
|
||||
|
||||
type Props = {
|
||||
toggleMenu: () => void;
|
||||
};
|
||||
export const ResponsiveLayoutHeader = ({ toggleMenu }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.responsiveLayoutHeader}>
|
||||
<Box $align="center" $gap="6rem" $direction="row">
|
||||
<div className="flex">
|
||||
<div className="showForMobile">
|
||||
<Button
|
||||
onClick={toggleMenu}
|
||||
color="primary-text"
|
||||
className="mr-st"
|
||||
icon={<Icon icon="menu" className="clr-primary-500" />}
|
||||
/>
|
||||
</div>
|
||||
<StyledLink href="/src/frontend/apps/desk/public">
|
||||
<Box $align="center" $gap="1rem" $direction="row">
|
||||
<Image height={30} priority src={HomeIcon} alt="" />
|
||||
<span className="fs-h3 fw-bold clr-primary-500">
|
||||
{t('La Régie')}
|
||||
</span>
|
||||
</Box>
|
||||
</StyledLink>
|
||||
</div>
|
||||
</Box>
|
||||
<Box $align="center" $gap="1rem" $direction="row">
|
||||
<div className="hideForMobile">
|
||||
<AccountDropdown />
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
.responsiveLayoutContainer {
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.responsiveLayoutHeader {
|
||||
height: var(--header-height);
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 1.125rem;
|
||||
border-bottom: 1px solid var(--c--theme--colors--greyscale-300);
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.responsiveLayoutHeader {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import { CardSection } from '@/components/cards/CardSection';
|
||||
import { AllMenuItems } from '@/components/layouts/responsive/menu/items/AllMenuItems';
|
||||
import { SeparatorVariant } from '@/components/separator/HorizontalSeparator';
|
||||
import { AccountDropdown } from '@/features/header/AccountDropdown';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
export const ResponsiveLayoutMenuMobile = ({ isOpen = false }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<menu className={`bg-primary-500 hideForMobile ${styles.sideMenu}`}>
|
||||
<div className={styles.sidebarContent}>
|
||||
<AllMenuItems />
|
||||
</div>
|
||||
</menu>
|
||||
<div
|
||||
className={classNames(styles.sideMenuResponsive, {
|
||||
[styles.sideMenuResponsiveActive]: isOpen,
|
||||
})}
|
||||
>
|
||||
<div className={`${styles.sideMenuResponsiveContent} gap-t`}>
|
||||
<CardSection separatorVariant={SeparatorVariant.DARK}>
|
||||
<AllMenuItems showLabel={true} />
|
||||
</CardSection>
|
||||
<CardSection showSeparator={false}>
|
||||
<div className={styles.sideMenuResponsiveSubContent}>
|
||||
<AccountDropdown />
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
</CardSection>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { MenuItem } from '@/components/layouts/responsive/menu/items/MenuItems';
|
||||
|
||||
type Props = {
|
||||
showLabel?: boolean;
|
||||
};
|
||||
|
||||
export const AllMenuItems = ({ showLabel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const menuItems = [
|
||||
{ route: '/contacts', label: t('Contacts'), icon: 'account_circle' },
|
||||
{ route: '/teams', label: t('Teams'), icon: 'groups' },
|
||||
{ route: '/mail-domains', label: t('Mail Domains'), icon: 'domain' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.route}
|
||||
icon={<Icon icon={item.icon} className="clr-primary-500" />}
|
||||
label={item.label}
|
||||
showLabelString={showLabel}
|
||||
route={item.route}
|
||||
activePaths={[item.route]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Tooltip } from '@openfun/cunningham-react';
|
||||
import classNames from 'classnames';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { StyledLink } from '@/components';
|
||||
|
||||
import styles from './menu-item.module.scss';
|
||||
|
||||
export type MenuItemProps = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
route: string;
|
||||
activePaths?: string[];
|
||||
showLabelString?: boolean;
|
||||
};
|
||||
|
||||
export const MenuItem = ({
|
||||
icon,
|
||||
label,
|
||||
route,
|
||||
showLabelString = false,
|
||||
activePaths = [],
|
||||
}: MenuItemProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive =
|
||||
pathname === route ||
|
||||
activePaths?.includes(pathname) ||
|
||||
pathname.startsWith(`${route}/`) ||
|
||||
activePaths?.some((a) => pathname.startsWith(`${a}/`));
|
||||
return (
|
||||
<StyledLink href={route} className="flex align-items ">
|
||||
<div
|
||||
className={classNames(`${styles.menuItemContainer}`, {
|
||||
[styles.active]: isActive,
|
||||
})}
|
||||
>
|
||||
<Tooltip content={label} placement="right">
|
||||
<div
|
||||
className={classNames(`${styles.menuItemContent}`, {
|
||||
[styles.menuItemContentActive]: isActive,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{showLabelString && (
|
||||
<span
|
||||
className={classNames(`${styles.label}`, {
|
||||
[styles.menuItemContentActive]: isActive,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
.menuItemContainer {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 1.03619rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:not(.active) {
|
||||
border: 2px solid transparent !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 2px solid white;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 0.75988rem;
|
||||
|
||||
&:not(.menuItemContentActive) {
|
||||
background-color: #ECECFE80
|
||||
|
||||
}
|
||||
|
||||
&.menuItemContentActive {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: var(--c--theme--spacings--t);
|
||||
|
||||
&:not(.menuItemContentActive) {
|
||||
color: #ECECFE80
|
||||
|
||||
}
|
||||
|
||||
&.menuItemContentActive {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
@media (min-width: 993px) {
|
||||
.sideMenuResponsive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.sideMenu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sideMenu {
|
||||
height: calc(100dvh - var(--header-height));
|
||||
padding: 5px;
|
||||
width: 70px;
|
||||
margin: 0;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.sidebarContent {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
@container {
|
||||
|
||||
}
|
||||
.sideMenuResponsive {
|
||||
z-index: 999;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
left: -100vw;
|
||||
height: 100%;
|
||||
background-color: var(--c--theme--colors--primary-500);
|
||||
transition: 0.3s;
|
||||
width: 100%;
|
||||
|
||||
&.sideMenuResponsiveActive {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sideMenuResponsiveContent {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.sideMenuResponsiveSubContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--c--theme--spacings--b);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
.responsiveLayoutContainer {
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.responsiveLayoutHeader {
|
||||
height: var(--header-height);
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 var(--c--theme--spacings--t);
|
||||
border-bottom: 1px solid var(--c--theme--colors--greyscale-300);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
.focusButton {
|
||||
width: 100%;
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.focusOnLeft {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.focusOnLeft {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.leftRightContent {
|
||||
|
||||
.left, .right {
|
||||
position: fixed;
|
||||
height: calc(100dvh - var(--header-height));
|
||||
width: 100vw;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: -100vw;
|
||||
overflow-y: auto !important;
|
||||
|
||||
&.active {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
left: -100vw;
|
||||
&.active {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 993px) {
|
||||
.leftRightContent {
|
||||
display: flex;
|
||||
height: inherit;
|
||||
flex:1;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: var(--c--theme--colors--primary-bg);
|
||||
padding: var(--c--theme--spacings--b);
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
border-right: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import style from './simple-loader.module.scss';
|
||||
|
||||
type Props = {
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
};
|
||||
export const SimpleLoader = ({ size = 'medium' }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(style.loading, {
|
||||
[style.small]: size === 'small',
|
||||
[style.medium]: size === 'medium',
|
||||
[style.large]: size === 'large',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
.loading {
|
||||
display: inline-block;
|
||||
|
||||
|
||||
border: 3px solid var(--c--theme--colors--primary-300);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--c--theme--colors--primary-500);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
-webkit-animation: spin 1s ease-in-out infinite;
|
||||
|
||||
&.small {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@-webkit-keyframes spin {
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import { BasicAvatar } from '@/components/avatar/BasicAvatar';
|
||||
|
||||
import style from './people.module.scss';
|
||||
|
||||
type Props = {
|
||||
fullName: string;
|
||||
isActive?: boolean;
|
||||
avatarStr?: string;
|
||||
};
|
||||
export const People = ({ fullName, isActive, avatarStr }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(style.container, {
|
||||
[style.active]: isActive,
|
||||
})}
|
||||
>
|
||||
<BasicAvatar letter={avatarStr ?? fullName.charAt(0)} />
|
||||
<div className="flex-center">
|
||||
<span className="fs-h6">{fullName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
color: var(--c--theme--colors--greyscale-1000);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Command } from 'cmdk';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { FocusOnContent } from '@/components/layouts/responsive/FocusOnContent';
|
||||
import { HorizontalSeparator } from '@/components/separator/HorizontalSeparator';
|
||||
|
||||
export type QuickSearchAction = {
|
||||
onSelect?: () => void;
|
||||
content: ReactNode;
|
||||
};
|
||||
|
||||
export type QuickSearchData<T> = {
|
||||
groupName: string;
|
||||
elements: T[];
|
||||
emptyString?: string;
|
||||
startActions?: QuickSearchAction[];
|
||||
endActions?: QuickSearchAction[];
|
||||
};
|
||||
|
||||
type Props<T> = {
|
||||
data: QuickSearchData<T>[];
|
||||
onFilter?: (str: string) => void;
|
||||
renderElement: (element: T) => ReactNode;
|
||||
onSelect?: (element: T) => void;
|
||||
inputContent?: ReactNode;
|
||||
};
|
||||
export function QuickSearch<T>({
|
||||
onSelect,
|
||||
onFilter,
|
||||
inputContent,
|
||||
data,
|
||||
renderElement,
|
||||
}: Props<T>) {
|
||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<div className="quick-search-container">
|
||||
<Command
|
||||
label="Recherche rapide de contact"
|
||||
shouldFilter={false}
|
||||
ref={ref}
|
||||
>
|
||||
{inputContent ?? (
|
||||
<div className="flex">
|
||||
<Command.Input
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||
autoFocus={true}
|
||||
placeholder="Rechercher"
|
||||
onValueChange={onFilter}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<HorizontalSeparator />
|
||||
<Command.List>
|
||||
<div>
|
||||
{data.map((group) => {
|
||||
return (
|
||||
<Command.Group
|
||||
key={group.groupName}
|
||||
heading={group.groupName}
|
||||
forceMount={true}
|
||||
>
|
||||
{group.startActions?.map((action, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={`${group.groupName}-action-${index}`}
|
||||
onSelect={action.onSelect}
|
||||
>
|
||||
<FocusOnContent>{action.content}</FocusOnContent>
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
{group.elements.map((groupElement, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={`${group.groupName}-element-${index}`}
|
||||
onSelect={() => onSelect?.(groupElement)}
|
||||
>
|
||||
{renderElement(groupElement)}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
{group.endActions?.map((action, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={`${group.groupName}-action-${index}`}
|
||||
onSelect={action.onSelect}
|
||||
>
|
||||
{action.content}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
{group.emptyString && group.elements.length === 0 && (
|
||||
<span className="ml-b clr-greyscale-500">
|
||||
{group.emptyString}
|
||||
</span>
|
||||
)}
|
||||
</Command.Group>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Command.List>
|
||||
</Command>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Item({
|
||||
children,
|
||||
shortcut,
|
||||
onSelect = () => {},
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
onSelect?: (value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Command.Item onSelect={onSelect}>
|
||||
{children}
|
||||
{shortcut && (
|
||||
<div cmdk-vercel-shortcuts="">
|
||||
{shortcut.split(' ').map((key) => {
|
||||
return <kbd key={key}>{key}</kbd>;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Command.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Button, ModalSize, useModal } from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Modal } from '@/components/Modal';
|
||||
import { UnstyledButton } from '@/components/button/UnstyledButton';
|
||||
import styles from '@/features/contacts/compontents/list/header/contact-list-header.module.scss';
|
||||
import { useCmdK } from '@/hooks/useCmdK';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
quickSearchComponent: (closeModal: () => void) => ReactNode;
|
||||
};
|
||||
export const ListSearchHeader = ({ title, quickSearchComponent }: Props) => {
|
||||
const modal = useModal();
|
||||
useCmdK(modal.open);
|
||||
return (
|
||||
<div className={`${styles.listHeader}`}>
|
||||
<div className="flex justify-between align-items">
|
||||
<p className="fs-h3 fw-bold">{title}</p>
|
||||
<Button
|
||||
onClick={modal.open}
|
||||
color="primary"
|
||||
size="nano"
|
||||
icon={<span className="material-icons">add</span>}
|
||||
/>
|
||||
</div>
|
||||
<UnstyledButton onClick={modal.open} className={styles.headerInput}>
|
||||
<div className="flex justify-between align-items ">
|
||||
<span className="material-icons fs-h4 clr-greyscale-700">search</span>
|
||||
<span className="fs-m ml-st clr-greyscale-700">Rechercher</span>
|
||||
</div>
|
||||
<div className={styles.kbd}># K</div>
|
||||
</UnstyledButton>
|
||||
|
||||
<Modal
|
||||
closeOnClickOutside={true}
|
||||
hideCloseButton={true}
|
||||
size={ModalSize.MEDIUM}
|
||||
{...modal}
|
||||
>
|
||||
{quickSearchComponent(modal.close)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
.listHeader {
|
||||
padding: var(--c--theme--spacings--st) var(--c--theme--spacings--s);
|
||||
padding-bottom: var(--c--theme--spacings--s) ;
|
||||
border-bottom: 1px solid var(--c--theme--colors--greyscale-100);
|
||||
|
||||
.headerInput {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
background-color: var(--c--theme--colors--greyscale-100);;
|
||||
|
||||
.kbd {
|
||||
padding: 3px 8px;
|
||||
|
||||
background-color: var(--c--theme--colors--greyscale-300);;
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
.quick-search-container {
|
||||
[cmdk-root] {
|
||||
|
||||
width: 100%;
|
||||
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
|
||||
transition: transform 100ms ease;
|
||||
outline: none;
|
||||
|
||||
.dark & {
|
||||
background: rgba(22, 22, 22, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-input] {
|
||||
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-size: 17px;
|
||||
padding: 16px;
|
||||
outline: none;
|
||||
background: white;
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
|
||||
|
||||
border-radius: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--c--theme--colors--greyscale-300);
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-vercel-badge] {
|
||||
height: 20px;
|
||||
background: var(--c--theme--colors--greyscale-700);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
border-radius: 4px;
|
||||
margin: 4px 0 4px 4px;
|
||||
user-select: none;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[cmdk-item] {
|
||||
content-visibility: auto;
|
||||
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
|
||||
user-select: none;
|
||||
will-change: background, color;
|
||||
transition: all 150ms ease;
|
||||
transition-property: none;
|
||||
|
||||
&[data-selected='true'] {
|
||||
background: var(--c--theme--colors--primary-200);
|
||||
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transition-property: background;
|
||||
background: var(--c--theme--colors--greyscale-500);
|
||||
}
|
||||
|
||||
& + [cmdk-item] {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-list] {
|
||||
height: 500px;
|
||||
padding: 10px;
|
||||
max-height: 700px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
transition: 100ms ease;
|
||||
transition-property: height;
|
||||
}
|
||||
|
||||
[cmdk-vercel-shortcuts] {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
gap: 8px;
|
||||
|
||||
kbd {
|
||||
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
padding: 4px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
background: var(--c--theme--colors--greyscale-500);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-separator] {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--c--theme--colors--greyscale-500);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
*:not([hidden]) + [cmdk-group] {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[cmdk-group-heading] {
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
;
|
||||
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
font-weight: bold;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
[cmdk-empty] {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.c__modal__scroller:has(.quick-search-container) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import style from './separator.module.scss';
|
||||
|
||||
export enum SeparatorVariant {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant?: SeparatorVariant;
|
||||
};
|
||||
|
||||
export const HorizontalSeparator = ({
|
||||
variant = SeparatorVariant.LIGHT,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(style.horizontal, {
|
||||
[style.dark]: variant === SeparatorVariant.DARK,
|
||||
[style.light]: variant === SeparatorVariant.LIGHT,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
.horizontal {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
||||
|
||||
&.light {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: #E5E5E533
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { Footer } from '@/features/footer/Footer';
|
||||
import { HEADER_HEIGHT, Header } from '@/features/header';
|
||||
import { Menu } from '@/features/menu';
|
||||
|
||||
@@ -11,21 +10,18 @@ export function MainLayout({ children }: PropsWithChildren) {
|
||||
const { config } = useConfigStore();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box $height="100vh">
|
||||
<Header />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
{config?.FEATURES.TEAMS && <Menu />}
|
||||
<Box
|
||||
as="main"
|
||||
$height={`calc(100vh - ${HEADER_HEIGHT})`}
|
||||
$width="100%"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<Box $height="100dvh">
|
||||
<Header />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
{config?.FEATURES.TEAMS && <Menu />}
|
||||
<Box
|
||||
as="main"
|
||||
$height={`calc(100dvh - ${HEADER_HEIGHT})`}
|
||||
$width="100%"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
<Footer />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { Footer } from '@/features/footer/Footer';
|
||||
import { Header } from '@/features/header';
|
||||
|
||||
export function PageLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<Box>
|
||||
<Header />
|
||||
<Box as="main" $width="100%">
|
||||
{children}
|
||||
</Box>
|
||||
<Footer />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Box $height="100dvh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Box $height="100dvh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './AppProvider';
|
||||
export * from './MainLayout';
|
||||
export * from './PageLayout';
|
||||
export * from './config';
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
: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
|
||||
--c--components--forms-datepicker--border-color
|
||||
);
|
||||
|
||||
/**
|
||||
* Select
|
||||
**/
|
||||
--c--components--forms-select--value-color--disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,9 @@
|
||||
@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');
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
.c__button:focus-visible,
|
||||
.c__select__wrapper:focus-visible,
|
||||
.c__datagrid__header:focus-visible {
|
||||
outline: var(--c--theme--colors--primary-600) solid 2px;
|
||||
border-radius: var(--c--components--button--border-radius--focus);
|
||||
}
|
||||
@import './cunningham-tokens';
|
||||
@import './cunningham-custom-tokens';
|
||||
@import '../assets/fonts/Marianne/Marianne-font';
|
||||
|
||||
.c__input,
|
||||
.c__field,
|
||||
@@ -21,223 +12,20 @@ button:focus-visible,
|
||||
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.c__input__wrapper:focus-within {
|
||||
border-color: var(
|
||||
--c--components--forms-input--border--color-error-hover
|
||||
) !important;
|
||||
}
|
||||
|
||||
.c__input__wrapper--error.c__input__wrapper:focus-within label {
|
||||
color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
|
||||
.c__input__wrapper--error .labelled-box label.placeholder {
|
||||
color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
|
||||
.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;
|
||||
/*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 {
|
||||
@@ -246,17 +34,12 @@ input:-webkit-autofill:focus {
|
||||
|
||||
.c__datagrid__table__container > table tbody tr:hover {
|
||||
background-color: var(
|
||||
--c--components--datagrid--body--background-color-hover
|
||||
--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;
|
||||
padding-inline: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -268,7 +51,7 @@ input:-webkit-autofill:focus {
|
||||
|
||||
.c__pagination__list .c__button--tertiary-text.c__button--active {
|
||||
background-color: var(
|
||||
--c--components--datagrid--pagination--background-color-active
|
||||
--c--components--datagrid--pagination--background-color-active
|
||||
);
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
}
|
||||
@@ -294,7 +77,7 @@ input:-webkit-autofill:focus {
|
||||
|
||||
.c__date-picker__wrapper {
|
||||
transition: all var(--c--theme--transitions--duration)
|
||||
var(--c--theme--transitions--ease-out);
|
||||
var(--c--theme--transitions--ease-out);
|
||||
}
|
||||
|
||||
.c__date-picker:not(.c__date-picker--disabled):hover .c__date-picker__wrapper {
|
||||
@@ -302,7 +85,7 @@ input:-webkit-autofill:focus {
|
||||
}
|
||||
|
||||
.c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover
|
||||
.c__date-picker__wrapper {
|
||||
.c__date-picker__wrapper {
|
||||
box-shadow: var(--c--theme--colors--danger-300) 0 0 0 2px;
|
||||
}
|
||||
|
||||
@@ -323,10 +106,6 @@ input:-webkit-autofill:focus {
|
||||
transition: all 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.c__radio input:focus-visible {
|
||||
outline: var(--c--theme--colors--primary-600) solid 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button
|
||||
*/
|
||||
@@ -350,18 +129,17 @@ input:-webkit-autofill:focus {
|
||||
|
||||
.c__button--with-icon--right {
|
||||
padding: 0.7rem var(--c--theme--spacings--t) 0.7rem
|
||||
var(--c--theme--spacings--s);
|
||||
var(--c--theme--spacings--s);
|
||||
}
|
||||
|
||||
.c__button--primary,
|
||||
.c__button--primary:focus-visible {
|
||||
.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
|
||||
--c--components--button--primary--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--primary--color-hover);
|
||||
}
|
||||
@@ -369,31 +147,37 @@ input:-webkit-autofill:focus {
|
||||
.c__button--primary:active,
|
||||
.c__button--primary.c__button--active {
|
||||
background-color: var(
|
||||
--c--components--button--primary--background--color-active
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
|
||||
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--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
|
||||
--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--primary-text
|
||||
{
|
||||
|
||||
color: var(--c--theme--colors--primary-500);
|
||||
}
|
||||
|
||||
.c__button--tertiary:disabled {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
.c__button--success {
|
||||
@@ -404,65 +188,51 @@ input:-webkit-autofill:focus {
|
||||
.c__button--success:hover,
|
||||
.c__button--success:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--success--background--color-hover
|
||||
--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
|
||||
--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);
|
||||
background-color: var(--c--theme--colors--greyscale-000);
|
||||
color: var(--c--theme--colors--primary-500);
|
||||
border: 1px solid var(--c--theme--colors--greyscale-250);
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--c--theme--colors--greyscale-000);
|
||||
border-color: var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
}
|
||||
|
||||
.c__button--secondary:hover,
|
||||
.c__button--secondary:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--secondary--background--color-hover
|
||||
--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--secondary:active,
|
||||
.c__button--secondary.c__button--active {
|
||||
|
||||
background-color: var(
|
||||
--c--components--button--secondary--background--color-active
|
||||
);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -470,14 +240,14 @@ input:-webkit-autofill:focus {
|
||||
.c__button--danger:hover,
|
||||
.c__button--danger:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--danger--background--color-hover
|
||||
--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
|
||||
--c--components--button--danger--background--color-disabled
|
||||
);
|
||||
}
|
||||
|
||||
@@ -492,3 +262,31 @@ input:-webkit-autofill:focus {
|
||||
.c__modal__close .c__button--tertiary-text:focus-visible {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c__modal__close
|
||||
.c__button {
|
||||
right: -0.5rem;
|
||||
top: -1rem;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast
|
||||
*/
|
||||
.c__toast__container {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert
|
||||
*/
|
||||
|
||||
.c__alert--success {
|
||||
border-color: var(--c--theme--colors--success-600);
|
||||
}
|
||||
.c__alert--info {
|
||||
border-color: var(--c--theme--colors--info-600);
|
||||
}
|
||||
.c__alert--error {
|
||||
border-color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,63 @@
|
||||
import { randAwsRequestId } from '@ngneat/falso';
|
||||
|
||||
import { DTOUpdateContact } from '@/features/contacts/api/types';
|
||||
import { ALL_CONTACTS, Contact } from '@/features/contacts/contact';
|
||||
|
||||
/**
|
||||
* All in this are mocked.
|
||||
*/
|
||||
export class ContactRepository {
|
||||
static DATA = [...ALL_CONTACTS];
|
||||
static async getAll(): Promise<Contact[]> {
|
||||
return new Promise((resolve) => resolve(ContactRepository.DATA));
|
||||
}
|
||||
|
||||
static async get(id: string): Promise<Contact | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const contacts = [...ContactRepository.DATA];
|
||||
resolve(contacts.find((contact) => contact.id === id));
|
||||
});
|
||||
}
|
||||
|
||||
static async update(
|
||||
id: string,
|
||||
payload: DTOUpdateContact,
|
||||
): Promise<Contact | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const contacts = [...ContactRepository.DATA];
|
||||
const toUpdate = contacts.findIndex((contact) => contact.id === id);
|
||||
if (toUpdate < 0) {
|
||||
reject(new Error());
|
||||
}
|
||||
|
||||
contacts[toUpdate] = { ...contacts[toUpdate], ...payload };
|
||||
ContactRepository.DATA = contacts;
|
||||
resolve(contacts[toUpdate]);
|
||||
});
|
||||
}
|
||||
|
||||
static async add(payload: DTOUpdateContact): Promise<Contact> {
|
||||
return new Promise((resolve) => {
|
||||
const contacts = [...ContactRepository.DATA];
|
||||
|
||||
const newContact: Contact = { id: randAwsRequestId(), ...payload };
|
||||
contacts.push(newContact);
|
||||
ContactRepository.DATA = contacts;
|
||||
resolve(newContact);
|
||||
});
|
||||
}
|
||||
|
||||
static async delete(id: string): Promise<Contact> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const contacts = [...ContactRepository.DATA];
|
||||
const toUpdate = contacts.findIndex((contact) => contact.id === id);
|
||||
if (toUpdate < 0) {
|
||||
reject(new Error());
|
||||
}
|
||||
|
||||
const contact = contacts.splice(toUpdate, 1);
|
||||
ContactRepository.DATA = contacts;
|
||||
resolve(contact[0]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
export type PublicContact = Omit<Contact, 'publicData'>;
|
||||
export type DTOUpdateContact = Omit<Contact, 'publicData' | 'id'>;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ContactRepository } from '@/features/contacts/api/contact-repository';
|
||||
import { DTOUpdateContact } from '@/features/contacts/api/types';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
export const QUERY_KEY_CONTACTS = 'contacts';
|
||||
|
||||
export const useContacts = () => {
|
||||
return useQuery<Contact[]>({
|
||||
queryKey: [QUERY_KEY_CONTACTS],
|
||||
queryFn: async () => ContactRepository.getAll(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useContact = (id: string) => {
|
||||
return useQuery<Contact | undefined>({
|
||||
queryKey: [QUERY_KEY_CONTACTS, id],
|
||||
queryFn: async () => ContactRepository.get(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateContact = () => {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload: DTOUpdateContact) => {
|
||||
return ContactRepository.add(payload);
|
||||
},
|
||||
onSuccess: async (contact) => {
|
||||
await client.setQueryData([QUERY_KEY_CONTACTS, contact.id], contact);
|
||||
client.setQueryData([QUERY_KEY_CONTACTS], (prev: Contact[]) => {
|
||||
const newResult = [...prev];
|
||||
newResult.push(contact);
|
||||
return newResult;
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateContact = (id?: string) => {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload: DTOUpdateContact) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
return ContactRepository.update(id, payload);
|
||||
},
|
||||
onSuccess: async (contact) => {
|
||||
if (contact) {
|
||||
await client.setQueryData([QUERY_KEY_CONTACTS, contact.id], contact);
|
||||
} else if (id != undefined) {
|
||||
await client.invalidateQueries({ queryKey: [QUERY_KEY_CONTACTS, id] });
|
||||
}
|
||||
await client.invalidateQueries({ queryKey: [QUERY_KEY_CONTACTS] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteContact = (id: string) => {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
return ContactRepository.delete(id);
|
||||
},
|
||||
onSuccess: async (contact) => {
|
||||
console.log('ICI');
|
||||
if (contact) {
|
||||
client.removeQueries({
|
||||
queryKey: [QUERY_KEY_CONTACTS, contact.id],
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
await client.invalidateQueries({ queryKey: [QUERY_KEY_CONTACTS] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
<svg width="104" height="80" viewBox="0 0 104 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1_5775)">
|
||||
<rect x="53" y="12.5" width="47" height="47" rx="23.5" fill="#E5E5E5"/>
|
||||
<rect x="53.7907" y="13.2907" width="45.4186" height="45.4186" rx="22.7093" stroke="white" stroke-opacity="0.75" stroke-width="1.58138"/>
|
||||
<path d="M75.8015 42.2823C75.2317 42.2823 74.8333 42.1966 74.6062 42.0252C74.3792 41.8584 74.2657 41.6152 74.2657 41.2955C74.2657 40.8461 74.4001 40.3759 74.6688 39.8848C74.9421 39.3937 75.3336 38.9327 75.8432 38.5018C76.3575 38.071 76.976 37.7212 77.6987 37.4525C78.4215 37.1838 79.2322 37.0494 80.131 37.0494C81.0344 37.0494 81.8475 37.1838 82.5703 37.4525C83.293 37.7212 83.9092 38.071 84.4188 38.5018C84.9331 38.9327 85.3269 39.3937 85.6002 39.8848C85.8736 40.3759 86.0102 40.8461 86.0102 41.2955C86.0102 41.6152 85.8944 41.8584 85.6628 42.0252C85.4357 42.1966 85.0373 42.2823 84.4675 42.2823H75.8015ZM80.138 35.8124C79.6283 35.8124 79.1604 35.6757 78.7342 35.4024C78.3126 35.129 77.9744 34.7607 77.7196 34.2974C77.4648 33.8341 77.3374 33.3153 77.3374 32.7408C77.3374 32.1755 77.4648 31.6659 77.7196 31.2119C77.979 30.7532 78.3219 30.3895 78.7481 30.1208C79.1743 29.8521 79.6376 29.7178 80.138 29.7178C80.643 29.7178 81.1063 29.8498 81.5279 30.1139C81.9541 30.378 82.2946 30.7393 82.5494 31.198C82.8089 31.652 82.9386 32.1616 82.9386 32.7269C82.9386 33.306 82.8089 33.8295 82.5494 34.2974C82.2946 34.7607 81.9541 35.129 81.5279 35.4024C81.1063 35.6757 80.643 35.8124 80.138 35.8124ZM68.2753 42.2823C67.8074 42.2823 67.4761 42.1873 67.2816 41.9974C67.087 41.8121 66.9897 41.548 66.9897 41.2051C66.9897 40.7326 67.1101 40.2531 67.3511 39.7666C67.592 39.2755 67.9348 38.8238 68.3796 38.4115C68.829 37.9992 69.3664 37.6656 69.9918 37.4108C70.6219 37.156 71.3261 37.0286 72.1045 37.0286C72.7345 37.0286 73.2974 37.1119 73.7932 37.2787C74.2935 37.4409 74.736 37.6447 75.1205 37.8903C74.7267 38.2007 74.3862 38.5528 74.0989 38.9466C73.8117 39.3358 73.5893 39.7365 73.4318 40.1488C73.2789 40.5565 73.2025 40.9457 73.2025 41.3163C73.2025 41.6916 73.2951 42.0136 73.4804 42.2823H68.2753ZM72.1045 35.9722C71.6643 35.9722 71.2589 35.8541 70.8883 35.6178C70.5177 35.3769 70.2212 35.0549 69.9988 34.6519C69.7764 34.2442 69.6652 33.7924 69.6652 33.2967C69.6652 32.801 69.7764 32.3562 69.9988 31.9624C70.2258 31.564 70.5246 31.249 70.8953 31.0173C71.2659 30.781 71.669 30.6629 72.1045 30.6629C72.5399 30.6629 72.943 30.7787 73.3136 31.0104C73.6889 31.2374 73.9877 31.5501 74.2101 31.9485C74.4325 32.3423 74.5437 32.7871 74.5437 33.2828C74.5437 33.7878 74.4325 34.2442 74.2101 34.6519C73.9877 35.0549 73.6912 35.3769 73.3206 35.6178C72.95 35.8541 72.5446 35.9722 72.1045 35.9722Z" fill="white" fill-opacity="0.75"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_1_5775)">
|
||||
<rect x="35.75" y="6.5" width="59" height="59" rx="29.5" fill="#CECECE"/>
|
||||
<rect x="36.7426" y="7.49257" width="57.0149" height="57.0149" rx="28.5074" stroke="white" stroke-opacity="0.75" stroke-width="1.98513"/>
|
||||
<path d="M64.3732 43.8863C63.6578 43.8863 63.1576 43.7787 62.8727 43.5635C62.5877 43.3541 62.4452 43.0488 62.4452 42.6475C62.4452 42.0834 62.6139 41.4931 62.9512 40.8766C63.2943 40.2601 63.7858 39.6814 64.4255 39.1406C65.071 38.5997 65.8475 38.1606 66.7547 37.8233C67.662 37.486 68.6798 37.3173 69.808 37.3173C70.9421 37.3173 71.9628 37.486 72.8701 37.8233C73.7773 38.1606 74.5508 38.5997 75.1906 39.1406C75.8361 39.6814 76.3305 40.2601 76.6736 40.8766C77.0167 41.4931 77.1883 42.0834 77.1883 42.6475C77.1883 43.0488 77.0429 43.3541 76.7521 43.5635C76.4671 43.7787 75.967 43.8863 75.2516 43.8863H64.3732ZM69.8168 35.7645C69.177 35.7645 68.5896 35.5929 68.0546 35.2498C67.5253 34.9066 67.1008 34.4443 66.7809 33.8627C66.461 33.2811 66.3011 32.6298 66.3011 31.9086C66.3011 31.1991 66.461 30.5593 66.7809 29.9894C67.1066 29.4136 67.537 28.9571 68.072 28.6197C68.6071 28.2824 69.1886 28.1138 69.8168 28.1138C70.4507 28.1138 71.0323 28.2795 71.5615 28.611C72.0966 28.9425 72.524 29.3962 72.8439 29.9719C73.1696 30.5419 73.3324 31.1816 73.3324 31.8911C73.3324 32.6181 73.1696 33.2753 72.8439 33.8627C72.524 34.4443 72.0966 34.9066 71.5615 35.2498C71.0323 35.5929 70.4507 35.7645 69.8168 35.7645ZM54.9254 43.8863C54.338 43.8863 53.9221 43.767 53.6779 43.5286C53.4336 43.296 53.3115 42.9645 53.3115 42.5341C53.3115 41.9409 53.4627 41.3389 53.7651 40.7283C54.0675 40.1118 54.4979 39.5448 55.0562 39.0272C55.6203 38.5095 56.295 38.0908 57.0801 37.7709C57.8711 37.4511 58.7551 37.2911 59.7321 37.2911C60.5231 37.2911 61.2297 37.3958 61.852 37.6052C62.4801 37.8087 63.0355 38.0646 63.5182 38.3729C63.0239 38.7625 62.5964 39.2045 62.2358 39.6989C61.8753 40.1874 61.5961 40.6905 61.3984 41.2081C61.2064 41.7199 61.1105 42.2084 61.1105 42.6737C61.1105 43.1448 61.2268 43.549 61.4594 43.8863H54.9254ZM59.7321 35.9651C59.1796 35.9651 58.6707 35.8168 58.2055 35.5202C57.7402 35.2178 57.368 34.8136 57.0888 34.3076C56.8097 33.7958 56.6701 33.2288 56.6701 32.6065C56.6701 31.9842 56.8097 31.4259 57.0888 30.9315C57.3738 30.4314 57.7489 30.0359 58.2142 29.7451C58.6795 29.4485 59.1854 29.3002 59.7321 29.3002C60.2788 29.3002 60.7848 29.4456 61.2501 29.7364C61.7211 30.0214 62.0963 30.4139 62.3754 30.9141C62.6546 31.4084 62.7942 31.9667 62.7942 32.589C62.7942 33.223 62.6546 33.7958 62.3754 34.3076C62.0963 34.8136 61.724 35.2178 61.2588 35.5202C60.7935 35.8168 60.2846 35.9651 59.7321 35.9651Z" fill="white" fill-opacity="0.75"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_1_5775)">
|
||||
<rect x="4" y="12.5" width="47" height="47" rx="23.5" fill="#E5E5E5"/>
|
||||
<rect x="4.79069" y="13.2907" width="45.4186" height="45.4186" rx="22.7093" stroke="white" stroke-opacity="0.75" stroke-width="1.58138"/>
|
||||
<path d="M26.8015 42.2823C26.2317 42.2823 25.8333 42.1966 25.6062 42.0252C25.3792 41.8584 25.2657 41.6152 25.2657 41.2955C25.2657 40.8461 25.4001 40.3759 25.6688 39.8848C25.9421 39.3937 26.3336 38.9327 26.8432 38.5018C27.3575 38.071 27.976 37.7212 28.6987 37.4525C29.4215 37.1838 30.2322 37.0494 31.131 37.0494C32.0344 37.0494 32.8475 37.1838 33.5703 37.4525C34.293 37.7212 34.9092 38.071 35.4188 38.5018C35.9331 38.9327 36.3269 39.3937 36.6002 39.8848C36.8736 40.3759 37.0102 40.8461 37.0102 41.2955C37.0102 41.6152 36.8944 41.8584 36.6628 42.0252C36.4357 42.1966 36.0373 42.2823 35.4675 42.2823H26.8015ZM31.138 35.8124C30.6283 35.8124 30.1604 35.6757 29.7342 35.4024C29.3126 35.129 28.9744 34.7607 28.7196 34.2974C28.4648 33.8341 28.3374 33.3153 28.3374 32.7408C28.3374 32.1755 28.4648 31.6659 28.7196 31.2119C28.979 30.7532 29.3219 30.3895 29.7481 30.1208C30.1743 29.8521 30.6376 29.7178 31.138 29.7178C31.643 29.7178 32.1063 29.8498 32.5279 30.1139C32.9541 30.378 33.2946 30.7393 33.5494 31.198C33.8089 31.652 33.9386 32.1616 33.9386 32.7269C33.9386 33.306 33.8089 33.8295 33.5494 34.2974C33.2946 34.7607 32.9541 35.129 32.5279 35.4024C32.1063 35.6757 31.643 35.8124 31.138 35.8124ZM19.2753 42.2823C18.8074 42.2823 18.4761 42.1873 18.2816 41.9974C18.087 41.8121 17.9897 41.548 17.9897 41.2051C17.9897 40.7326 18.1101 40.2531 18.3511 39.7666C18.592 39.2755 18.9348 38.8238 19.3796 38.4115C19.829 37.9992 20.3664 37.6656 20.9918 37.4108C21.6219 37.156 22.3261 37.0286 23.1045 37.0286C23.7345 37.0286 24.2974 37.1119 24.7932 37.2787C25.2935 37.4409 25.736 37.6447 26.1205 37.8903C25.7267 38.2007 25.3862 38.5528 25.0989 38.9466C24.8117 39.3358 24.5893 39.7365 24.4318 40.1488C24.2789 40.5565 24.2025 40.9457 24.2025 41.3163C24.2025 41.6916 24.2951 42.0136 24.4804 42.2823H19.2753ZM23.1045 35.9722C22.6643 35.9722 22.2589 35.8541 21.8883 35.6178C21.5177 35.3769 21.2212 35.0549 20.9988 34.6519C20.7764 34.2442 20.6652 33.7924 20.6652 33.2967C20.6652 32.801 20.7764 32.3562 20.9988 31.9624C21.2258 31.564 21.5246 31.249 21.8953 31.0173C22.2659 30.781 22.669 30.6629 23.1045 30.6629C23.5399 30.6629 23.943 30.7787 24.3136 31.0104C24.6889 31.2374 24.9877 31.5501 25.2101 31.9485C25.4325 32.3423 25.5437 32.7871 25.5437 33.2828C25.5437 33.7878 25.4325 34.2442 25.2101 34.6519C24.9877 35.0549 24.6912 35.3769 24.3206 35.6178C23.95 35.8541 23.5446 35.9722 23.1045 35.9722Z" fill="white" fill-opacity="0.75"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_d_1_5775)">
|
||||
<rect x="8.75" y="6.5" width="59" height="59" rx="29.5" fill="#CECECE"/>
|
||||
<rect x="9.74257" y="7.49257" width="57.0149" height="57.0149" rx="28.5074" stroke="white" stroke-opacity="0.75" stroke-width="1.98513"/>
|
||||
<path d="M37.3732 43.8863C36.6578 43.8863 36.1576 43.7787 35.8727 43.5635C35.5877 43.3541 35.4452 43.0488 35.4452 42.6475C35.4452 42.0834 35.6139 41.4931 35.9512 40.8766C36.2943 40.2601 36.7858 39.6814 37.4255 39.1406C38.071 38.5997 38.8475 38.1606 39.7547 37.8233C40.662 37.486 41.6798 37.3173 42.808 37.3173C43.9421 37.3173 44.9628 37.486 45.8701 37.8233C46.7773 38.1606 47.5508 38.5997 48.1906 39.1406C48.8361 39.6814 49.3305 40.2601 49.6736 40.8766C50.0167 41.4931 50.1883 42.0834 50.1883 42.6475C50.1883 43.0488 50.0429 43.3541 49.7521 43.5635C49.4671 43.7787 48.967 43.8863 48.2516 43.8863H37.3732ZM42.8168 35.7645C42.177 35.7645 41.5896 35.5929 41.0546 35.2498C40.5253 34.9066 40.1008 34.4443 39.7809 33.8627C39.461 33.2811 39.3011 32.6298 39.3011 31.9086C39.3011 31.1991 39.461 30.5593 39.7809 29.9894C40.1066 29.4136 40.537 28.9571 41.072 28.6197C41.6071 28.2824 42.1886 28.1138 42.8168 28.1138C43.4507 28.1138 44.0323 28.2795 44.5615 28.611C45.0966 28.9425 45.524 29.3962 45.8439 29.9719C46.1696 30.5419 46.3324 31.1816 46.3324 31.8911C46.3324 32.6181 46.1696 33.2753 45.8439 33.8627C45.524 34.4443 45.0966 34.9066 44.5615 35.2498C44.0323 35.5929 43.4507 35.7645 42.8168 35.7645ZM27.9254 43.8863C27.338 43.8863 26.9221 43.767 26.6779 43.5286C26.4336 43.296 26.3115 42.9645 26.3115 42.5341C26.3115 41.9409 26.4627 41.3389 26.7651 40.7283C27.0675 40.1118 27.4979 39.5448 28.0562 39.0272C28.6203 38.5095 29.295 38.0908 30.0801 37.7709C30.8711 37.4511 31.7551 37.2911 32.7321 37.2911C33.5231 37.2911 34.2297 37.3958 34.852 37.6052C35.4801 37.8087 36.0355 38.0646 36.5182 38.3729C36.0239 38.7625 35.5964 39.2045 35.2358 39.6989C34.8753 40.1874 34.5961 40.6905 34.3984 41.2081C34.2064 41.7199 34.1105 42.2084 34.1105 42.6737C34.1105 43.1448 34.2268 43.549 34.4594 43.8863H27.9254ZM32.7321 35.9651C32.1796 35.9651 31.6707 35.8168 31.2055 35.5202C30.7402 35.2178 30.368 34.8136 30.0888 34.3076C29.8097 33.7958 29.6701 33.2288 29.6701 32.6065C29.6701 31.9842 29.8097 31.4259 30.0888 30.9315C30.3738 30.4314 30.7489 30.0359 31.2142 29.7451C31.6795 29.4485 32.1854 29.3002 32.7321 29.3002C33.2788 29.3002 33.7848 29.4456 34.2501 29.7364C34.7211 30.0214 35.0963 30.4139 35.3754 30.9141C35.6546 31.4084 35.7942 31.9667 35.7942 32.589C35.7942 33.223 35.6546 33.7958 35.3754 34.3076C35.0963 34.8136 34.724 35.2178 34.2588 35.5202C33.7935 35.8168 33.2846 35.9651 32.7321 35.9651Z" fill="white" fill-opacity="0.75"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_d_1_5775)">
|
||||
<rect x="16.25" y="0.5" width="71" height="71" rx="35.5" fill="url(#paint0_linear_1_5775)"/>
|
||||
<rect x="17.4444" y="1.69444" width="68.6111" height="68.6111" rx="34.3056" stroke="white" stroke-opacity="0.75" stroke-width="2.38889"/>
|
||||
<path d="M44.9312 45.2695C44.3569 45.2695 43.9058 45.1396 43.5776 44.8799C43.2495 44.6201 43.0854 44.2646 43.0854 43.8135C43.0854 43.1504 43.2837 42.4565 43.6802 41.7319C44.0835 41.0073 44.6611 40.3271 45.4131 39.6914C46.1719 39.0557 47.0845 38.5396 48.1509 38.1431C49.2173 37.7466 50.4136 37.5483 51.7397 37.5483C53.0728 37.5483 54.2725 37.7466 55.3389 38.1431C56.4053 38.5396 57.3145 39.0557 58.0664 39.6914C58.8252 40.3271 59.4062 41.0073 59.8096 41.7319C60.2129 42.4565 60.4146 43.1504 60.4146 43.8135C60.4146 44.2646 60.2505 44.6201 59.9224 44.8799C59.5942 45.1396 59.1431 45.2695 58.5688 45.2695H44.9312ZM51.75 35.7231C51.0049 35.7231 50.3179 35.5215 49.689 35.1182C49.0669 34.7148 48.5645 34.1714 48.1816 33.4878C47.8057 32.8042 47.6177 32.0386 47.6177 31.1909C47.6177 30.3569 47.8057 29.605 48.1816 28.9351C48.5645 28.2583 49.0703 27.7217 49.6992 27.3252C50.3281 26.9287 51.0117 26.7305 51.75 26.7305C52.4951 26.7305 53.1821 26.9253 53.811 27.3149C54.4399 27.7046 54.9424 28.2378 55.3184 28.9146C55.7012 29.5845 55.8926 30.3364 55.8926 31.1704C55.8926 32.0249 55.7012 32.7974 55.3184 33.4878C54.9424 34.1714 54.4399 34.7148 53.811 35.1182C53.1821 35.5215 52.4951 35.7231 51.75 35.7231Z" fill="#E9E9E9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1_5775" x="49" y="12.5" width="55" height="55" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_5775"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_5775" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_1_5775" x="31.75" y="6.5" width="67" height="67" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_5775"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_5775" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_1_5775" x="0" y="12.5" width="55" height="55" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_5775"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_5775" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter3_d_1_5775" x="4.75" y="6.5" width="67" height="67" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_5775"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_5775" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter4_d_1_5775" x="12.25" y="0.5" width="79" height="79" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_5775"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_5775" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1_5775" x1="51.75" y1="-8" x2="53.75" y2="91.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#929292"/>
|
||||
<stop offset="1" stop-color="#CECECE"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,170 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { LoadingButton } from '@/components/button/LoadingButton';
|
||||
import { CardSection } from '@/components/cards/CardSection';
|
||||
import { RHFInput } from '@/components/form/hook-form/RHFInput';
|
||||
import { RHFProvider } from '@/components/form/hook-form/RHFProvider';
|
||||
import { RHFTextArea } from '@/components/form/hook-form/RHFTextArea';
|
||||
import { HorizontalSeparator } from '@/components/separator/HorizontalSeparator';
|
||||
import { DTOUpdateContact } from '@/features/contacts/api/types';
|
||||
import {
|
||||
useCreateContact,
|
||||
useUpdateContact,
|
||||
} from '@/features/contacts/api/useContact';
|
||||
import { ContactViewPublicData } from '@/features/contacts/compontents/view/ContactViewPublicData';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
|
||||
|
||||
import style from './contact-form.module.scss';
|
||||
|
||||
export type ContactFormValues = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
information: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
notes: string;
|
||||
address: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
contact?: Contact;
|
||||
};
|
||||
export const ContactForm = ({ contact }: Props) => {
|
||||
const responsive = useResponsiveLayout();
|
||||
const router = useRouter();
|
||||
const updateMutation = useUpdateContact(contact?.id);
|
||||
const createMutation = useCreateContact();
|
||||
const { t } = useTranslation('contact');
|
||||
const schema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
information: z.string(),
|
||||
phone: z.string(),
|
||||
email: z.string(),
|
||||
address: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
const methods = useForm<ContactFormValues>({
|
||||
delayError: 0,
|
||||
defaultValues: {
|
||||
firstName: contact?.firstName ?? '',
|
||||
lastName: contact?.lastName ?? '',
|
||||
information: contact?.information ?? '',
|
||||
phone: contact?.phone ?? '',
|
||||
email: contact?.email ?? '',
|
||||
address: contact?.address ?? '',
|
||||
url: contact?.url ?? '',
|
||||
notes: contact?.notes ?? '',
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
const onCancel = () => {
|
||||
if (contact) {
|
||||
router.back();
|
||||
} else {
|
||||
responsive.focusOnLeft();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (values: DTOUpdateContact) => {
|
||||
if (contact) {
|
||||
updateMutation.mutate(values, { onSuccess: () => router.back() });
|
||||
} else {
|
||||
createMutation.mutate(values, {
|
||||
onSuccess: (contact) => router.push(`/contacts/${contact.id}`),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RHFProvider showSubmit={false} methods={methods} id="contact-form">
|
||||
<div className={style.header}>
|
||||
<div className={`flex justify-between align-items pl-b pr-b `}>
|
||||
<Button type="button" onClick={onCancel} color="secondary">
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<p className="fw-bold fs-h3">
|
||||
{contact
|
||||
? t('form.update.mode.title')
|
||||
: t('form.contact.header.create.title')}
|
||||
</p>
|
||||
<LoadingButton
|
||||
loading={updateMutation.isPending}
|
||||
type="submit"
|
||||
form="contact-form"
|
||||
onClick={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
{t('Validate')}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
<HorizontalSeparator />
|
||||
</div>
|
||||
|
||||
{contact?.publicData && (
|
||||
<>
|
||||
<CardSection>
|
||||
<ContactViewPublicData isEditMode={true} contact={contact} />
|
||||
</CardSection>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CardSection>
|
||||
<p className="fw-bold fs-l">{t('form.contact.contact.title')}</p>
|
||||
{!contact?.publicData && (
|
||||
<>
|
||||
<RHFInput name="firstName" label={t('Firstname')} fullWidth />
|
||||
<RHFInput name="lastName" label={t('Lastname')} fullWidth />
|
||||
</>
|
||||
)}
|
||||
|
||||
<RHFInput
|
||||
name="phone"
|
||||
label={t('form.contact.phone.label')}
|
||||
fullWidth
|
||||
/>
|
||||
<RHFInput
|
||||
name="email"
|
||||
label={t('form.mail.label', { ns: 'common' })}
|
||||
fullWidth
|
||||
/>
|
||||
<RHFInput
|
||||
name="address"
|
||||
label={t('form.address.label', { ns: 'common' })}
|
||||
fullWidth
|
||||
/>
|
||||
<RHFInput
|
||||
name="url"
|
||||
label={t('form.url.label', { ns: 'common' })}
|
||||
fullWidth
|
||||
/>
|
||||
<RHFTextArea
|
||||
name="information"
|
||||
label={t('Informations')}
|
||||
fullWidth
|
||||
rows={2}
|
||||
/>
|
||||
</CardSection>
|
||||
|
||||
<CardSection showSeparator={false}>
|
||||
<p className="fw-bold fs-l">{t('form.contact.moreInfo.title')}</p>
|
||||
<RHFTextArea
|
||||
name="notes"
|
||||
label={t('form.notes.label', { ns: 'common' })}
|
||||
fullWidth
|
||||
rows={6}
|
||||
/>
|
||||
</CardSection>
|
||||
</RHFProvider>
|
||||
);
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
.header {
|
||||
position: sticky;
|
||||
background-color: white;
|
||||
top: -30px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.header {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { LeftRightContent } from '@/components/layouts/responsive/LeftRightContent';
|
||||
import { ResponsiveLayout } from '@/components/layouts/responsive/ResponsiveLayout';
|
||||
import { ContactList } from '@/features/contacts/compontents/list/ContactList';
|
||||
|
||||
export function ContactLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<ResponsiveLayout>
|
||||
<LeftRightContent leftContent={<ContactList />}>
|
||||
{children}
|
||||
</LeftRightContent>
|
||||
</ResponsiveLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ContactList } from '@/features/contacts/compontents/list/ContactList';
|
||||
|
||||
import styles from './contract-layout.module.scss';
|
||||
|
||||
export const ContactLayoutLeft = () => {
|
||||
return (
|
||||
<div className={styles.leftPanel} aria-label="Teams panel">
|
||||
<ContactList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.leftRightContent {
|
||||
|
||||
.left, .right {
|
||||
position: fixed;
|
||||
height: calc(100dvh - var(--header-height));
|
||||
width: 100vw;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: -100vw;
|
||||
overflow-y: auto !important;
|
||||
|
||||
&.active {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
left: -100vw;
|
||||
&.active {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 993px) {
|
||||
.leftRightContent {
|
||||
display: flex;
|
||||
height: inherit;
|
||||
flex:1;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
background-color: var(--c--theme--colors--primary-bg);
|
||||
padding: var(--c--theme--spacings--b);
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
border-right: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import classNames from 'classnames';
|
||||
import { useRouter } from 'next/router';
|
||||
import * as React from 'react';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, StyledLink } from '@/components';
|
||||
import { FocusOnContent } from '@/components/layouts/responsive/FocusOnContent';
|
||||
import { useContacts } from '@/features/contacts/api/useContact';
|
||||
import { ContactListItem } from '@/features/contacts/compontents/list/ContactListItem';
|
||||
import { ContactListHeader } from '@/features/contacts/compontents/list/header/ContactListHeader';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
import styles from './contact-list.module.scss';
|
||||
|
||||
type ContactGroup = {
|
||||
letter: string;
|
||||
contacts: Contact[];
|
||||
};
|
||||
|
||||
export const ContactList = () => {
|
||||
const { t } = useTranslation('contact');
|
||||
const listQuery = useContacts();
|
||||
|
||||
const {
|
||||
query: { id },
|
||||
} = useRouter();
|
||||
|
||||
const groups = useMemo(() => {
|
||||
if (!listQuery.data) {
|
||||
return [];
|
||||
}
|
||||
const data = listQuery.data.sort((a, b) =>
|
||||
a.lastName.localeCompare(b.lastName),
|
||||
);
|
||||
|
||||
const map = data.reduce(
|
||||
(acc: { [key: string]: Contact[] }, val: Contact) => {
|
||||
const lastName = val.lastName;
|
||||
const char = lastName.charAt(0).toUpperCase();
|
||||
const current = acc[char] ?? [];
|
||||
current.push(val);
|
||||
acc[char] = [...current];
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const res: ContactGroup[] = Object.keys(map).map((el) => ({
|
||||
letter: el,
|
||||
contacts: map[el],
|
||||
}));
|
||||
return res;
|
||||
}, [listQuery.data]);
|
||||
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
|
||||
const refs = alphabet.reduce(
|
||||
(acc: { [key: string]: RefObject<any> }, value) => {
|
||||
acc[value.toUpperCase()] = React.createRef();
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// const scrollIntoView = (letter: string) => {
|
||||
// const ref = refs[letter.toUpperCase()];
|
||||
// if (!ref || !ref.current) {
|
||||
// return;
|
||||
// }
|
||||
// ref.current.scrollIntoView({
|
||||
// behavior: 'smooth',
|
||||
// block: 'start',
|
||||
// });
|
||||
// };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ContactListHeader />
|
||||
<div className={`${styles.listContainer}`}>
|
||||
{/*<div className={styles.fixedElement}>*/}
|
||||
{/* <div className={styles.letterShortcutsList}>*/}
|
||||
{/* {alphabet.map((element) => {*/}
|
||||
{/* return (*/}
|
||||
{/* <span*/}
|
||||
{/* onClick={() => scrollIntoView(element)}*/}
|
||||
{/* className={`fs-s clr-greyscale-300 ${styles.letterShortcut}`}*/}
|
||||
{/* >*/}
|
||||
{/* {element.toUpperCase()}*/}
|
||||
{/* </span>*/}
|
||||
{/* );*/}
|
||||
{/* })}*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
<p className="fs-h6 clr-primary-500 fw-bold pl-s">
|
||||
{t('contact.list.title')}
|
||||
</p>
|
||||
{groups.map((group, index) => {
|
||||
return (
|
||||
<Box ref={refs[group.letter]} key={group.letter} $gap="6px">
|
||||
<div className="fs-l fw-bold pl-s clr-greyscale-500">
|
||||
{group.letter}
|
||||
</div>
|
||||
{group.contacts.map((contact) => {
|
||||
const isActive = id === contact.id;
|
||||
return (
|
||||
<StyledLink
|
||||
key={`${contact.id}-${index}`}
|
||||
href={`/contacts/${contact.id}`}
|
||||
>
|
||||
<FocusOnContent>
|
||||
<Box
|
||||
className={classNames(styles.contactItem, {
|
||||
[styles.active]: isActive,
|
||||
})}
|
||||
>
|
||||
<ContactListItem
|
||||
isActive={isActive}
|
||||
contact={contact}
|
||||
/>
|
||||
</Box>
|
||||
</FocusOnContent>
|
||||
</StyledLink>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { People } from '@/components/people/People';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
type Props = {
|
||||
contact: Contact;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const ContactListItem = ({ contact, isActive }: Props) => {
|
||||
return (
|
||||
<People
|
||||
isActive={isActive}
|
||||
fullName={`${contact.firstName} ${contact.lastName}`}
|
||||
avatarStr={contact.firstName.charAt(0)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
.listContainer {
|
||||
position: relative;
|
||||
height: calc(100dvh - 60px - 139px);
|
||||
padding-top: var(--c--theme--spacings--s) ;
|
||||
scrollbar-width: thin;
|
||||
overflow-y: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.fixedElement {
|
||||
|
||||
position:fixed;
|
||||
top:calc(60px + 139px);
|
||||
bottom: 0;
|
||||
margin-left: 275px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index:100;
|
||||
width:max-content;
|
||||
|
||||
}
|
||||
|
||||
.test {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background-color: #A0E659;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.letterShortcutsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 98;
|
||||
overflow-y: auto;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.letterShortcut {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contactItem {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
padding: 3px 12px 5px 24px;
|
||||
&:hover, &.active {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.contactListItemContainer {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
color: var(--c--theme--colors--greyscale-1000);
|
||||
|
||||
&.active {
|
||||
//color: var(--c--theme--colors--primary-500) !important;
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ListSearchHeader } from '@/components/search/list/ListSearchHeader';
|
||||
import { ContactQuickSearch } from '@/features/contacts/compontents/search/ContactQuickSearch';
|
||||
|
||||
export const ContactListHeader = () => {
|
||||
return (
|
||||
<ListSearchHeader
|
||||
title="Contacts"
|
||||
quickSearchComponent={(closeModal) => (
|
||||
<ContactQuickSearch afterSelect={closeModal} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
.listHeader {
|
||||
padding: var(--c--theme--spacings--st) var(--c--theme--spacings--s);
|
||||
padding-bottom: var(--c--theme--spacings--s) ;
|
||||
border-bottom: 1px solid var(--c--theme--colors--greyscale-100);
|
||||
|
||||
.headerInput {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
background-color: var(--c--theme--colors--greyscale-100);;
|
||||
|
||||
.kbd {
|
||||
padding: 3px 8px;
|
||||
|
||||
background-color: var(--c--theme--colors--greyscale-300);;
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { QuickSearch, QuickSearchData } from '@/components/search/QuickSearch';
|
||||
import { useContacts } from '@/features/contacts/api/useContact';
|
||||
import { ContactListItem } from '@/features/contacts/compontents/list/ContactListItem';
|
||||
import { CreateNewContactSearchShortcut } from '@/features/contacts/compontents/search/CreateNewContactSearchShortcut';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
type Props = {
|
||||
afterSelect?: () => void;
|
||||
};
|
||||
export const ContactQuickSearch = ({ afterSelect }: Props) => {
|
||||
const { t } = useTranslation('contact');
|
||||
const router = useRouter();
|
||||
const contacts = useContacts();
|
||||
|
||||
const getDefaultData = (): QuickSearchData<Contact>[] => {
|
||||
return [
|
||||
{
|
||||
groupName: t('contact.search.my_contact.title'),
|
||||
elements: contacts.data ? [...contacts.data] : [],
|
||||
startActions: [
|
||||
{
|
||||
onSelect: () => {
|
||||
router.push('/contacts/create');
|
||||
afterSelect?.();
|
||||
},
|
||||
content: <CreateNewContactSearchShortcut />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: t('contact.search.other_contact.title'),
|
||||
elements: [],
|
||||
emptyString: t('contact.search.other_contact.empty.label'),
|
||||
},
|
||||
];
|
||||
};
|
||||
const [data, setData] =
|
||||
useState<QuickSearchData<Contact>[]>(getDefaultData());
|
||||
|
||||
useEffect(() => {
|
||||
setData(getDefaultData());
|
||||
}, [contacts.data]);
|
||||
|
||||
const onFilter = (str: string) => {
|
||||
const result = getDefaultData();
|
||||
const myContacts = contacts.data ?? [];
|
||||
|
||||
if (str === '') {
|
||||
result[0].elements = result[0].elements.splice(0, 5);
|
||||
setData(result);
|
||||
} else {
|
||||
const newMyContact = myContacts.filter((element) => {
|
||||
return (
|
||||
element.firstName.toLowerCase().indexOf(str.toLowerCase()) > -1 ||
|
||||
element.lastName.toLowerCase().indexOf(str.toLowerCase()) > -1
|
||||
);
|
||||
});
|
||||
|
||||
// const newOtherContact = other_contact.filter((element) => {
|
||||
// return element.toLowerCase().indexOf(str.toLowerCase()) > -1;
|
||||
// });
|
||||
|
||||
result[0].elements = newMyContact.splice(0, 5);
|
||||
result[1].elements = [];
|
||||
|
||||
setData(result);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = (contact: Contact) => {
|
||||
router.push(`/contacts/` + contact.id);
|
||||
afterSelect?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<QuickSearch
|
||||
data={data}
|
||||
onFilter={onFilter}
|
||||
onSelect={onSelect}
|
||||
renderElement={(contact) => <ContactListItem contact={contact} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CreateNewContactSearchShortcut = () => {
|
||||
const { t } = useTranslation('contact');
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div
|
||||
role="none"
|
||||
className="flex-v-center"
|
||||
onClick={() => router.push('/contacts/create')}
|
||||
>
|
||||
<span className="material-icons ">add</span>
|
||||
<span className="ml-st">{t('contact.search.add_new_contact')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Card } from '@/components';
|
||||
import { CardSection } from '@/components/cards/CardSection';
|
||||
import { SimpleViewField } from '@/components/form/simple-view-field/SimpleViewField';
|
||||
import { ContactViewHeader } from '@/features/contacts/compontents/view/ContactViewHeader';
|
||||
import { ContactViewPersonalData } from '@/features/contacts/compontents/view/ContactViewPersonalData';
|
||||
import { ContactViewPublicData } from '@/features/contacts/compontents/view/ContactViewPublicData';
|
||||
import { ContactViewShortcuts } from '@/features/contacts/compontents/view/ContactViewShortcuts';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
import style from './contact-view.module.scss';
|
||||
|
||||
type Props = {
|
||||
contact: Contact;
|
||||
};
|
||||
export const ContactView = ({ contact }: Props) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardSection>
|
||||
<ContactViewHeader contact={contact} />
|
||||
</CardSection>
|
||||
<CardSection>
|
||||
<ContactViewShortcuts />
|
||||
</CardSection>
|
||||
{contact.publicData && (
|
||||
<CardSection>
|
||||
<ContactViewPublicData contact={contact} />
|
||||
</CardSection>
|
||||
)}
|
||||
<CardSection>
|
||||
<ContactViewPersonalData contact={contact} />
|
||||
</CardSection>
|
||||
<CardSection>
|
||||
<div className={style.informationContainer}>
|
||||
<SimpleViewField label="Informations">
|
||||
<span className="clr-danger-800">{contact?.information}</span>
|
||||
</SimpleViewField>
|
||||
</div>
|
||||
</CardSection>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
DeleteConfirmationModal,
|
||||
ModalSize,
|
||||
useModal,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { IconOptions } from '@/components';
|
||||
import { ContactAvatar } from '@/components/avatar/ContactAvatar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
} from '@/components/dropdown-menu/DropdownMenu';
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { FocusOnLeft } from '@/components/layouts/responsive/FocusOnLeft';
|
||||
import { useDeleteContact } from '@/features/contacts/api/useContact';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
import style from './contact-view.module.scss';
|
||||
|
||||
type Props = {
|
||||
contact: Contact;
|
||||
showOptions?: boolean;
|
||||
};
|
||||
export const ContactViewHeader = ({ contact, showOptions = true }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const deleteContact = useDeleteContact(contact.id);
|
||||
const deleteModal = useModal();
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const actions: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Update'),
|
||||
icon: 'edit',
|
||||
callback: () => router.push(`/contacts/${contact.id}/edit`),
|
||||
},
|
||||
{
|
||||
label: t('Remove contact'),
|
||||
icon: 'delete',
|
||||
callback: deleteModal.open,
|
||||
},
|
||||
];
|
||||
|
||||
const getInformationText = (): string => {
|
||||
let result = contact.publicData?.information ?? '';
|
||||
|
||||
const personalInformation =
|
||||
contact.information?.replace(/(\r\n|\n|\r)/gm, '').trim() ?? '';
|
||||
|
||||
if (result.length > 0 && personalInformation.length > 0) {
|
||||
result = `${result} | ${contact.information}`;
|
||||
} else if (result.length === 0 && personalInformation.length > 0) {
|
||||
result = personalInformation;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${style.contactHeader} gap-st`}>
|
||||
<ContactAvatar size="large" contact={contact} />
|
||||
<span className="fs-h3 fw-bold">
|
||||
{contact.firstName} {contact.lastName}
|
||||
</span>
|
||||
<span
|
||||
className={`fs-l clr-grey-400 text-center ${style.informationText}`}
|
||||
>
|
||||
{getInformationText()}
|
||||
</span>
|
||||
|
||||
<div className={style.back}>
|
||||
<FocusOnLeft>
|
||||
<Icon icon="arrow_back" />
|
||||
</FocusOnLeft>
|
||||
</div>
|
||||
{showOptions && (
|
||||
<div className={style.moreActions}>
|
||||
<DropdownMenu
|
||||
options={actions}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<IconOptions
|
||||
isOpen={isDropOpen}
|
||||
aria-label={t('Open the team options')}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DeleteConfirmationModal
|
||||
{...deleteModal}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={t('delete.contact.modal.title', {
|
||||
contact: `${contact.firstName} ${contact.lastName}`,
|
||||
})}
|
||||
onDecide={(decision) => {
|
||||
console.log(decision);
|
||||
if (decision === 'delete') {
|
||||
deleteContact.mutate(undefined, {
|
||||
onSuccess: () => router.push('/contacts'),
|
||||
});
|
||||
} else {
|
||||
deleteModal.close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
</DeleteConfirmationModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SimpleViewField } from '@/components/form/simple-view-field/SimpleViewField';
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
type Props = {
|
||||
contact: Contact;
|
||||
};
|
||||
|
||||
export const ContactViewPersonalData = ({ contact }: Props) => {
|
||||
const { t } = useTranslation(['contact', 'common']);
|
||||
if (!contact) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="gap-s">
|
||||
<p className="clr-greyscale-700 fw-bold fs-h5">
|
||||
{t('form.contact.contact.title')}
|
||||
</p>
|
||||
|
||||
<SimpleViewField
|
||||
label={t('form.contact.phone.label')}
|
||||
right={<Icon className="clr-greyscale-500" icon="phone" />}
|
||||
>
|
||||
<span className="clr-danger-800">{contact?.phone ?? '-'}</span>
|
||||
</SimpleViewField>
|
||||
<SimpleViewField
|
||||
label={t('form.contact.email.label')}
|
||||
right={<Icon className="clr-greyscale-500" icon="email" />}
|
||||
>
|
||||
<span className="clr-danger-800">{contact?.email ?? '-'}</span>
|
||||
</SimpleViewField>
|
||||
<SimpleViewField label={t('form.address.label')}>
|
||||
<span className="clr-danger-800">{contact?.address ?? '-'}</span>
|
||||
</SimpleViewField>
|
||||
<SimpleViewField
|
||||
label={t('form.url.label')}
|
||||
right={<Icon className="clr-greyscale-500" icon="link" />}
|
||||
>
|
||||
<span className="clr-danger-800">{contact?.url ?? '-'}</span>
|
||||
</SimpleViewField>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SimpleViewField } from '@/components/form/simple-view-field/SimpleViewField';
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { Contact } from '@/features/contacts/contact';
|
||||
|
||||
type Props = {
|
||||
contact: Contact;
|
||||
isEditMode?: boolean;
|
||||
};
|
||||
|
||||
export const ContactViewPublicData = ({
|
||||
contact,
|
||||
isEditMode = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
if (!contact) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="gap-s">
|
||||
<p className="clr-greyscale-700 fw-bold fs-h5">
|
||||
{t('form.contact.publicData.title')}
|
||||
</p>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<SimpleViewField label={t('Firstname')}>
|
||||
<span className="clr-danger-800">
|
||||
{contact.publicData?.firstName}
|
||||
</span>
|
||||
</SimpleViewField>
|
||||
<SimpleViewField label={t('Lastname')}>
|
||||
<span className="clr-danger-800">
|
||||
{contact.publicData?.lastName}
|
||||
</span>
|
||||
</SimpleViewField>
|
||||
<SimpleViewField label={t('form.contact.information.label')}>
|
||||
<span className="clr-danger-800">
|
||||
{contact.publicData?.information}
|
||||
</span>
|
||||
</SimpleViewField>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SimpleViewField
|
||||
label="Téléphone"
|
||||
right={<Icon className="clr-greyscale-400" icon="phone" />}
|
||||
>
|
||||
<span className="clr-danger-800">{contact.publicData?.phone}</span>
|
||||
</SimpleViewField>
|
||||
<SimpleViewField
|
||||
label="Email"
|
||||
right={<Icon className="clr-greyscale-400" icon="email" />}
|
||||
>
|
||||
<span className="clr-danger-800">{contact.publicData?.email}</span>
|
||||
</SimpleViewField>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import style from './contact-view.module.scss';
|
||||
|
||||
type Props = {};
|
||||
export const ContactViewShortcuts = (props: Props) => {
|
||||
const shortcuts = [
|
||||
{ label: 'Téléphone', icon: 'phone' },
|
||||
{ label: 'Message', icon: 'message' },
|
||||
{ label: 'Email', icon: 'email' },
|
||||
];
|
||||
return (
|
||||
<div className="flex align-items justify-center">
|
||||
{shortcuts.map((shortcut) => (
|
||||
<div key={shortcut.label} className={style.shortcutContainer}>
|
||||
<div className={style.shortcutIconContainer}>
|
||||
<span className="material-icons" aria-hidden={true}>
|
||||
{shortcut.icon}
|
||||
</span>
|
||||
</div>
|
||||
<span className="fs-s fw-bold clr-primary-500 mt-t">
|
||||
{shortcut.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { FocusOnLeft } from '@/components/layouts/responsive/FocusOnLeft';
|
||||
import HomeIcon from '@/features/contacts/assets/home-contact.svg';
|
||||
|
||||
import styles from './unselected-contact.module.scss';
|
||||
|
||||
export const UnselectedContact = () => {
|
||||
const { t } = useTranslation('contact');
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<HomeIcon />
|
||||
<span className="fs-ml fw-bold clr-greyscale-700">
|
||||
{t('contact.home.unselected.title')}
|
||||
</span>
|
||||
<span className="fs-s clr-greyscale-500 text-center">
|
||||
{t('contact.home.unselected.subTitle')}
|
||||
</span>
|
||||
<FocusOnLeft>
|
||||
<Icon icon="visibility" />
|
||||
</FocusOnLeft>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
.contactHeader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.informationText {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.back {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
.moreActions {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.shortcutsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.shortcutContainer {
|
||||
cursor: pointer;
|
||||
margin: 0 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.shortcutIconContainer {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--c--theme--colors--primary-500);
|
||||
}
|
||||
|
||||
|
||||
.informationContainer {
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid var(--c--theme--colors--greyscale-100);
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
padding: var(--c--theme--spacings--s);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
randAwsRequestId,
|
||||
randBoolean,
|
||||
randCatchPhrase,
|
||||
randEmail,
|
||||
randFirstName,
|
||||
randFullAddress,
|
||||
randLastName,
|
||||
randLines,
|
||||
randPhoneNumber,
|
||||
randUrl,
|
||||
} from '@ngneat/falso';
|
||||
|
||||
import { PublicContact } from '@/features/contacts/api/types';
|
||||
|
||||
export type Contact = {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
address?: string;
|
||||
url?: string;
|
||||
phone?: string;
|
||||
information?: string;
|
||||
notes?: string;
|
||||
publicData?: PublicContact;
|
||||
};
|
||||
|
||||
const getMockedContact = (): Contact => {
|
||||
return {
|
||||
id: randAwsRequestId(),
|
||||
firstName: randFirstName({ withAccents: false }),
|
||||
lastName: randLastName({ withAccents: false }),
|
||||
email: randEmail(),
|
||||
address: randFullAddress(),
|
||||
url: randUrl(),
|
||||
notes: randLines(),
|
||||
phone: randPhoneNumber({ countryCode: 'FR' }),
|
||||
information: randCatchPhrase({ maxCharCount: 50 }),
|
||||
};
|
||||
};
|
||||
|
||||
export const ALL_CONTACTS = Array.from(Array(50).keys()).map((id) => {
|
||||
const contact: Contact = getMockedContact();
|
||||
const havePublicData = randBoolean();
|
||||
if (havePublicData) {
|
||||
contact.publicData = getMockedContact();
|
||||
}
|
||||
return contact;
|
||||
});
|
||||
|
||||
export const ALL_OTHER_CONTACTS = Array.from(Array(20).keys()).map((id) => {
|
||||
return getMockedContact();
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Box, LogoGouv, StyledLink, Text } from '@/components';
|
||||
import { useConfigStore } from '@/core';
|
||||
|
||||
import IconLink from './assets/external-link.svg';
|
||||
|
||||
const BlueStripe = styled.div`
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: var(--c--theme--colors--primary-600);
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
export const Footer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useConfigStore();
|
||||
|
||||
return (
|
||||
<Box $position="relative" as="footer">
|
||||
<BlueStripe />
|
||||
<Box $padding={{ top: 'large', horizontal: 'big', bottom: 'small' }}>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="1.5rem"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css="flex-wrap: wrap;"
|
||||
>
|
||||
<Box>
|
||||
<LogoGouv
|
||||
textProps={{
|
||||
$size: '1.2rem',
|
||||
$margin: { vertical: '0.3rem' },
|
||||
$css: `
|
||||
line-height:18px;
|
||||
text-transform: uppercase;
|
||||
`,
|
||||
$maxWidth: '100px',
|
||||
}}
|
||||
imagesWidth={66}
|
||||
>
|
||||
République Française
|
||||
</LogoGouv>
|
||||
</Box>
|
||||
<Box
|
||||
$direction="row"
|
||||
$css={`
|
||||
column-gap: 1.5rem;
|
||||
row-gap: .5rem;
|
||||
flex-wrap: wrap;
|
||||
`}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: 'legifrance.gouv.fr',
|
||||
href: 'https://legifrance.gouv.fr/',
|
||||
},
|
||||
{
|
||||
label: 'info.gouv.fr',
|
||||
href: 'https://info.gouv.fr/',
|
||||
},
|
||||
{
|
||||
label: 'service-public.fr',
|
||||
href: 'https://service-public.fr/',
|
||||
},
|
||||
{
|
||||
label: 'data.gouv.fr',
|
||||
href: 'https://data.gouv.fr/',
|
||||
},
|
||||
].map(({ label, href }) => (
|
||||
<StyledLink
|
||||
key={label}
|
||||
href={href}
|
||||
target="__blank"
|
||||
$css={`
|
||||
gap:0.2rem;
|
||||
transition: box-shadow 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 0 0 var(--c--theme--colors--greyscale-text);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Text $weight="bold">{label}</Text>
|
||||
<IconLink width={18} aria-hidden="true" />
|
||||
</StyledLink>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
$direction="row"
|
||||
$margin={{ top: 'big' }}
|
||||
$padding={{ top: 'tiny' }}
|
||||
$css="
|
||||
flex-wrap: wrap;
|
||||
border-top: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
column-gap: 1rem;
|
||||
row-gap: .5rem;
|
||||
"
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: t('Legal Notice'),
|
||||
href: '/legal-notice',
|
||||
},
|
||||
{
|
||||
label: t('Personal data and cookies'),
|
||||
href: '/personal-data-cookies',
|
||||
},
|
||||
{
|
||||
label: t('Accessibility: non-compliant'),
|
||||
href: '/accessibility',
|
||||
},
|
||||
].map(({ label, href }) => (
|
||||
<StyledLink
|
||||
key={label}
|
||||
href={href}
|
||||
$css={`
|
||||
padding-right: 1rem;
|
||||
&:not(:last-child) {
|
||||
box-shadow: inset -1px 0px 0px 0px var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Text
|
||||
$variation="600"
|
||||
$size="m"
|
||||
$css={`
|
||||
transition: box-shadow 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 0 0 var(--c--theme--colors--greyscale-text);
|
||||
}
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
as="p"
|
||||
$size="m"
|
||||
$margin={{ top: 'big' }}
|
||||
$variation="600"
|
||||
$display="inline"
|
||||
>
|
||||
{config?.RELEASE && (
|
||||
<>
|
||||
{t(`Version: {{release}}`, { release: config?.RELEASE })} •
|
||||
</>
|
||||
)}
|
||||
|
||||
<Trans>
|
||||
Unless otherwise stated, all content on this site is under
|
||||
<StyledLink
|
||||
href="https://github.com/etalab/licence-ouverte/blob/master/LO.md"
|
||||
target="__blank"
|
||||
$css={`
|
||||
display:inline-flex;
|
||||
box-shadow: 0px 1px 0 0 var(--c--theme--colors--greyscale-text);
|
||||
margin-left: 0.3rem;
|
||||
`}
|
||||
>
|
||||
<Text $variation="600">licence etalab-2.0</Text>
|
||||
<IconLink width={18} aria-hidden="true" />
|
||||
</StyledLink>
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6Zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8Z"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 219 B |
@@ -1,41 +1,29 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton, Text } from '@/components';
|
||||
import { DropdownMenu } from '@/components/dropdown-menu/DropdownMenu';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { Breakpoints, useBreakpoint } from '@/hooks/useBreakpoints';
|
||||
|
||||
export const AccountDropdown = () => {
|
||||
const isMobile = useBreakpoint(Breakpoints.LG, false);
|
||||
const { t } = useTranslation();
|
||||
const { userData, logout } = useAuthStore();
|
||||
|
||||
const userName = userData?.name || t('No Username');
|
||||
const classNamesColors = classNames('', {
|
||||
['clr-primary-500']: !isMobile,
|
||||
['clr-greyscale-000']: isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<DropButton
|
||||
button={
|
||||
<Box $flex $direction="row" $align="center">
|
||||
<Text $theme="primary">{userName}</Text>
|
||||
<Text className="material-icons" $theme="primary" aria-hidden="true">
|
||||
arrow_drop_down
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
<DropdownMenu
|
||||
showArrow
|
||||
arrowClassname={classNamesColors}
|
||||
options={[{ icon: 'logout', label: t('Logout'), callback: logout }]}
|
||||
>
|
||||
<Box $css="display: flex; direction: column; gap: 0.5rem">
|
||||
<Button
|
||||
onClick={logout}
|
||||
key="logout"
|
||||
color="primary-text"
|
||||
icon={
|
||||
<span className="material-icons" aria-hidden="true">
|
||||
logout
|
||||
</span>
|
||||
}
|
||||
aria-label={t('Logout')}
|
||||
>
|
||||
<Text $weight="normal">{t('Logout')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
<span className={classNamesColors}>{userName}</span>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Box, LogoGouv, StyledLink, Text } from '@/components/';
|
||||
import { Box, StyledLink, Text } from '@/components/';
|
||||
import { LaGaufre } from '@/features/header/LaGaufre';
|
||||
|
||||
import { LanguagePicker } from '../language/';
|
||||
@@ -11,15 +10,7 @@ import { LanguagePicker } from '../language/';
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
import { default as IconApplication } from './assets/icon-application.svg?url';
|
||||
|
||||
export const HEADER_HEIGHT = '100px';
|
||||
|
||||
const RedStripe = styled.div`
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background: var(--c--theme--colors--danger-500);
|
||||
top: 0;
|
||||
`;
|
||||
export const HEADER_HEIGHT = '60px';
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -33,31 +24,17 @@ export const Header = () => {
|
||||
$zIndex="100"
|
||||
$css="box-shadow: 0 1px 4px #00000040;"
|
||||
>
|
||||
<RedStripe />
|
||||
<Box
|
||||
$margin={{ horizontal: 'xbig' }}
|
||||
$margin={{ horizontal: 'small' }}
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
>
|
||||
<Box $align="center" $gap="6rem" $direction="row">
|
||||
<LogoGouv
|
||||
textProps={{
|
||||
$size: 't',
|
||||
$css: `
|
||||
line-height:11px;
|
||||
text-transform: uppercase;
|
||||
`,
|
||||
$margin: { vertical: '3px' },
|
||||
$maxWidth: '100px',
|
||||
}}
|
||||
>
|
||||
République Française
|
||||
</LogoGouv>
|
||||
<StyledLink href="/">
|
||||
<Box $align="center" $gap="1rem" $direction="row">
|
||||
<Image priority src={IconApplication} alt="" />
|
||||
<Text $margin="none" as="h2" $theme="primary">
|
||||
<Image height={30} priority src={IconApplication} alt="" />
|
||||
<Text $margin="none" as="h3" $theme="primary">
|
||||
{t('Régie')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -1,92 +1,51 @@
|
||||
import { Select } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components/';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
} from '@/components/dropdown-menu/DropdownMenu';
|
||||
import { useDropdownMenu } from '@/components/dropdown-menu/useDropdownMenu';
|
||||
import { Icon } from '@/components/icons/Icon';
|
||||
import { Breakpoints, useBreakpoint } from '@/hooks/useBreakpoints';
|
||||
|
||||
import IconLanguage from './assets/icon-language.svg?url';
|
||||
|
||||
const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
|
||||
flex-shrink: 0;
|
||||
width: 5.5rem;
|
||||
|
||||
.c__select__wrapper {
|
||||
min-height: 2rem;
|
||||
height: auto;
|
||||
border-color: #ddd;
|
||||
padding: 0 0.15rem 0 0.45rem;
|
||||
border-radius: 1px;
|
||||
|
||||
.labelled-box .labelled-box__children {
|
||||
padding-right: 2rem;
|
||||
|
||||
.c_select__render .typo-text {
|
||||
${({ $isSmall }) => $isSmall && `display: none;`}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--c--theme--colors--primary-500);
|
||||
}
|
||||
|
||||
.c__button--tertiary-text:focus-visible {
|
||||
outline: var(--c--theme--colors--primary-600) solid 2px;
|
||||
border-radius: var(--c--components--button--border-radius--focus);
|
||||
}
|
||||
}
|
||||
`;
|
||||
import style from './language-picker.module.scss';
|
||||
|
||||
export const LanguagePicker = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const isMobile = useBreakpoint(Breakpoints.LG, false);
|
||||
const { i18n } = useTranslation();
|
||||
const { preload: languages } = i18n.options;
|
||||
|
||||
const optionsPicker = useMemo(() => {
|
||||
return (languages || []).map((lang) => ({
|
||||
value: lang,
|
||||
label: lang,
|
||||
render: () => (
|
||||
<Box
|
||||
className="c_select__render"
|
||||
$direction="row"
|
||||
$gap="0.7rem"
|
||||
$align="center"
|
||||
>
|
||||
<Image priority src={IconLanguage} alt="" />
|
||||
<Text $theme="primary">{lang.toUpperCase()}</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
}, [languages]);
|
||||
|
||||
/**
|
||||
* @description prevent select div to receive focus on keyboard navigation so the focus goes directly to inner button
|
||||
* @see https://github.com/numerique-gouv/people/pull/379
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
document
|
||||
.querySelector('.c__select-language-picker .c__select__wrapper')
|
||||
?.setAttribute('tabindex', '-1');
|
||||
}, []);
|
||||
const dropdownMenu = useDropdownMenu();
|
||||
const options: DropdownMenuOption[] = (languages || []).map((lang) => {
|
||||
return {
|
||||
label: lang.toUpperCase(),
|
||||
callback: () => {
|
||||
void i18n.changeLanguage(lang);
|
||||
},
|
||||
};
|
||||
});
|
||||
const classNamesColors = {
|
||||
['clr-primary-500']: !isMobile,
|
||||
['clr-greyscale-000']: isMobile,
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectStyled
|
||||
label={t('Language')}
|
||||
showLabelWhenSelected={false}
|
||||
clearable={false}
|
||||
hideLabel
|
||||
defaultValue={i18n.language}
|
||||
className="c_select__no_bg c__select-language-picker"
|
||||
options={optionsPicker}
|
||||
onChange={(e) => {
|
||||
i18n.changeLanguage(e.target.value as string).catch((err) => {
|
||||
console.error('Error changing language', err);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<DropdownMenu
|
||||
{...dropdownMenu}
|
||||
showArrow
|
||||
options={options}
|
||||
arrowClassname={classNames('', classNamesColors)}
|
||||
>
|
||||
<div className={style.simpleContent}>
|
||||
<Icon
|
||||
className={classNames('fs-h5', classNamesColors)}
|
||||
icon="translate"
|
||||
/>
|
||||
<span className={classNames('mr-st', classNamesColors)}>
|
||||
{i18n.language.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.334 6.66683L15.2673 14.0002H13.8307L13.03 12.0002H10.3033L9.50398 14.0002H8.06798L11.0007 6.66683H12.334ZM6.66732 1.3335V2.66683H10.6673V4.00016H9.35532C8.84106 5.54821 8.0203 6.97684 6.94198 8.20083C7.42285 8.6299 7.94444 9.01104 8.49932 9.33883L7.99865 10.5908C7.28233 10.1846 6.61238 9.70149 6.00065 9.15016C4.80971 10.228 3.39934 11.035 1.86665 11.5155L1.50932 10.2295C2.82254 9.81077 4.03266 9.11972 5.06065 8.2015C4.29978 7.34012 3.66603 6.37434 3.17865 5.3335H4.67198C5.04355 6.0194 5.4891 6.66257 6.00065 7.2515C6.83406 6.2909 7.49085 5.19037 7.94065 4.00083L1.33398 4.00016V2.66683H5.33398V1.3335H6.66732ZM11.6673 8.59016L10.836 10.6668H12.4973L11.6673 8.59016Z"
|
||||
fill="#000091"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 877 B |
@@ -0,0 +1,6 @@
|
||||
.simpleContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--theme--spacings--st);
|
||||
}
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { Button } from '@openfun/cunningham-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton, IconOptions, Text } from '@/components';
|
||||
import { Box, DropButton, IconOptions } from '@/components';
|
||||
|
||||
import { MailDomain, Role } from '../../domains/types';
|
||||
import { Access } from '../types';
|
||||
@@ -60,7 +60,7 @@ export const AccessAction = ({
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Text $theme="primary">{t('Update role')}</Text>
|
||||
{t('Update role')}
|
||||
</Button>
|
||||
)}
|
||||
{mailDomain.abilities.delete && (
|
||||
@@ -77,7 +77,7 @@ export const AccessAction = ({
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Text $theme="primary">{t('Remove from domain')}</Text>
|
||||
{t('Remove from domain')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
+55
-54
@@ -105,11 +105,11 @@ export const AccessesGrid = ({
|
||||
}, [data?.count, pageSize, setPagesCount]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
$padding={{ bottom: 'small' }}
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
$css={`
|
||||
<Box $padding={{ horizontal: 'big' }}>
|
||||
<Card
|
||||
size="full"
|
||||
$overflow="auto"
|
||||
$css={`
|
||||
& .c__pagination__goto {
|
||||
display: none;
|
||||
}
|
||||
@@ -122,56 +122,57 @@ export const AccessesGrid = ({
|
||||
text-align: right;
|
||||
}
|
||||
`}
|
||||
aria-label={t('Accesses list card')}
|
||||
>
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
aria-label={t('Accesses list card')}
|
||||
>
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
|
||||
<DataGrid
|
||||
columns={[
|
||||
{
|
||||
id: 'icon-user',
|
||||
renderCell() {
|
||||
return (
|
||||
<Box $direction="row" $align="center">
|
||||
<IconUser
|
||||
aria-label={t('Access icon')}
|
||||
width={20}
|
||||
height={20}
|
||||
color={colorsTokens()['primary-600']}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
<DataGrid
|
||||
columns={[
|
||||
{
|
||||
id: 'icon-user',
|
||||
renderCell() {
|
||||
return (
|
||||
<Box $direction="row" $align="center">
|
||||
<IconUser
|
||||
aria-label={t('Access icon')}
|
||||
width={20}
|
||||
height={20}
|
||||
color={colorsTokens()['primary-500']}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Names'),
|
||||
field: 'user.name',
|
||||
},
|
||||
{
|
||||
field: 'user.email',
|
||||
headerName: t('Emails'),
|
||||
},
|
||||
{
|
||||
field: 'localizedRole',
|
||||
headerName: t('Roles'),
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => (
|
||||
<AccessAction
|
||||
mailDomain={mailDomain}
|
||||
access={row}
|
||||
currentRole={currentRole}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
rows={accesses}
|
||||
isLoading={isLoading}
|
||||
pagination={pagination}
|
||||
onSortModelChange={setSortModel}
|
||||
sortModel={sortModel}
|
||||
/>
|
||||
</Card>
|
||||
{
|
||||
headerName: t('Names'),
|
||||
field: 'user.name',
|
||||
},
|
||||
{
|
||||
field: 'user.email',
|
||||
headerName: t('Emails'),
|
||||
},
|
||||
{
|
||||
field: 'localizedRole',
|
||||
headerName: t('Roles'),
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => (
|
||||
<AccessAction
|
||||
mailDomain={mailDomain}
|
||||
access={row}
|
||||
currentRole={currentRole}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
rows={accesses}
|
||||
isLoading={isLoading}
|
||||
pagination={pagination}
|
||||
onSortModelChange={setSortModel}
|
||||
sortModel={sortModel}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,6 @@ export const ItemList = () => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
$margin={{ top: 'none' }}
|
||||
$padding="none"
|
||||
role="listbox"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user