🎉(all) bootstrap the Calendars project
This repository was forked from Drive in late December 2025 and boostraped as a minimal demo of backend+caldav server+frontend integration. There is much left to do and to fix!
29
src/frontend/.dockerignore
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
# System-specific files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Docker
|
||||
compose.*
|
||||
env.d
|
||||
|
||||
# Docs
|
||||
docs
|
||||
*.md
|
||||
*.log
|
||||
|
||||
# Development/test cache & configurations
|
||||
data
|
||||
.cache
|
||||
.circleci
|
||||
.git
|
||||
.vscode
|
||||
.iml
|
||||
.idea
|
||||
db.sqlite3
|
||||
.mypy_cache
|
||||
.pylint.d
|
||||
.pytest_cache
|
||||
|
||||
# Frontend
|
||||
node_modules
|
||||
72
src/frontend/Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
||||
FROM node:22-alpine AS frontend-deps
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
COPY ./package.json ./package.json
|
||||
COPY ./yarn.lock ./yarn.lock
|
||||
COPY ./apps/calendars/package.json ./apps/calendars/package.json
|
||||
COPY ./packages/open-calendar/package.json ./packages/open-calendar/package.json
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY .dockerignore ./.dockerignore
|
||||
# COPY ./.prettierrc.js ./.prettierrc.js
|
||||
#COPY ./packages/eslint-config-calendars ./packages/eslint-config-calendars
|
||||
COPY ./packages/open-calendar ./packages/open-calendar
|
||||
COPY ./apps/calendars ./apps/calendars
|
||||
|
||||
# Build open-calendar package
|
||||
WORKDIR /home/frontend/packages/open-calendar
|
||||
RUN yarn build
|
||||
WORKDIR /home/frontend
|
||||
|
||||
### ---- Front-end builder image ----
|
||||
FROM frontend-deps AS calendars
|
||||
|
||||
WORKDIR /home/frontend/apps/calendars
|
||||
|
||||
FROM frontend-deps AS calendars-dev
|
||||
|
||||
WORKDIR /home/frontend/apps/calendars
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
RUN yarn build-theme
|
||||
|
||||
# Build open-calendar package if dist doesn't exist, then start dev server
|
||||
CMD ["/bin/sh", "-c", "cd /home/frontend/packages/open-calendar && ([ -d dist ] || yarn build) && cd /home/frontend/apps/calendars && yarn dev"]
|
||||
|
||||
# Tilt will rebuild calendars target so, we dissociate calendars and calendars-builder
|
||||
# to avoid rebuilding the app at every changes.
|
||||
FROM calendars AS calendars-builder
|
||||
|
||||
WORKDIR /home/frontend/apps/calendars
|
||||
|
||||
ARG API_ORIGIN
|
||||
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
USER root
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
COPY --from=calendars-builder \
|
||||
/home/frontend/apps/calendars/out \
|
||||
/usr/share/nginx/html
|
||||
|
||||
COPY ./apps/calendars/conf/default.conf /etc/nginx/conf.d
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
1
src/frontend/apps/calendars/.env
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=
|
||||
1
src/frontend/apps/calendars/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8921
|
||||
41
src/frontend/apps/calendars/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
40
src/frontend/apps/calendars/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.
|
||||
2
src/frontend/apps/calendars/__mocks__/fileMock.js
Normal file
@@ -0,0 +1,2 @@
|
||||
module.exports = "test-file-stub";
|
||||
|
||||
26
src/frontend/apps/calendars/conf/default.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 8080;
|
||||
listen 3000;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
add_header X-Frame-Options DENY always;
|
||||
|
||||
location / {
|
||||
try_files $uri index.html $uri/ =404;
|
||||
}
|
||||
|
||||
location ~ "^/401/?$" {
|
||||
try_files $uri /401.html;
|
||||
}
|
||||
|
||||
location ~ "^/403/?$" {
|
||||
try_files $uri /403.html;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
}
|
||||
411
src/frontend/apps/calendars/cunningham.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import { cunninghamConfig } from "@gouvfr-lasuite/ui-kit";
|
||||
|
||||
// TODO: Temporary solution to override the default button tertiary text color, waiting for the new ui-kit to be released
|
||||
|
||||
/**
|
||||
* Deep merge function that recursively merges objects
|
||||
* @param target - The target object to merge into
|
||||
* @param source - The source object to merge from
|
||||
* @returns A new object with deeply merged properties
|
||||
*
|
||||
* @example
|
||||
* const obj1 = { a: { x: 1, y: 2 }, b: 3 };
|
||||
* const obj2 = { a: { y: 3, z: 4 }, c: 5 };
|
||||
* const merged = deepMerge(obj1, obj2);
|
||||
* // Result: { a: { x: 1, y: 3, z: 4 }, b: 3, c: 5 }
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function deepMerge(target: any, source: any): any {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = result[key];
|
||||
|
||||
if (
|
||||
sourceValue &&
|
||||
typeof sourceValue === "object" &&
|
||||
!Array.isArray(sourceValue) &&
|
||||
targetValue &&
|
||||
typeof targetValue === "object" &&
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
// Recursively merge nested objects
|
||||
result[key] = deepMerge(targetValue, sourceValue);
|
||||
} else if (sourceValue !== undefined) {
|
||||
// Override with source value
|
||||
result[key] = sourceValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const themesImages = {
|
||||
anct: {
|
||||
favicon: "/assets/anct_favicon.png",
|
||||
logo: "/assets/anct_logo_beta.svg",
|
||||
"logo-icon": "/assets/anct_logo-icon.svg",
|
||||
},
|
||||
dark: {
|
||||
favicon: "/assets/favicon.png",
|
||||
logo: "/assets/logo_beta.svg",
|
||||
"logo-icon": "/assets/logo-icon_beta.svg",
|
||||
},
|
||||
default: {
|
||||
favicon: "/assets/favicon.png",
|
||||
logo: "/assets/logo_beta.svg",
|
||||
"logo-icon": "/assets/logo-icon_beta.svg",
|
||||
},
|
||||
};
|
||||
|
||||
const themesGaufre = {
|
||||
anct: {
|
||||
widgetPath: "https://static.suite.anct.gouv.fr/widgets/lagaufre.js",
|
||||
apiUrl:
|
||||
"https://operateurs.suite.anct.gouv.fr/api/v1.0/lagaufre/services/?operator=9f5624fc-ef99-4d10-ae3f-403a81eb16ef&siret=21870030000013",
|
||||
},
|
||||
dark: {
|
||||
widgetPath: "https://static.suite.anct.gouv.fr/widgets/lagaufre.js",
|
||||
apiUrl: "https://lasuite.numerique.gouv.fr/api/services",
|
||||
},
|
||||
default: {
|
||||
widgetPath: "https://static.suite.anct.gouv.fr/widgets/lagaufre.js",
|
||||
apiUrl: "https://lasuite.numerique.gouv.fr/api/services",
|
||||
},
|
||||
};
|
||||
|
||||
const getComponents = (theme: keyof typeof themesImages) => {
|
||||
return {
|
||||
datagrid: {
|
||||
"body--background-color-hover":
|
||||
"ref(contextuals.background.semantic.contextual.primary)",
|
||||
},
|
||||
gaufre: {
|
||||
widgetPath: `'${themesGaufre[theme].widgetPath}'`,
|
||||
apiUrl: `'${themesGaufre[theme].apiUrl}'`,
|
||||
},
|
||||
favicon: {
|
||||
src: `'${themesImages[theme].favicon}'`,
|
||||
},
|
||||
logo: {
|
||||
src: `url('${themesImages[theme].logo}')`,
|
||||
},
|
||||
"logo-icon": {
|
||||
src: `url('${themesImages[theme]["logo-icon"]}')`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const defaultConfig = deepMerge(cunninghamConfig, {
|
||||
themes: {
|
||||
anct: {
|
||||
globals: {
|
||||
colors: {
|
||||
"brand-050": "#EEF0F9",
|
||||
"brand-100": "#DDE2F5",
|
||||
"brand-150": "#CBD4F1",
|
||||
"brand-200": "#B8C6F0",
|
||||
"brand-250": "#A5B7F2",
|
||||
"brand-300": "#91A8F7",
|
||||
"brand-350": "#7C98FE",
|
||||
"brand-400": "#6A89FF",
|
||||
"brand-450": "#5A7AFB",
|
||||
"brand-500": "#4B6BF0",
|
||||
"brand-550": "#3E5CE7",
|
||||
"brand-600": "#3352D5",
|
||||
"brand-650": "#2A47C0",
|
||||
"brand-700": "#2340A3",
|
||||
"brand-750": "#223A7F",
|
||||
"brand-800": "#1F325F",
|
||||
"brand-850": "#1B2845",
|
||||
"brand-900": "#151E30",
|
||||
"brand-950": "#0D121D",
|
||||
"gray-000": "#FFFFFF",
|
||||
"gray-025": "#F6F8FA",
|
||||
"gray-050": "#ECF1F7",
|
||||
"gray-100": "#DDE3F3",
|
||||
"gray-150": "#CCD4EA",
|
||||
"gray-200": "#BCC7E0",
|
||||
"gray-250": "#AEB9D2",
|
||||
"gray-300": "#A1ABC4",
|
||||
"gray-350": "#949EB6",
|
||||
"gray-400": "#8791A9",
|
||||
"gray-450": "#7A849B",
|
||||
"gray-500": "#6D778E",
|
||||
"gray-550": "#616A81",
|
||||
"gray-600": "#555E75",
|
||||
"gray-650": "#495268",
|
||||
"gray-700": "#3E475C",
|
||||
"gray-750": "#333B50",
|
||||
"gray-800": "#283044",
|
||||
"gray-850": "#1E2539",
|
||||
"gray-900": "#171B28",
|
||||
"gray-950": "#0F1118",
|
||||
"gray-1000": "#000000",
|
||||
"info-050": "#E9F2F8",
|
||||
"info-100": "#D2E5F1",
|
||||
"info-150": "#C0D7F0",
|
||||
"info-200": "#AEC8F0",
|
||||
"info-250": "#9EB9F2",
|
||||
"info-300": "#8DABEE",
|
||||
"info-350": "#7B9DE9",
|
||||
"info-400": "#6E8FDB",
|
||||
"info-450": "#6282CD",
|
||||
"info-500": "#5575BF",
|
||||
"info-550": "#4968B1",
|
||||
"info-600": "#3E5CA3",
|
||||
"info-650": "#344F97",
|
||||
"info-700": "#294389",
|
||||
"info-750": "#243972",
|
||||
"info-800": "#1D2F5B",
|
||||
"info-850": "#1A2744",
|
||||
"info-900": "#151D2F",
|
||||
"info-950": "#0E131F",
|
||||
"success-050": "#E7F1E9",
|
||||
"success-100": "#CFE3D3",
|
||||
"success-150": "#B9D8C0",
|
||||
"success-200": "#A1CEAC",
|
||||
"success-250": "#85C496",
|
||||
"success-300": "#63BC7F",
|
||||
"success-350": "#45B16B",
|
||||
"success-400": "#1CA659",
|
||||
"success-450": "#00984C",
|
||||
"success-500": "#008A3F",
|
||||
"success-550": "#007C32",
|
||||
"success-600": "#006E24",
|
||||
"success-650": "#016016",
|
||||
"success-700": "#005305",
|
||||
"success-750": "#0D450A",
|
||||
"success-800": "#11380E",
|
||||
"success-850": "#132A11",
|
||||
"success-900": "#101E0F",
|
||||
"success-950": "#091209",
|
||||
"warning-050": "#F6F0E8",
|
||||
"warning-100": "#EDE2D1",
|
||||
"warning-150": "#E6D3B8",
|
||||
"warning-200": "#E3C39F",
|
||||
"warning-250": "#E3B082",
|
||||
"warning-300": "#E19E5C",
|
||||
"warning-350": "#D98E3F",
|
||||
"warning-400": "#CF7D19",
|
||||
"warning-450": "#C17000",
|
||||
"warning-500": "#B36300",
|
||||
"warning-550": "#A45600",
|
||||
"warning-600": "#964900",
|
||||
"warning-650": "#893C00",
|
||||
"warning-700": "#7B2F00",
|
||||
"warning-750": "#68270D",
|
||||
"warning-800": "#562013",
|
||||
"warning-850": "#411D18",
|
||||
"warning-900": "#2E1714",
|
||||
"warning-950": "#1D0F0D",
|
||||
"error-050": "#F9EFEC",
|
||||
"error-100": "#F4DFD9",
|
||||
"error-150": "#F0CEC6",
|
||||
"error-200": "#EEBCB2",
|
||||
"error-250": "#EEA99D",
|
||||
"error-300": "#EF9487",
|
||||
"error-350": "#F37C6E",
|
||||
"error-400": "#F65F53",
|
||||
"error-450": "#EF443C",
|
||||
"error-500": "#E0342E",
|
||||
"error-550": "#D0201F",
|
||||
"error-600": "#C0000C",
|
||||
"error-650": "#AA0000",
|
||||
"error-700": "#910C06",
|
||||
"error-750": "#731E16",
|
||||
"error-800": "#58201A",
|
||||
"error-850": "#411D18",
|
||||
"error-900": "#2E1714",
|
||||
"error-950": "#1D0F0D",
|
||||
"red-050": "#F9EFEC",
|
||||
"red-100": "#F4DEDA",
|
||||
"red-150": "#F0CDC9",
|
||||
"red-200": "#EEBBB6",
|
||||
"red-250": "#EEA8A2",
|
||||
"red-300": "#F0938D",
|
||||
"red-350": "#EC7E78",
|
||||
"red-400": "#E46D67",
|
||||
"red-450": "#D95B58",
|
||||
"red-500": "#CA4E4B",
|
||||
"red-550": "#BB403F",
|
||||
"red-600": "#AC3233",
|
||||
"red-650": "#9D2227",
|
||||
"red-700": "#882023",
|
||||
"red-750": "#721D1B",
|
||||
"red-800": "#58201A",
|
||||
"red-850": "#401D18",
|
||||
"red-900": "#2E1714",
|
||||
"red-950": "#1D0F0D",
|
||||
"orange-050": "#F8F0E9",
|
||||
"orange-100": "#F1E0D3",
|
||||
"orange-150": "#ECD0BD",
|
||||
"orange-200": "#E9C0A5",
|
||||
"orange-250": "#E8AE8A",
|
||||
"orange-300": "#EB9870",
|
||||
"orange-350": "#EB845A",
|
||||
"orange-400": "#E66E37",
|
||||
"orange-450": "#DD5B16",
|
||||
"orange-500": "#CE4D00",
|
||||
"orange-550": "#BF3E00",
|
||||
"orange-600": "#B02F00",
|
||||
"orange-650": "#A11E00",
|
||||
"orange-700": "#8A1E14",
|
||||
"orange-750": "#731E16",
|
||||
"orange-800": "#58201A",
|
||||
"orange-850": "#401D18",
|
||||
"orange-900": "#2E1714",
|
||||
"orange-950": "#1D0F0D",
|
||||
"brown-050": "#F5F0E8",
|
||||
"brown-100": "#ECE2D1",
|
||||
"brown-150": "#E9D1B9",
|
||||
"brown-200": "#E3C19D",
|
||||
"brown-250": "#DCB187",
|
||||
"brown-300": "#D2A26F",
|
||||
"brown-350": "#C49562",
|
||||
"brown-400": "#B68855",
|
||||
"brown-450": "#A97B48",
|
||||
"brown-500": "#9B6E3B",
|
||||
"brown-550": "#8E612F",
|
||||
"brown-600": "#815521",
|
||||
"brown-650": "#744913",
|
||||
"brown-700": "#673D00",
|
||||
"brown-750": "#5A3100",
|
||||
"brown-800": "#4E2600",
|
||||
"brown-850": "#3D1F0B",
|
||||
"brown-900": "#2C170F",
|
||||
"brown-950": "#1C0F0B",
|
||||
"yellow-050": "#F3F0E7",
|
||||
"yellow-100": "#E9E2CF",
|
||||
"yellow-150": "#E0D4B7",
|
||||
"yellow-200": "#DAC59A",
|
||||
"yellow-250": "#D5B67A",
|
||||
"yellow-300": "#D0A559",
|
||||
"yellow-350": "#CC9331",
|
||||
"yellow-400": "#C48400",
|
||||
"yellow-450": "#B77600",
|
||||
"yellow-500": "#AA6800",
|
||||
"yellow-550": "#9D5A00",
|
||||
"yellow-600": "#914D00",
|
||||
"yellow-650": "#843F00",
|
||||
"yellow-700": "#773200",
|
||||
"yellow-750": "#6A2601",
|
||||
"yellow-800": "#56210F",
|
||||
"yellow-850": "#401D16",
|
||||
"yellow-900": "#2E1714",
|
||||
"yellow-950": "#1D0F0D",
|
||||
"green-050": "#E3F1EF",
|
||||
"green-100": "#CAE5E1",
|
||||
"green-150": "#B0DBD4",
|
||||
"green-200": "#91D1C7",
|
||||
"green-250": "#6AC8BC",
|
||||
"green-300": "#4DBCAF",
|
||||
"green-350": "#3CAFA2",
|
||||
"green-400": "#2AA194",
|
||||
"green-450": "#109487",
|
||||
"green-500": "#00867A",
|
||||
"green-550": "#00786D",
|
||||
"green-600": "#016A60",
|
||||
"green-650": "#015D53",
|
||||
"green-700": "#005047",
|
||||
"green-750": "#00443B",
|
||||
"green-800": "#00382F",
|
||||
"green-850": "#002C25",
|
||||
"green-900": "#041F1A",
|
||||
"green-950": "#041310",
|
||||
"blue1-050": "#EAF2F9",
|
||||
"blue1-100": "#D4E4F3",
|
||||
"blue1-150": "#BFD7F0",
|
||||
"blue1-200": "#AAC9EF",
|
||||
"blue1-250": "#96BBF1",
|
||||
"blue1-300": "#82ACF6",
|
||||
"blue1-350": "#709BFE",
|
||||
"blue1-400": "#608BFF",
|
||||
"blue1-450": "#537BFB",
|
||||
"blue1-500": "#476DEC",
|
||||
"blue1-550": "#3C60DD",
|
||||
"blue1-600": "#3252CF",
|
||||
"blue1-650": "#2B48B9",
|
||||
"blue1-700": "#28409B",
|
||||
"blue1-750": "#24397E",
|
||||
"blue1-800": "#223260",
|
||||
"blue1-850": "#1F2A48",
|
||||
"blue1-900": "#191F32",
|
||||
"blue1-950": "#111320",
|
||||
"blue2-050": "#E5F2F3",
|
||||
"blue2-100": "#CDE6EC",
|
||||
"blue2-150": "#B7D9EA",
|
||||
"blue2-200": "#9BCDE7",
|
||||
"blue2-250": "#84C1E0",
|
||||
"blue2-300": "#6BB4D8",
|
||||
"blue2-350": "#5CA6CB",
|
||||
"blue2-400": "#4D99BC",
|
||||
"blue2-450": "#3E8CAE",
|
||||
"blue2-500": "#2F7FA2",
|
||||
"blue2-550": "#1F7295",
|
||||
"blue2-600": "#056688",
|
||||
"blue2-650": "#00597B",
|
||||
"blue2-700": "#004C6C",
|
||||
"blue2-750": "#003F5E",
|
||||
"blue2-800": "#003353",
|
||||
"blue2-850": "#0C273E",
|
||||
"blue2-900": "#0E1C2C",
|
||||
"blue2-950": "#0A121C",
|
||||
"purple-050": "#EDF1FA",
|
||||
"purple-100": "#DCE2F5",
|
||||
"purple-150": "#CCD4F1",
|
||||
"purple-200": "#BCC4F0",
|
||||
"purple-250": "#ADB6F2",
|
||||
"purple-300": "#9EA6F6",
|
||||
"purple-350": "#8E95FD",
|
||||
"purple-400": "#8083FF",
|
||||
"purple-450": "#7173FF",
|
||||
"purple-500": "#6665F1",
|
||||
"purple-550": "#5B57E2",
|
||||
"purple-600": "#5049D4",
|
||||
"purple-650": "#4641BC",
|
||||
"purple-700": "#3D39A2",
|
||||
"purple-750": "#363680",
|
||||
"purple-800": "#2E3162",
|
||||
"purple-850": "#242848",
|
||||
"purple-900": "#1C1E32",
|
||||
"purple-950": "#121320",
|
||||
"pink-050": "#F8EFF4",
|
||||
"pink-100": "#F0DEE9",
|
||||
"pink-150": "#EBCDDF",
|
||||
"pink-200": "#E7BDD6",
|
||||
"pink-250": "#E5A9CC",
|
||||
"pink-300": "#E695C0",
|
||||
"pink-350": "#EA7CAE",
|
||||
"pink-400": "#E4659F",
|
||||
"pink-450": "#DD4F93",
|
||||
"pink-500": "#CD4085",
|
||||
"pink-550": "#BE3279",
|
||||
"pink-600": "#AE216D",
|
||||
"pink-650": "#9B195D",
|
||||
"pink-700": "#86164E",
|
||||
"pink-750": "#6E1B3D",
|
||||
"pink-800": "#551E31",
|
||||
"pink-850": "#3F1C24",
|
||||
"pink-900": "#2D161A",
|
||||
"pink-950": "#1C0E10",
|
||||
},
|
||||
},
|
||||
components: getComponents("anct"),
|
||||
},
|
||||
dark: {
|
||||
globals: cunninghamConfig.themes.default.globals,
|
||||
components: getComponents("dark"),
|
||||
},
|
||||
default: {
|
||||
components: getComponents("default"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const config = defaultConfig;
|
||||
|
||||
export default config;
|
||||
22
src/frontend/apps/calendars/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
39
src/frontend/apps/calendars/jest.config.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Config } from "jest";
|
||||
import { pathsToModuleNameMapper } from "ts-jest";
|
||||
import tsconfig from "./tsconfig.json";
|
||||
|
||||
const config: Config = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/__tests__/**/*.test.ts", "**/__tests__/**/*.test.tsx"],
|
||||
moduleNameMapper: {
|
||||
// Handle static assets FIRST (before path aliases)
|
||||
"\\.(css|less|scss|sass|svg|png|jpg|jpeg|gif)$":
|
||||
"<rootDir>/__mocks__/fileMock.js",
|
||||
// Then handle path aliases
|
||||
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: {
|
||||
jsx: "react",
|
||||
moduleResolution: "node",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$))"],
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "svg"],
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/**/__tests__/**",
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
src/frontend/apps/calendars/next.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { NextConfig } from "next";
|
||||
import path from "path";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
reactStrictMode: false,
|
||||
webpack: (config, { isServer }) => {
|
||||
// Resolve workspace packages
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
"open-dav-calendar": path.resolve(
|
||||
__dirname,
|
||||
"../../packages/open-calendar/dist/index.js"
|
||||
),
|
||||
};
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
54
src/frontend/apps/calendars/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "calendars",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"build-theme": "cunningham -g css,scss,ts -o src/styles && mv src/styles/cunningham-tokens.scss src/styles/cunningham-tokens-sass.scss",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0 <25.0.0",
|
||||
"yarn": "1.22.22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.4",
|
||||
"@openfun/cunningham-react": "4.0.0",
|
||||
"open-dav-calendar": "*",
|
||||
"date-fns": "4.1.0",
|
||||
"@tanstack/react-query": "5.90.10",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@viselect/react": "3.9.0",
|
||||
"clsx": "2.1.1",
|
||||
"i18next": "25.6.2",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"next": "15.4.9",
|
||||
"next-i18next": "15.4.2",
|
||||
"pretty-bytes": "7.1.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-dropzone": "14.3.8",
|
||||
"react-hook-form": "7.66.0",
|
||||
"react-i18next": "16.3.3",
|
||||
"react-toastify": "11.0.5",
|
||||
"sass": "1.94.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/react-query-devtools": "5.66.9",
|
||||
"@eslint/eslintrc": "3.2.0",
|
||||
"@tanstack/eslint-plugin-query": "5.66.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/react": "19.2.5",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"eslint": "9.20.1",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"jest": "29.7.0",
|
||||
"ts-jest": "29.2.5",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
BIN
src/frontend/apps/calendars/public/assets/401-background.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
src/frontend/apps/calendars/public/assets/403-background.png
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
src/frontend/apps/calendars/public/assets/anct_favicon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
16
src/frontend/apps/calendars/public/assets/anct_logo-icon.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.6765 9.72627H8.41971C6.23248 9.72627 4.03303 11.4473 3.50711 13.5704L2 19.6543V7.84882C2 6.57794 2.29141 5.62077 2.87422 4.97729C3.46451 4.32576 4.28269 4 5.32877 4H8.50063C8.88917 4 9.22541 4.02815 9.50935 4.08446C9.79328 4.14076 10.0548 4.23728 10.2939 4.37402C10.533 4.50272 10.7833 4.6837 11.0448 4.91696L11.6837 5.48403C11.9975 5.74946 12.2927 5.93848 12.5691 6.05109C12.8456 6.1637 13.1856 6.22001 13.5891 6.22001H24.0125C25.2155 6.22001 26.1271 6.55381 26.7472 7.22142C27.3035 7.81312 27.6133 8.64807 27.6765 9.72627Z" fill="#FBC63A"/>
|
||||
<path d="M5.67656 27C4.46609 27 3.6366 26.6536 3.18806 25.9607C2.73746 25.2761 2.6776 24.266 3.00848 22.9303L5.32709 13.5705C5.61138 12.4229 6.80027 11.4926 7.98256 11.4926H29.3898C30.5721 11.4926 31.3001 12.4229 31.0158 13.5705L28.6972 22.9303C28.3663 24.266 27.8172 25.2761 27.0499 25.9607C26.2805 26.6536 25.3615 27 24.293 27H5.67656Z" fill="#2845C1"/>
|
||||
<rect x="4" y="23" width="24" height="9" rx="4.5" fill="#ECF1F7"/>
|
||||
<rect x="4" y="23" width="24" height="9" rx="4.5" fill="url(#paint0_linear_1156_814)" fill-opacity="0.1"/>
|
||||
<path d="M8.34245 29.7362C8.29257 29.7362 8.26763 29.7362 8.24858 29.7265C8.23182 29.718 8.2182 29.7044 8.20966 29.6876C8.19995 29.6686 8.19995 29.6436 8.19995 29.5937V25.2425C8.19995 25.1926 8.19995 25.1677 8.20966 25.1486C8.2182 25.1318 8.23182 25.1182 8.24858 25.1097C8.26763 25.1 8.29257 25.1 8.34245 25.1H9.56434C10.5313 25.1 11.1208 25.5768 11.1208 26.3584C11.1208 26.648 11.0195 26.9064 10.8257 27.1068C10.7572 27.1777 10.7229 27.2131 10.7171 27.2364C10.7109 27.2615 10.7123 27.274 10.7237 27.2972C10.7343 27.3187 10.7739 27.3451 10.8532 27.3978C11.1981 27.6275 11.3857 27.9699 11.3857 28.3719C11.3857 29.2196 10.73 29.7362 9.66369 29.7362H8.34245ZM9.58421 25.8815H9.28295C9.23307 25.8815 9.20813 25.8815 9.18908 25.8912C9.17232 25.8998 9.1587 25.9134 9.15016 25.9301C9.14045 25.9492 9.14045 25.9741 9.14045 26.024V26.7855C9.14045 26.8354 9.14045 26.8603 9.15016 26.8794C9.1587 26.8961 9.17232 26.9097 9.18908 26.9183C9.20813 26.928 9.23307 26.928 9.28295 26.928H9.58421C9.94186 26.928 10.1538 26.7359 10.1538 26.3981C10.1538 26.0736 9.94186 25.8815 9.58421 25.8815ZM9.71667 27.7095H9.28295C9.23307 27.7095 9.20813 27.7095 9.18908 27.7192C9.17232 27.7278 9.1587 27.7414 9.15016 27.7582C9.14045 27.7772 9.14045 27.8022 9.14045 27.852V28.8122C9.14045 28.8621 9.14045 28.887 9.15016 28.9061C9.1587 28.9228 9.17232 28.9365 9.18908 28.945C9.20813 28.9547 9.23307 28.9547 9.28295 28.9547H9.71667C10.1538 28.9547 10.4187 28.7229 10.4187 28.3321C10.4187 27.9347 10.1538 27.7095 9.71667 27.7095Z" fill="#2845C1"/>
|
||||
<path d="M12.4271 29.7362C12.3772 29.7362 12.3523 29.7362 12.3333 29.7265C12.3165 29.718 12.3029 29.7044 12.2943 29.6876C12.2846 29.6686 12.2846 29.6436 12.2846 29.5937V25.2425C12.2846 25.1926 12.2846 25.1677 12.2943 25.1486C12.3029 25.1318 12.3165 25.1182 12.3333 25.1097C12.3523 25.1 12.3772 25.1 12.4271 25.1H14.8444C14.8943 25.1 14.9192 25.1 14.9383 25.1097C14.955 25.1182 14.9687 25.1318 14.9772 25.1486C14.9869 25.1677 14.9869 25.1926 14.9869 25.2425V25.739C14.9869 25.7889 14.9869 25.8138 14.9772 25.8329C14.9687 25.8496 14.955 25.8633 14.9383 25.8718C14.9192 25.8815 14.8943 25.8815 14.8444 25.8815H13.3676C13.3177 25.8815 13.2928 25.8815 13.2738 25.8912C13.257 25.8998 13.2434 25.9134 13.2348 25.9301C13.2251 25.9492 13.2251 25.9741 13.2251 26.024V26.8385C13.2251 26.8884 13.2251 26.9133 13.2348 26.9323C13.2434 26.9491 13.257 26.9627 13.2738 26.9713C13.2928 26.981 13.3177 26.981 13.3676 26.981H14.5795C14.6294 26.981 14.6543 26.981 14.6733 26.9907C14.6901 26.9992 14.7037 27.0128 14.7123 27.0296C14.722 27.0487 14.722 27.0736 14.722 27.1235V27.62C14.722 27.6699 14.722 27.6948 14.7123 27.7139C14.7037 27.7306 14.6901 27.7443 14.6733 27.7528C14.6543 27.7625 14.6294 27.7625 14.5795 27.7625H13.3676C13.3177 27.7625 13.2928 27.7625 13.2738 27.7722C13.257 27.7808 13.2434 27.7944 13.2348 27.8111C13.2251 27.8302 13.2251 27.8551 13.2251 27.905V28.8122C13.2251 28.8621 13.2251 28.887 13.2348 28.9061C13.2434 28.9228 13.257 28.9365 13.2738 28.945C13.2928 28.9547 13.3177 28.9547 13.3676 28.9547H14.8444C14.8943 28.9547 14.9192 28.9547 14.9383 28.9644C14.955 28.9729 14.9687 28.9866 14.9772 29.0033C14.9869 29.0224 14.9869 29.0473 14.9869 29.0972V29.5937C14.9869 29.6436 14.9869 29.6686 14.9772 29.6876C14.9687 29.7044 14.955 29.718 14.9383 29.7265C14.9192 29.7362 14.8943 29.7362 14.8444 29.7362H12.4271Z" fill="#2845C1"/>
|
||||
<path d="M15.7762 25.9544C15.7263 25.9544 15.7014 25.9544 15.6823 25.9447C15.6656 25.9361 15.652 25.9225 15.6434 25.9057C15.6337 25.8867 15.6337 25.8618 15.6337 25.8119V25.2425C15.6337 25.1926 15.6337 25.1677 15.6434 25.1486C15.652 25.1318 15.6656 25.1182 15.6823 25.1097C15.7014 25.1 15.7263 25.1 15.7762 25.1H19.2929C19.3428 25.1 19.3678 25.1 19.3868 25.1097C19.4036 25.1182 19.4172 25.1318 19.4257 25.1486C19.4354 25.1677 19.4354 25.1926 19.4354 25.2425V25.8119C19.4354 25.8618 19.4354 25.8867 19.4257 25.9057C19.4172 25.9225 19.4036 25.9361 19.3868 25.9447C19.3678 25.9544 19.3428 25.9544 19.2929 25.9544H18.1473C18.0974 25.9544 18.0725 25.9544 18.0535 25.9641C18.0367 25.9726 18.0231 25.9862 18.0145 26.003C18.0048 26.0221 18.0048 26.047 18.0048 26.0969V29.5937C18.0048 29.6436 18.0048 29.6686 17.9951 29.6876C17.9866 29.7044 17.973 29.718 17.9562 29.7265C17.9371 29.7362 17.9122 29.7362 17.8623 29.7362H17.2068C17.1569 29.7362 17.132 29.7362 17.113 29.7265C17.0962 29.718 17.0826 29.7044 17.074 29.6876C17.0643 29.6686 17.0643 29.6436 17.0643 29.5937V26.0969C17.0643 26.047 17.0643 26.0221 17.0546 26.003C17.0461 25.9862 17.0325 25.9726 17.0157 25.9641C16.9966 25.9544 16.9717 25.9544 16.9218 25.9544H15.7762Z" fill="#2845C1"/>
|
||||
<path d="M19.3724 29.7362C19.3031 29.7362 19.2685 29.7362 19.2463 29.7217C19.2268 29.709 19.2131 29.6891 19.2081 29.6664C19.2024 29.6405 19.2146 29.6081 19.2392 29.5433L20.8864 25.192C20.899 25.1588 20.9053 25.1422 20.9161 25.13C20.9257 25.1192 20.9377 25.1109 20.9512 25.1058C20.9665 25.1 20.9842 25.1 21.0197 25.1H22.0548C22.0903 25.1 22.108 25.1 22.1233 25.1058C22.1368 25.1109 22.1488 25.1192 22.1584 25.13C22.1692 25.1422 22.1755 25.1588 22.188 25.192L23.8353 29.5433C23.8598 29.6081 23.8721 29.6405 23.8664 29.6664C23.8613 29.6891 23.8476 29.709 23.8282 29.7217C23.806 29.7362 23.7713 29.7362 23.702 29.7362H23.0076C22.9718 29.7362 22.9539 29.7362 22.9385 29.7304C22.925 29.7252 22.9129 29.7167 22.9033 29.7058C22.8925 29.6934 22.8863 29.6766 22.8739 29.643L22.4988 28.6241C22.4865 28.5905 22.4803 28.5737 22.4694 28.5613C22.4599 28.5503 22.4478 28.5419 22.4342 28.5367C22.4188 28.5308 22.4009 28.5308 22.3651 28.5308H20.7094C20.6736 28.5308 20.6556 28.5308 20.6403 28.5367C20.6267 28.5419 20.6146 28.5503 20.605 28.5613C20.5942 28.5737 20.588 28.5905 20.5757 28.6241L20.2006 29.643C20.1882 29.6766 20.182 29.6934 20.1712 29.7058C20.1616 29.7167 20.1495 29.7252 20.1359 29.7304C20.1206 29.7362 20.1027 29.7362 20.0668 29.7362H19.3724ZM20.9713 27.538C20.9478 27.6024 20.936 27.6346 20.9419 27.6603C20.9471 27.6828 20.9608 27.7024 20.9802 27.715C21.0023 27.7294 21.0366 27.7294 21.1051 27.7294H21.9694C22.0379 27.7294 22.0721 27.7294 22.0943 27.715C22.1136 27.7024 22.1274 27.6828 22.1325 27.6603C22.1385 27.6346 22.1267 27.6024 22.1032 27.538L21.6711 26.3542C21.6298 26.2411 21.6092 26.1846 21.579 26.1686C21.5529 26.1547 21.5216 26.1547 21.4955 26.1686C21.4653 26.1846 21.4447 26.2411 21.4034 26.3542L20.9713 27.538Z" fill="#2845C1"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1156_814" x1="16" y1="23" x2="16" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2845C1"/>
|
||||
<stop offset="1" stop-color="#ECF1F7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="177" height="40" viewBox="0 0 177 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.6765 13.7263H12.4197C10.2325 13.7263 8.03303 15.4473 7.50711 17.5704L6 23.6543V11.8488C6 10.5779 6.29141 9.62077 6.87422 8.97729C7.46451 8.32576 8.28269 8 9.32877 8H12.5006C12.8892 8 13.2254 8.02815 13.5093 8.08446C13.7933 8.14076 14.0548 8.23728 14.2939 8.37402C14.533 8.50272 14.7833 8.6837 15.0448 8.91696L15.6837 9.48403C15.9975 9.74946 16.2927 9.93848 16.5691 10.0511C16.8456 10.1637 17.1856 10.22 17.5891 10.22H28.0125C29.2155 10.22 30.1271 10.5538 30.7472 11.2214C31.3035 11.8131 31.6133 12.6481 31.6765 13.7263Z" fill="#FCC73B"/>
|
||||
<path d="M9.67656 31.0001C8.46609 31.0001 7.6366 30.6537 7.18806 29.9608C6.73746 29.2762 6.6776 28.2661 7.00848 26.9304L9.32709 17.5706C9.61138 16.423 10.8003 15.4927 11.9826 15.4927H33.3898C34.5721 15.4927 35.3001 16.423 35.0158 17.5706L32.6972 26.9304C32.3663 28.2661 31.8172 29.2762 31.0499 29.9608C30.2805 30.6537 29.3615 31.0001 28.293 31.0001H9.67656Z" fill="#000091"/>
|
||||
<path d="M42.266 28V12.6H51.242V15.196H45.39V18.848H50.362V21.444H45.39V28H42.266ZM54.5853 14.976C53.5953 14.976 52.7593 14.14 52.7593 13.15C52.7593 12.16 53.5953 11.324 54.5853 11.324C55.5753 11.324 56.3893 12.16 56.3893 13.15C56.3893 14.14 55.5753 14.976 54.5853 14.976ZM53.1773 28V16.912H55.9713V28H53.1773ZM64.2058 25.8C65.3058 25.8 66.1858 25.294 66.7138 24.524L68.8918 26.196C67.8578 27.56 66.2298 28.44 64.2058 28.44C60.3778 28.44 58.0898 25.69 58.0898 22.456C58.0898 19.222 60.3778 16.472 64.2058 16.472C66.2298 16.472 67.8578 17.352 68.8918 18.716L66.7138 20.388C66.1858 19.618 65.3058 19.112 64.1618 19.112C62.3358 19.112 60.9938 20.52 60.9938 22.456C60.9938 24.414 62.3358 25.8 64.2058 25.8ZM70.5375 28V11.5H73.3315V17.792C74.1015 17.044 75.1355 16.472 76.6095 16.472C79.0075 16.472 80.9215 18.122 80.9215 21.4V28H78.1055V21.51C78.1055 20.036 77.2915 19.112 75.9055 19.112C74.4975 19.112 73.7495 20.058 73.3315 20.762V28H70.5375ZM85.0622 14.976C84.0722 14.976 83.2362 14.14 83.2362 13.15C83.2362 12.16 84.0722 11.324 85.0622 11.324C86.0522 11.324 86.8662 12.16 86.8662 13.15C86.8662 14.14 86.0522 14.976 85.0622 14.976ZM83.6542 28V16.912H86.4482V28H83.6542ZM99.5667 26.196C98.5327 27.582 96.8167 28.44 94.7267 28.44C90.7887 28.44 88.5667 25.69 88.5667 22.456C88.5667 19.178 90.6347 16.472 94.2647 16.472C97.3447 16.472 99.3687 18.562 99.3687 21.466C99.3687 22.082 99.2807 22.654 99.1927 23.028H91.4267C91.6907 25.096 92.9667 25.932 94.7047 25.932C95.9147 25.932 96.9707 25.404 97.5647 24.612L99.5667 26.196ZM94.1987 18.76C92.7687 18.76 91.8227 19.552 91.5147 21.004H96.6187C96.5747 19.882 95.7607 18.76 94.1987 18.76ZM101.659 28V16.912H104.453V18.012C105.179 17.264 106.125 16.692 107.379 16.692C107.753 16.692 108.083 16.758 108.347 16.846V19.596C107.995 19.508 107.621 19.442 107.115 19.442C105.751 19.442 104.871 20.19 104.453 20.894V28H101.659ZM109.188 26.394L111.036 24.722C111.718 25.58 112.532 26.196 113.588 26.196C114.49 26.196 114.952 25.668 114.952 25.008C114.952 23.072 109.65 23.798 109.65 19.904C109.65 17.946 111.3 16.472 113.61 16.472C115.304 16.472 116.844 17.286 117.636 18.364L115.788 19.992C115.216 19.288 114.49 18.716 113.632 18.716C112.752 18.716 112.334 19.2 112.334 19.772C112.334 21.664 117.636 21.004 117.636 24.832C117.592 27.164 115.722 28.44 113.632 28.44C111.652 28.44 110.244 27.648 109.188 26.394Z" fill="#000091"/>
|
||||
<rect x="123" y="12" width="50" height="16" rx="8" fill="#E0E0FF"/>
|
||||
<path d="M129.165 24L132.08 16.3H134.126L137.041 24H135.38L134.643 21.998H131.563L130.826 24H129.165ZM132.047 20.667H134.159L133.103 17.774L132.047 20.667ZM138.12 24V16.3H139.682V22.581H142.608V24H138.12ZM143.873 24V16.3H146.414C148.108 16.3 149.142 17.169 149.142 18.599C149.142 20.018 148.108 20.887 146.414 20.887H145.435V24H143.873ZM146.48 17.598H145.435V19.589H146.48C147.14 19.589 147.536 19.204 147.536 18.577C147.536 17.994 147.14 17.598 146.48 17.598ZM150.324 24V16.3H151.886V19.369H155.769V16.3H157.331V24H155.769V20.788H151.886V24H150.324ZM158.406 24L161.321 16.3H163.367L166.282 24H164.621L163.884 21.998H160.804L160.067 24H158.406ZM161.288 20.667H163.4L162.344 17.774L161.288 20.667Z" fill="#5151BB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
26
src/frontend/apps/calendars/public/assets/anct_logo_beta.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg width="126" height="40" viewBox="0 0 126 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.6765 13.7263H12.4197C10.2325 13.7263 8.03303 15.4473 7.50711 17.5704L6 23.6543V11.8488C6 10.5779 6.29141 9.62077 6.87422 8.97729C7.46451 8.32576 8.28269 8 9.32877 8H12.5006C12.8892 8 13.2254 8.02815 13.5093 8.08446C13.7933 8.14076 14.0548 8.23728 14.2939 8.37402C14.533 8.50272 14.7833 8.6837 15.0448 8.91696L15.6837 9.48403C15.9975 9.74946 16.2927 9.93848 16.5691 10.0511C16.8456 10.1637 17.1856 10.22 17.5891 10.22H28.0125C29.2155 10.22 30.1271 10.5538 30.7472 11.2214C31.3035 11.8131 31.6133 12.6481 31.6765 13.7263Z" fill="#FBC63A"/>
|
||||
<path d="M9.67656 31.0001C8.46609 31.0001 7.6366 30.6537 7.18806 29.9608C6.73746 29.2762 6.6776 28.2661 7.00848 26.9304L9.32709 17.5706C9.61138 16.423 10.8003 15.4927 11.9826 15.4927H33.3898C34.5721 15.4927 35.3001 16.423 35.0158 17.5706L32.6972 26.9304C32.3663 28.2661 31.8172 29.2762 31.0499 29.9608C30.2805 30.6537 29.3615 31.0001 28.293 31.0001H9.67656Z" fill="#2845C1"/>
|
||||
<rect x="8" y="27" width="24" height="9" rx="4.5" fill="#ECF1F7"/>
|
||||
<rect x="8" y="27" width="24" height="9" rx="4.5" fill="url(#paint0_linear_3435_94)" fill-opacity="0.1"/>
|
||||
<path d="M12.3425 33.7364C12.2926 33.7364 12.2676 33.7364 12.2486 33.7267C12.2318 33.7181 12.2182 33.7045 12.2097 33.6877C12.2 33.6687 12.2 33.6437 12.2 33.5939V29.2426C12.2 29.1927 12.2 29.1678 12.2097 29.1487C12.2182 29.132 12.2318 29.1183 12.2486 29.1098C12.2676 29.1001 12.2926 29.1001 12.3425 29.1001H13.5643C14.5313 29.1001 15.1208 29.577 15.1208 30.3585C15.1208 30.6481 15.0195 30.9066 14.8257 31.1069C14.7572 31.1778 14.7229 31.2132 14.7171 31.2365C14.7109 31.2616 14.7123 31.2741 14.7237 31.2973C14.7343 31.3188 14.7739 31.3452 14.8532 31.398C15.1981 31.6276 15.3857 31.9701 15.3857 32.372C15.3857 33.2198 14.73 33.7364 13.6637 33.7364H12.3425ZM13.5842 29.8816H13.283C13.2331 29.8816 13.2081 29.8816 13.1891 29.8913C13.1723 29.8999 13.1587 29.9135 13.1502 29.9303C13.1405 29.9493 13.1405 29.9743 13.1405 30.0241V30.7856C13.1405 30.8355 13.1405 30.8604 13.1502 30.8795C13.1587 30.8962 13.1723 30.9099 13.1891 30.9184C13.2081 30.9281 13.2331 30.9281 13.283 30.9281H13.5842C13.9419 30.9281 14.1538 30.736 14.1538 30.3983C14.1538 30.0737 13.9419 29.8816 13.5842 29.8816ZM13.7167 31.7097H13.283C13.2331 31.7097 13.2081 31.7097 13.1891 31.7194C13.1723 31.7279 13.1587 31.7415 13.1502 31.7583C13.1405 31.7773 13.1405 31.8023 13.1405 31.8522V32.8123C13.1405 32.8622 13.1405 32.8871 13.1502 32.9062C13.1587 32.923 13.1723 32.9366 13.1891 32.9451C13.2081 32.9548 13.2331 32.9548 13.283 32.9548H13.7167C14.1538 32.9548 14.4187 32.723 14.4187 32.3322C14.4187 31.9348 14.1538 31.7097 13.7167 31.7097Z" fill="#2845C1"/>
|
||||
<path d="M16.4271 33.7364C16.3772 33.7364 16.3523 33.7364 16.3333 33.7267C16.3165 33.7181 16.3029 33.7045 16.2943 33.6877C16.2846 33.6687 16.2846 33.6437 16.2846 33.5939V29.2426C16.2846 29.1927 16.2846 29.1678 16.2943 29.1487C16.3029 29.132 16.3165 29.1183 16.3333 29.1098C16.3523 29.1001 16.3772 29.1001 16.4271 29.1001H18.8444C18.8943 29.1001 18.9192 29.1001 18.9383 29.1098C18.955 29.1183 18.9687 29.132 18.9772 29.1487C18.9869 29.1678 18.9869 29.1927 18.9869 29.2426V29.7391C18.9869 29.789 18.9869 29.814 18.9772 29.833C18.9687 29.8498 18.955 29.8634 18.9383 29.8719C18.9192 29.8816 18.8943 29.8816 18.8444 29.8816H17.3676C17.3177 29.8816 17.2928 29.8816 17.2738 29.8913C17.257 29.8999 17.2434 29.9135 17.2348 29.9303C17.2251 29.9493 17.2251 29.9743 17.2251 30.0241V30.8386C17.2251 30.8885 17.2251 30.9134 17.2348 30.9325C17.2434 30.9492 17.257 30.9629 17.2738 30.9714C17.2928 30.9811 17.3177 30.9811 17.3676 30.9811H18.5795C18.6294 30.9811 18.6543 30.9811 18.6733 30.9908C18.6901 30.9993 18.7037 31.013 18.7123 31.0297C18.722 31.0488 18.722 31.0737 18.722 31.1236V31.6201C18.722 31.67 18.722 31.695 18.7123 31.714C18.7037 31.7308 18.6901 31.7444 18.6733 31.7529C18.6543 31.7626 18.6294 31.7626 18.5795 31.7626H17.3676C17.3177 31.7626 17.2928 31.7626 17.2738 31.7723C17.257 31.7809 17.2434 31.7945 17.2348 31.8113C17.2251 31.8303 17.2251 31.8553 17.2251 31.9051V32.8123C17.2251 32.8622 17.2251 32.8871 17.2348 32.9062C17.2434 32.923 17.257 32.9366 17.2738 32.9451C17.2928 32.9548 17.3177 32.9548 17.3676 32.9548H18.8444C18.8943 32.9548 18.9192 32.9548 18.9383 32.9645C18.955 32.9731 18.9687 32.9867 18.9772 33.0035C18.9869 33.0225 18.9869 33.0474 18.9869 33.0973V33.5939C18.9869 33.6437 18.9869 33.6687 18.9772 33.6877C18.9687 33.7045 18.955 33.7181 18.9383 33.7267C18.9192 33.7364 18.8943 33.7364 18.8444 33.7364H16.4271Z" fill="#2845C1"/>
|
||||
<path d="M19.7762 29.9545C19.7263 29.9545 19.7014 29.9545 19.6823 29.9448C19.6656 29.9362 19.652 29.9226 19.6434 29.9059C19.6337 29.8868 19.6337 29.8619 19.6337 29.812V29.2426C19.6337 29.1927 19.6337 29.1678 19.6434 29.1487C19.652 29.132 19.6656 29.1183 19.6823 29.1098C19.7014 29.1001 19.7263 29.1001 19.7762 29.1001H23.2929C23.3428 29.1001 23.3678 29.1001 23.3868 29.1098C23.4036 29.1183 23.4172 29.132 23.4257 29.1487C23.4354 29.1678 23.4354 29.1927 23.4354 29.2426V29.812C23.4354 29.8619 23.4354 29.8868 23.4257 29.9059C23.4172 29.9226 23.4036 29.9362 23.3868 29.9448C23.3678 29.9545 23.3428 29.9545 23.2929 29.9545H22.1473C22.0974 29.9545 22.0725 29.9545 22.0535 29.9642C22.0367 29.9727 22.0231 29.9864 22.0145 30.0031C22.0048 30.0222 22.0048 30.0471 22.0048 30.097V33.5939C22.0048 33.6437 22.0048 33.6687 21.9951 33.6877C21.9866 33.7045 21.973 33.7181 21.9562 33.7267C21.9371 33.7364 21.9122 33.7364 21.8623 33.7364H21.2068C21.1569 33.7364 21.132 33.7364 21.113 33.7267C21.0962 33.7181 21.0826 33.7045 21.074 33.6877C21.0643 33.6687 21.0643 33.6437 21.0643 33.5939V30.097C21.0643 30.0471 21.0643 30.0222 21.0546 30.0031C21.0461 29.9864 21.0325 29.9727 21.0157 29.9642C20.9966 29.9545 20.9717 29.9545 20.9218 29.9545H19.7762Z" fill="#2845C1"/>
|
||||
<path d="M23.3724 33.7364C23.3031 33.7364 23.2685 33.7364 23.2463 33.7218C23.2268 33.7091 23.2131 33.6892 23.2081 33.6666C23.2024 33.6406 23.2146 33.6082 23.2392 33.5434L24.8864 29.1921C24.899 29.159 24.9053 29.1424 24.9161 29.1301C24.9257 29.1193 24.9377 29.111 24.9512 29.1059C24.9665 29.1001 24.9842 29.1001 25.0197 29.1001H26.0548C26.0903 29.1001 26.108 29.1001 26.1233 29.1059C26.1368 29.111 26.1488 29.1193 26.1584 29.1301C26.1692 29.1424 26.1755 29.159 26.188 29.1921L27.8353 33.5434C27.8598 33.6082 27.8721 33.6406 27.8664 33.6666C27.8613 33.6892 27.8476 33.7091 27.8282 33.7218C27.806 33.7364 27.7713 33.7364 27.702 33.7364H27.0076C26.9718 33.7364 26.9539 33.7364 26.9385 33.7305C26.925 33.7253 26.9129 33.7169 26.9033 33.7059C26.8925 33.6935 26.8863 33.6767 26.8739 33.6431L26.4988 32.6242C26.4865 32.5906 26.4803 32.5738 26.4694 32.5614C26.4599 32.5504 26.4478 32.542 26.4342 32.5368C26.4188 32.5309 26.4009 32.5309 26.3651 32.5309H24.7094C24.6736 32.5309 24.6556 32.5309 24.6403 32.5368C24.6267 32.542 24.6146 32.5504 24.605 32.5614C24.5942 32.5738 24.588 32.5906 24.5757 32.6242L24.2006 33.6431C24.1882 33.6767 24.182 33.6935 24.1712 33.7059C24.1616 33.7169 24.1495 33.7253 24.1359 33.7305C24.1206 33.7364 24.1027 33.7364 24.0668 33.7364H23.3724ZM24.9713 31.5382C24.9478 31.6025 24.936 31.6347 24.9419 31.6604C24.9471 31.6829 24.9608 31.7026 24.9802 31.7151C25.0023 31.7295 25.0366 31.7295 25.1051 31.7295H25.9694C26.0379 31.7295 26.0721 31.7295 26.0943 31.7151C26.1136 31.7026 26.1274 31.6829 26.1325 31.6604C26.1385 31.6347 26.1267 31.6025 26.1032 31.5382L25.6711 30.3543C25.6298 30.2413 25.6092 30.1847 25.579 30.1687C25.5529 30.1548 25.5216 30.1548 25.4955 30.1687C25.4653 30.1847 25.4447 30.2413 25.4034 30.3543L24.9713 31.5382Z" fill="#2845C1"/>
|
||||
<path d="M64.9111 15.5364C65.9592 15.5364 66.8937 15.7488 67.7139 16.1741C68.4871 16.5537 69.1304 17.0638 69.6434 17.7037C69.7121 17.7894 69.6942 17.9141 69.6067 17.9804L67.953 19.235C67.8606 19.305 67.7288 19.2818 67.6586 19.1895C67.3841 18.8284 67.0298 18.5295 66.5967 18.2932C66.1106 18.0199 65.5408 17.8831 64.8877 17.8831C64.3563 17.8831 63.8627 17.9816 63.4072 18.179C62.9668 18.3764 62.5866 18.6499 62.2676 18.9993C61.9486 19.3486 61.6979 19.7589 61.5156 20.2297C61.3334 20.6854 61.2422 21.1869 61.2422 21.7336C61.2422 22.4627 61.4018 23.1237 61.7207 23.7161C62.0397 24.2933 62.4727 24.7491 63.0195 25.0833C63.5664 25.4174 64.2045 25.5842 64.9336 25.5842C65.5564 25.5842 66.1106 25.4475 66.5967 25.1741C67.0298 24.9378 67.3841 24.6396 67.6586 24.2787C67.7288 24.1865 67.8606 24.1632 67.953 24.2333L69.6051 25.4866C69.6932 25.5534 69.7106 25.6793 69.6406 25.7649C69.128 26.391 68.4855 26.9003 67.7139 27.2932C66.8937 27.7185 65.9592 27.9318 64.9111 27.9319C63.6199 27.9319 62.5033 27.6502 61.5615 27.0881C60.6197 26.5109 59.8905 25.7516 59.374 24.8098C58.8728 23.868 58.6221 22.8425 58.6221 21.7336C58.6221 20.8984 58.7662 20.1087 59.0547 19.3645C59.3433 18.6202 59.7617 17.9593 60.3086 17.3821C60.8554 16.805 61.5155 16.3563 62.29 16.0374C63.0648 15.7032 63.9389 15.5364 64.9111 15.5364Z" fill="#2845C1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.3818 15.5364C96.1716 15.5364 96.8856 15.665 97.5234 15.9231C98.1614 16.1813 98.7084 16.5388 99.1641 16.9944C99.635 17.4501 99.9923 17.9897 100.235 18.6125C100.478 19.2352 100.6 19.911 100.6 20.6399C100.6 20.9133 100.585 21.1795 100.555 21.4377C100.533 21.6247 100.506 21.7878 100.476 21.927C100.457 22.0152 100.378 22.0754 100.288 22.0754H92.2901C92.174 22.0754 92.082 22.1741 92.0942 22.2896C92.1581 22.8936 92.3041 23.4146 92.5342 23.8528C92.8531 24.4603 93.3008 24.9162 93.8779 25.22C94.4552 25.5238 95.1087 25.676 95.8379 25.676C96.5214 25.676 97.1368 25.5393 97.6836 25.2659C98.1764 25.0261 98.5814 24.716 98.899 24.336C98.9718 24.2488 99.1013 24.2311 99.1901 24.3019L100.677 25.487C100.761 25.5543 100.777 25.6766 100.71 25.7608C100.195 26.4035 99.5428 26.9222 98.7539 27.3167C97.9033 27.7267 96.9387 27.9319 95.8604 27.9319C94.5236 27.9319 93.3842 27.6502 92.4424 27.0881C91.5007 26.5109 90.7795 25.7515 90.2783 24.8098C89.777 23.868 89.5264 22.8425 89.5264 21.7336C89.5264 20.8679 89.6549 20.0628 89.9131 19.3186C90.1865 18.5743 90.5739 17.9207 91.0752 17.3586C91.5764 16.7815 92.1919 16.3339 92.9209 16.0149C93.65 15.6959 94.4704 15.5364 95.3818 15.5364ZM95.3135 17.6096C94.6451 17.6096 94.0599 17.7619 93.5586 18.0657C93.0728 18.3694 92.7005 18.8173 92.4424 19.4094C92.3587 19.5964 92.2895 19.7981 92.2334 20.014C92.2015 20.1366 92.2966 20.2532 92.4234 20.2532H97.9293C98.0423 20.2532 98.1336 20.1596 98.1218 20.0473C98.0839 19.6869 97.9762 19.3455 97.7969 19.0227C97.5843 18.5975 97.2733 18.2555 96.8633 17.9973C96.4531 17.7391 95.9363 17.6096 95.3135 17.6096Z" fill="#2845C1"/>
|
||||
<path d="M114.805 15.5364C115.64 15.5364 116.415 15.7187 117.129 16.0833C117.793 16.4155 118.326 16.8296 118.725 17.3258C118.791 17.4073 118.776 17.5253 118.697 17.5932L117.293 18.791C117.204 18.8665 117.071 18.8504 116.997 18.7601C116.738 18.441 116.44 18.1714 116.104 17.9514C115.709 17.6933 115.276 17.5637 114.805 17.5637C114.471 17.5638 114.182 17.6249 113.939 17.7463C113.712 17.8679 113.537 18.0274 113.415 18.2249C113.309 18.4223 113.256 18.6429 113.256 18.886C113.256 19.2503 113.385 19.5542 113.643 19.7971C113.916 20.025 114.266 20.2228 114.691 20.3899C115.117 20.5569 115.564 20.7394 116.035 20.9368C116.521 21.1191 116.977 21.3469 117.402 21.6204C117.828 21.8786 118.17 22.2277 118.428 22.6682C118.701 23.0934 118.838 23.6324 118.838 24.2854C118.838 25.0599 118.648 25.721 118.269 26.2678C117.904 26.7993 117.418 27.2097 116.811 27.4983C116.203 27.7869 115.534 27.9319 114.805 27.9319C113.818 27.9319 112.959 27.7494 112.23 27.385C111.559 27.0351 110.964 26.575 110.447 26.0057C110.373 25.9247 110.382 25.7996 110.465 25.7273L111.868 24.4922C111.954 24.4168 112.085 24.4286 112.159 24.515C112.49 24.8982 112.855 25.2171 113.256 25.4709C113.711 25.7594 114.22 25.9036 114.782 25.9036C115.314 25.9036 115.717 25.7668 115.99 25.4934C116.279 25.2049 116.423 24.8555 116.423 24.4456C116.423 24.0659 116.286 23.7623 116.013 23.5344C115.754 23.3066 115.413 23.1087 114.987 22.9417C114.562 22.7746 114.106 22.5995 113.62 22.4172C113.149 22.2198 112.701 21.9846 112.275 21.7112C111.85 21.4378 111.501 21.0885 111.228 20.6633C110.969 20.2228 110.84 19.6604 110.84 18.9768C110.84 18.3388 111.008 17.7618 111.342 17.2454C111.676 16.7137 112.139 16.2953 112.731 15.9915C113.339 15.6877 114.03 15.5364 114.805 15.5364Z" fill="#2845C1"/>
|
||||
<path d="M52.001 13.6733C52.001 13.7837 51.9114 13.8733 51.801 13.8733H46.0027C45.8923 13.8733 45.8027 13.9628 45.8027 14.0733V17.979C45.8027 18.0894 45.8923 18.179 46.0027 18.179H50.8889C50.9993 18.179 51.0889 18.2685 51.0889 18.379V20.3266C51.0889 20.4371 50.9993 20.5266 50.8889 20.5266H46.0027C45.8923 20.5266 45.8027 20.6162 45.8027 20.7266V27.2758C45.8027 27.3863 45.7132 27.4758 45.6027 27.4758H43.2C43.0895 27.4758 43 27.3863 43 27.2758V11.7256C43 11.6152 43.0895 11.5256 43.2 11.5256H51.801C51.9114 11.5256 52.001 11.6152 52.001 11.7256V13.6733Z" fill="#2845C1"/>
|
||||
<path d="M56.4697 27.2758C56.4697 27.3863 56.3802 27.4758 56.2697 27.4758H54.1639C54.0534 27.4758 53.9639 27.3863 53.9639 27.2758V16.1915C53.9639 16.081 54.0534 15.9915 54.1639 15.9915H56.2697C56.3802 15.9915 56.4697 16.081 56.4697 16.1915V27.2758Z" fill="#2845C1"/>
|
||||
<path d="M73.9727 16.7886C73.9727 16.8787 74.0837 16.924 74.1493 16.8622C74.5262 16.5068 74.9607 16.2089 75.4531 15.969C76.0456 15.6804 76.7374 15.5364 77.5273 15.5364C78.3779 15.5364 79.1374 15.726 79.8057 16.1057C80.4892 16.4855 81.0289 17.0479 81.4238 17.7922C81.8186 18.5365 82.0156 19.4711 82.0156 20.595V27.2758C82.0156 27.3863 81.9261 27.4758 81.8156 27.4758H79.6863C79.5759 27.4758 79.4863 27.3863 79.4863 27.2758V20.6858C79.4863 19.8047 79.2584 19.1211 78.8027 18.635C78.347 18.1338 77.7244 17.8831 76.9346 17.8831C76.4334 17.8831 75.993 17.9816 75.6133 18.179C75.2335 18.3612 74.9062 18.5974 74.6328 18.886C74.3934 19.1536 74.1868 19.4345 74.0127 19.7283C73.9861 19.7732 73.9727 19.8246 73.9727 19.8768V27.2758C73.9727 27.3863 73.8831 27.4758 73.7727 27.4758H71.6658C71.5554 27.4758 71.4658 27.3863 71.4658 27.2758V10.587C71.4658 10.4765 71.5554 10.387 71.6658 10.387H73.7727C73.8831 10.387 73.9727 10.4765 73.9727 10.587V16.7886Z" fill="#2845C1"/>
|
||||
<path d="M87.374 27.2758C87.374 27.3863 87.2845 27.4758 87.174 27.4758H85.0682C84.9577 27.4758 84.8682 27.3863 84.8682 27.2758V16.1915C84.8682 16.081 84.9577 15.9915 85.0682 15.9915H87.174C87.2845 15.9915 87.374 16.081 87.374 16.1915V27.2758Z" fill="#2845C1"/>
|
||||
<path d="M108.6 15.7639C108.797 15.7639 108.979 15.7794 109.146 15.8098C109.259 15.8303 109.365 15.854 109.464 15.8813C109.547 15.9041 109.602 15.9812 109.602 16.0673V18.1279C109.602 18.258 109.479 18.3535 109.352 18.3276C109.248 18.3064 109.141 18.2875 109.032 18.2708C108.85 18.2404 108.629 18.2249 108.371 18.2249C107.9 18.2249 107.475 18.3006 107.096 18.4524C106.731 18.6043 106.412 18.8095 106.139 19.0676C105.937 19.2472 105.759 19.4436 105.607 19.6564C105.502 19.8038 105.455 19.9837 105.455 20.165V27.2758C105.455 27.3863 105.366 27.4758 105.255 27.4758H103.148C103.038 27.4758 102.948 27.3863 102.948 27.2758V16.1915C102.948 16.081 103.038 15.9915 103.148 15.9915H105.255C105.366 15.9915 105.455 16.081 105.455 16.1915V16.9959C105.455 17.087 105.568 17.1315 105.632 17.0666C105.759 16.9377 105.89 16.8149 106.024 16.6985C106.359 16.4099 106.739 16.182 107.164 16.0149C107.589 15.8478 108.068 15.7639 108.6 15.7639Z" fill="#2845C1"/>
|
||||
<path d="M55.2393 10.2502C55.5429 10.2502 55.8166 10.326 56.0596 10.4778C56.3178 10.6297 56.5156 10.8348 56.6523 11.093C56.8042 11.3361 56.8799 11.6095 56.8799 11.9133C56.8799 12.2171 56.8042 12.4979 56.6523 12.7561C56.5156 13.0143 56.3178 13.2194 56.0596 13.3713C55.8166 13.508 55.543 13.5764 55.2393 13.5764C54.9356 13.5763 54.6546 13.508 54.3965 13.3713C54.1383 13.2194 53.9331 13.0143 53.7812 12.7561C53.6295 12.498 53.5537 12.217 53.5537 11.9133C53.5537 11.4576 53.7132 11.0702 54.0322 10.7512C54.3663 10.4172 54.7686 10.2504 55.2393 10.2502Z" fill="#2845C1"/>
|
||||
<path d="M86.1436 10.2502C86.4473 10.2502 86.7208 10.3259 86.9639 10.4778C87.2221 10.6297 87.4199 10.8348 87.5566 11.093C87.7085 11.336 87.7842 11.6096 87.7842 11.9133C87.7842 12.217 87.7084 12.498 87.5566 12.7561C87.4199 13.0143 87.2221 13.2194 86.9639 13.3713C86.7208 13.508 86.4473 13.5764 86.1436 13.5764C85.8398 13.5764 85.559 13.508 85.3008 13.3713C85.0425 13.2194 84.8375 13.0143 84.6855 12.7561C84.5337 12.4979 84.458 12.2171 84.458 11.9133C84.458 11.4576 84.6175 11.0702 84.9365 10.7512C85.2706 10.4172 85.6728 10.2503 86.1436 10.2502Z" fill="#2845C1"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3435_94" x1="20" y1="27" x2="20" y2="36" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2845C1"/>
|
||||
<stop offset="1" stop-color="#ECF1F7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
BIN
src/frontend/apps/calendars/public/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 670 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.6765 9.72627H8.41971C6.23248 9.72627 4.03303 11.4473 3.50711 13.5704L2 19.6543V7.84882C2 6.57794 2.29141 5.62077 2.87422 4.97729C3.46451 4.32576 4.28269 4 5.32877 4H8.50063C8.88917 4 9.22541 4.02815 9.50935 4.08446C9.79328 4.14076 10.0548 4.23728 10.2939 4.37402C10.533 4.50272 10.7833 4.6837 11.0448 4.91696L11.6837 5.48403C11.9975 5.74946 12.2927 5.93848 12.5691 6.05109C12.8456 6.1637 13.1856 6.22001 13.5891 6.22001H24.0125C25.2155 6.22001 26.1271 6.55381 26.7472 7.22142C27.3035 7.81312 27.6133 8.64807 27.6765 9.72627Z" fill="#C83F49"/>
|
||||
<path d="M5.67656 27C4.46609 27 3.6366 26.6536 3.18806 25.9607C2.73746 25.2761 2.6776 24.266 3.00848 22.9303L5.32709 13.5705C5.61138 12.4229 6.80027 11.4926 7.98256 11.4926H29.3898C30.5721 11.4926 31.3001 12.4229 31.0158 13.5705L28.6972 22.9303C28.3663 24.266 27.8172 25.2761 27.0499 25.9607C26.2805 26.6536 25.3615 27 24.293 27H5.67656Z" fill="#2845C1"/>
|
||||
<rect x="10" y="22" width="12" height="9" rx="4.5" fill="#EEF1F4"/>
|
||||
<rect x="10" y="22" width="12" height="9" rx="4.5" fill="url(#paint0_linear_1156_814)" fill-opacity="0.1"/>
|
||||
<path d="M18.9901 29.2782C18.7635 29.3485 18.5616 29.3864 18.3842 29.3864C17.7931 29.3864 17.3941 28.9103 17.1921 27.9527H17.1675C16.6798 28.986 15.9951 29.5 15.133 29.5C14.4877 29.5 13.9704 29.2349 13.5813 28.6993C13.1921 28.1637 13 27.4928 13 26.6812C13 25.7344 13.2217 24.977 13.6601 24.3873C14.0985 23.7976 14.6946 23.5 15.4483 23.5C15.8522 23.5 16.2167 23.6244 16.532 23.8679C16.8473 24.1168 17.0887 24.463 17.2562 24.9121H17.2759L17.6256 23.6136H18.8867L17.8325 26.4919C17.9507 27.1628 18.0739 27.6226 18.2118 27.8661C18.33 28.1096 18.4975 28.234 18.7044 28.234C18.8227 28.234 18.9163 28.2124 19 28.1745L18.9901 29.2782ZM16.8916 26.4432C16.7882 25.8318 16.6207 25.3557 16.3941 25.0311C16.1724 24.7011 15.9015 24.5388 15.5911 24.5388C15.1872 24.5388 14.8621 24.739 14.6207 25.1339C14.3793 25.5343 14.2709 26.0266 14.2709 26.6055C14.2709 27.1357 14.3645 27.5739 14.5764 27.931C14.7833 28.2881 15.064 28.4612 15.4138 28.4612C15.7094 28.4612 15.9803 28.3043 16.2217 28.0068C16.468 27.6984 16.67 27.2493 16.8325 26.6596L16.8916 26.4432Z" fill="#C83F49"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1156_814" x1="16" y1="22" x2="16" y2="31" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C83F49"/>
|
||||
<stop offset="1" stop-color="#EEF1F4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
16
src/frontend/apps/calendars/public/assets/logo-icon_beta.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M55.3531 19.4525H16.8394C12.465 19.4525 8.06606 22.8947 7.01421 27.1408L4 39.3087V15.6976C4 13.1559 4.58282 11.2415 5.74845 9.95457C6.92902 8.65152 8.56539 8 10.6575 8H17.0013C17.7783 8 18.4508 8.0563 19.0187 8.16891C19.5866 8.28152 20.1096 8.47457 20.5878 8.74805C21.066 9.00544 21.5666 9.3674 22.0897 9.83392L23.3674 10.9681C23.995 11.4989 24.5853 11.877 25.1383 12.1022C25.6912 12.3274 26.3711 12.44 27.1781 12.44H48.025C50.4309 12.44 52.2541 13.1076 53.4945 14.4428C54.607 15.6262 55.2265 17.2961 55.3531 19.4525Z" fill="#C83F49"/>
|
||||
<path d="M11.3531 54C8.93219 54 7.27319 53.3071 6.37613 51.9213C5.47493 50.5522 5.3552 48.532 6.01696 45.8606L10.6542 27.1409C11.2228 24.8457 13.6005 22.9851 15.9651 22.9851H58.7796C61.1442 22.9851 62.6002 24.8457 62.0316 27.1409L57.3944 45.8606C56.7326 48.532 55.6344 50.5522 54.0997 51.9213C52.5609 53.3071 50.723 54 48.586 54H11.3531Z" fill="#2845C1"/>
|
||||
<rect x="8" y="46" width="48" height="18" rx="9" fill="#EEF1F4"/>
|
||||
<rect x="8" y="46" width="48" height="18" rx="9" fill="url(#paint0_linear_11386_15350)" fill-opacity="0.1"/>
|
||||
<path d="M16.6854 59.4725C16.5856 59.4725 16.5358 59.4725 16.4976 59.4531C16.4641 59.436 16.4369 59.4087 16.4198 59.3752C16.4004 59.3371 16.4004 59.2872 16.4004 59.1875V50.485C16.4004 50.3852 16.4004 50.3353 16.4198 50.2972C16.4369 50.2637 16.4641 50.2364 16.4976 50.2194C16.5358 50.2 16.5856 50.2 16.6854 50.2H19.1292C21.0632 50.2 22.2421 51.1537 22.2421 52.7168C22.2421 53.296 22.0394 53.8129 21.6519 54.2136C21.5149 54.3553 21.4463 54.4262 21.4348 54.4727C21.4223 54.5229 21.425 54.5479 21.4479 54.5943C21.4691 54.6373 21.5483 54.6901 21.7069 54.7957C22.3966 55.255 22.7719 55.9399 22.7719 56.7437C22.7719 58.4393 21.4605 59.4725 19.3279 59.4725H16.6854ZM19.1689 51.763H18.5664C18.4666 51.763 18.4168 51.763 18.3786 51.7825C18.3451 51.7995 18.3179 51.8268 18.3008 51.8603C18.2814 51.8984 18.2814 51.9483 18.2814 52.048V53.571C18.2814 53.6707 18.2814 53.7206 18.3008 53.7587C18.3179 53.7922 18.3451 53.8195 18.3786 53.8366C18.4168 53.856 18.4666 53.856 18.5664 53.856H19.1689C19.8842 53.856 20.3081 53.4718 20.3081 52.7963C20.3081 52.1472 19.8842 51.763 19.1689 51.763ZM19.4338 55.4191H18.5664C18.4666 55.4191 18.4168 55.4191 18.3786 55.4385C18.3451 55.4556 18.3179 55.4828 18.3008 55.5163C18.2814 55.5544 18.2814 55.6043 18.2814 55.7041V57.6244C18.2814 57.7242 18.2814 57.774 18.3008 57.8121C18.3179 57.8457 18.3451 57.8729 18.3786 57.89C18.4168 57.9094 18.4666 57.9094 18.5664 57.9094H19.4338C20.3081 57.9094 20.838 57.4458 20.838 56.6642C20.838 55.8694 20.3081 55.4191 19.4338 55.4191Z" fill="#2845C1"/>
|
||||
<path d="M24.8547 59.4725C24.755 59.4725 24.7051 59.4725 24.667 59.4531C24.6335 59.436 24.6062 59.4087 24.5892 59.3752C24.5697 59.3371 24.5697 59.2872 24.5697 59.1875V50.485C24.5697 50.3852 24.5697 50.3353 24.5892 50.2972C24.6062 50.2637 24.6335 50.2364 24.667 50.2194C24.7051 50.2 24.755 50.2 24.8547 50.2H29.6893C29.7891 50.2 29.8389 50.2 29.877 50.2194C29.9106 50.2364 29.9378 50.2637 29.9549 50.2972C29.9743 50.3353 29.9743 50.3852 29.9743 50.485V51.478C29.9743 51.5778 29.9743 51.6277 29.9549 51.6658C29.9378 51.6993 29.9106 51.7265 29.877 51.7436C29.8389 51.763 29.7891 51.763 29.6893 51.763H26.7357C26.636 51.763 26.5861 51.763 26.548 51.7825C26.5145 51.7995 26.4872 51.8268 26.4702 51.8603C26.4507 51.8984 26.4507 51.9483 26.4507 52.048V53.677C26.4507 53.7767 26.4507 53.8266 26.4702 53.8647C26.4872 53.8982 26.5145 53.9255 26.548 53.9425C26.5861 53.962 26.636 53.962 26.7357 53.962H29.1594C29.2592 53.962 29.3091 53.962 29.3472 53.9814C29.3807 53.9984 29.408 54.0257 29.425 54.0592C29.4444 54.0973 29.4444 54.1472 29.4444 54.247V55.24C29.4444 55.3398 29.4444 55.3897 29.425 55.4278C29.408 55.4613 29.3807 55.4885 29.3472 55.5056C29.3091 55.525 29.2592 55.525 29.1594 55.525H26.7357C26.636 55.525 26.5861 55.525 26.548 55.5445C26.5145 55.5615 26.4872 55.5888 26.4702 55.6223C26.4507 55.6604 26.4507 55.7103 26.4507 55.81V57.6244C26.4507 57.7242 26.4507 57.774 26.4702 57.8121C26.4872 57.8457 26.5145 57.8729 26.548 57.89C26.5861 57.9094 26.636 57.9094 26.7357 57.9094H29.6893C29.7891 57.9094 29.8389 57.9094 29.877 57.9288C29.9106 57.9459 29.9378 57.9731 29.9549 58.0067C29.9743 58.0448 29.9743 58.0946 29.9743 58.1944V59.1875C29.9743 59.2872 29.9743 59.3371 29.9549 59.3752C29.9378 59.4087 29.9106 59.436 29.877 59.4531C29.8389 59.4725 29.7891 59.4725 29.6893 59.4725H24.8547Z" fill="#2845C1"/>
|
||||
<path d="M31.5529 51.9087C31.4531 51.9087 31.4033 51.9087 31.3652 51.8893C31.3316 51.8723 31.3044 51.845 31.2873 51.8115C31.2679 51.7734 31.2679 51.7235 31.2679 51.6237V50.485C31.2679 50.3852 31.2679 50.3353 31.2873 50.2972C31.3044 50.2637 31.3316 50.2364 31.3652 50.2194C31.4033 50.2 31.4531 50.2 31.5529 50.2H38.5864C38.6861 50.2 38.736 50.2 38.7741 50.2194C38.8076 50.2364 38.8349 50.2637 38.852 50.2972C38.8714 50.3353 38.8714 50.3852 38.8714 50.485V51.6237C38.8714 51.7235 38.8714 51.7734 38.852 51.8115C38.8349 51.845 38.8076 51.8723 38.7741 51.8893C38.736 51.9087 38.6861 51.9087 38.5864 51.9087H36.2951C36.1954 51.9087 36.1455 51.9087 36.1074 51.9282C36.0739 51.9452 36.0466 51.9725 36.0296 52.006C36.0101 52.0441 36.0101 52.094 36.0101 52.1937V59.1875C36.0101 59.2872 36.0101 59.3371 35.9907 59.3752C35.9737 59.4087 35.9464 59.436 35.9129 59.4531C35.8748 59.4725 35.8249 59.4725 35.7251 59.4725H34.4141C34.3144 59.4725 34.2645 59.4725 34.2264 59.4531C34.1929 59.436 34.1656 59.4087 34.1486 59.3752C34.1291 59.3371 34.1291 59.2872 34.1291 59.1875V52.1937C34.1291 52.094 34.1291 52.0441 34.1097 52.006C34.0927 51.9725 34.0654 51.9452 34.0319 51.9282C33.9938 51.9087 33.9439 51.9087 33.8441 51.9087H31.5529Z" fill="#2845C1"/>
|
||||
<path d="M38.7454 59.4725C38.6068 59.4725 38.5375 59.4725 38.493 59.4434C38.4541 59.4179 38.4268 59.3783 38.4167 59.3329C38.4052 59.281 38.4298 59.2162 38.4788 59.0866L41.7734 50.384C41.7985 50.3177 41.8111 50.2845 41.8327 50.26C41.8518 50.2384 41.8759 50.2218 41.9029 50.2115C41.9334 50.2 41.9689 50.2 42.0399 50.2H44.11C44.181 50.2 44.2165 50.2 44.2471 50.2115C44.274 50.2218 44.2981 50.2384 44.3172 50.26C44.3389 50.2845 44.3515 50.3177 44.3766 50.384L47.6711 59.0866C47.7202 59.2162 47.7447 59.281 47.7332 59.3329C47.7232 59.3783 47.6958 59.4179 47.6569 59.4434C47.6124 59.4725 47.5432 59.4725 47.4046 59.4725H46.0158C45.9441 59.4725 45.9083 59.4725 45.8776 59.4607C45.8504 59.4503 45.8262 59.4335 45.8071 59.4116C45.7854 59.3868 45.7731 59.3532 45.7483 59.2859L44.9981 57.2482C44.9734 57.1809 44.961 57.1473 44.9394 57.1225C44.9202 57.1007 44.896 57.0838 44.8689 57.0734C44.8382 57.0616 44.8023 57.0616 44.7307 57.0616H41.4192C41.3476 57.0616 41.3118 57.0616 41.281 57.0734C41.2539 57.0838 41.2297 57.1007 41.2106 57.1225C41.1889 57.1473 41.1765 57.1809 41.1518 57.2482L40.4016 59.2859C40.3769 59.3532 40.3645 59.3868 40.3428 59.4116C40.3237 59.4335 40.2995 59.4503 40.2724 59.4607C40.2416 59.4725 40.2058 59.4725 40.1342 59.4725H38.7454ZM41.943 55.0761C41.896 55.2048 41.8725 55.2692 41.8844 55.3206C41.8947 55.3656 41.9222 55.4049 41.9609 55.43C42.0052 55.4588 42.0737 55.4588 42.2107 55.4588H43.9392C44.0763 55.4588 44.1448 55.4588 44.189 55.43C44.2278 55.4049 44.2552 55.3656 44.2656 55.3206C44.2774 55.2692 44.2539 55.2048 44.2069 55.0761L43.3427 52.7084C43.2601 52.4823 43.2189 52.3692 43.1585 52.3372C43.1063 52.3094 43.0437 52.3094 42.9914 52.3372C42.9311 52.3692 42.8898 52.4823 42.8072 52.7084L41.943 55.0761Z" fill="#2845C1"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_11386_15350" x1="32" y1="46" x2="32" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2845C1"/>
|
||||
<stop offset="1" stop-color="#EEF1F4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
6
src/frontend/apps/calendars/public/assets/logo_alpha.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="153" height="40" viewBox="0 0 153 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.18213 11.5846C8.59434 11.4599 9.03364 11.3924 9.49206 11.3924H30.5085C30.9669 11.3924 31.4062 11.4599 31.8184 11.5846C31.8178 10.391 31.8053 9.77042 31.5608 9.29057C31.3342 8.84582 30.9726 8.48423 30.5279 8.25762C30.0223 8 29.3604 8 28.0366 8H11.9639C10.6402 8 9.97827 8 9.47266 8.25762C9.02792 8.48423 8.66633 8.84582 8.43972 9.29057C8.19522 9.77041 8.18276 10.391 8.18213 11.5846Z" fill="#FBC63A"/>
|
||||
<path d="M24.2853 24.4406C24.2853 24.4959 24.2405 24.5406 24.1853 24.5406H21.6615C21.5786 24.5406 21.5317 24.4454 21.5823 24.3796L24.106 21.1015C24.1643 21.0259 24.2853 21.0671 24.2853 21.1625V24.4406Z" fill="#2845C1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.3585 22.7595L32.9244 16.496C33.3174 14.9241 32.1285 13.4014 30.5082 13.4014H9.49178C7.8715 13.4014 6.68261 14.9241 7.07559 16.496L8.64148 22.7595C8.74063 23.1561 8.74063 23.571 8.64148 23.9676L7.07559 30.2312C6.68261 31.8031 7.8715 33.3258 9.49178 33.3258H30.5082C32.1285 33.3258 33.3174 31.8031 32.9244 30.2312L31.3585 23.9676C31.2594 23.571 31.2594 23.1561 31.3585 22.7595ZM15.1567 20.7227C15.2234 20.685 15.3059 20.7331 15.3059 20.8097V29.1573C15.3059 29.2125 15.3507 29.2573 15.4059 29.2573H17.28C17.3352 29.2573 17.38 29.2125 17.38 29.1573V18.6027C17.38 18.5475 17.3352 18.5027 17.28 18.5027H15.3942C15.3766 18.5027 15.3594 18.5074 15.3441 18.5162L12.1431 20.3686C12.0924 20.398 12.0776 20.4644 12.1111 20.5124L13.0261 21.8235C13.0557 21.8658 13.1125 21.8787 13.1574 21.8533L15.1567 20.7227ZM19.3434 24.5137C19.33 24.5312 19.3228 24.5525 19.3228 24.5745V26.315C19.3228 26.3702 19.3676 26.415 19.4228 26.415H24.1853C24.2405 26.415 24.2853 26.4598 24.2853 26.515V29.1726C24.2853 29.2279 24.33 29.2726 24.3853 29.2726H26.2593C26.3146 29.2726 26.3593 29.2279 26.3593 29.1726V26.515C26.3593 26.4598 26.4041 26.415 26.4593 26.415H27.3502C27.4054 26.415 27.4502 26.3702 27.4502 26.315V24.6406C27.4502 24.5854 27.4054 24.5406 27.3502 24.5406H26.4593C26.4041 24.5406 26.3593 24.4959 26.3593 24.4406V18.6181C26.3593 18.5629 26.3146 18.5181 26.2593 18.5181H23.9813C23.9502 18.5181 23.9208 18.5326 23.9019 18.5573L19.3434 24.5137Z" fill="#2845C1"/>
|
||||
<path d="M49.13 25.514C50.89 25.514 52.276 24.656 53.156 23.402L55.62 25.294C54.212 27.208 51.924 28.44 49.13 28.44C44.268 28.44 40.946 24.7 40.946 20.3C40.946 15.9 44.268 12.16 49.13 12.16C51.924 12.16 54.212 13.414 55.62 15.284L53.156 17.198C52.276 15.944 50.89 15.086 49.13 15.086C46.226 15.086 44.158 17.352 44.158 20.3C44.158 23.248 46.226 25.514 49.13 25.514ZM60.4263 28.33C58.2043 28.33 56.6863 27.054 56.6863 25.008C56.6863 23.336 57.9843 22.082 60.3603 21.686L63.7483 21.114V20.828C63.7483 19.662 62.8683 18.914 61.5923 18.914C60.5143 18.914 59.6783 19.42 59.0843 20.234L57.0383 18.672C58.0283 17.308 59.6563 16.472 61.6803 16.472C64.8923 16.472 66.5423 18.386 66.5423 20.828V28H63.7483V26.922C63.0443 27.78 61.7243 28.33 60.4263 28.33ZM59.4583 24.876C59.4583 25.624 60.0523 26.108 60.9983 26.108C62.2743 26.108 63.1983 25.514 63.7483 24.634V23.072L61.1083 23.512C59.9423 23.71 59.4583 24.194 59.4583 24.876ZM69.3018 28V11.5H72.0958V28H69.3018ZM85.2143 26.196C84.1803 27.582 82.4643 28.44 80.3743 28.44C76.4363 28.44 74.2143 25.69 74.2143 22.456C74.2143 19.178 76.2823 16.472 79.9123 16.472C82.9923 16.472 85.0163 18.562 85.0163 21.466C85.0163 22.082 84.9283 22.654 84.8403 23.028H77.0743C77.3383 25.096 78.6143 25.932 80.3523 25.932C81.5623 25.932 82.6183 25.404 83.2123 24.612L85.2143 26.196ZM79.8463 18.76C78.4163 18.76 77.4703 19.552 77.1623 21.004H82.2663C82.2223 19.882 81.4083 18.76 79.8463 18.76ZM87.3065 28V16.912H90.1005V17.792C90.8705 17.044 91.9045 16.472 93.3785 16.472C95.7765 16.472 97.6905 18.122 97.6905 21.4V28H94.8745V21.51C94.8745 20.036 94.0385 19.112 92.6745 19.112C91.2665 19.112 90.5185 20.058 90.1005 20.762V28H87.3065ZM99.6312 22.456C99.6312 19.222 101.765 16.472 105.263 16.472C106.759 16.472 107.837 16.934 108.695 17.792V11.5H111.511V28H108.695V27.12C107.837 27.978 106.759 28.44 105.263 28.44C101.765 28.44 99.6312 25.69 99.6312 22.456ZM102.557 22.456C102.557 24.392 103.767 25.8 105.659 25.8C106.913 25.8 107.947 25.272 108.695 24.26V20.652C107.947 19.64 106.913 19.112 105.659 19.112C103.767 19.112 102.557 20.52 102.557 22.456ZM114.421 28V16.912H117.215V18.012C117.941 17.264 118.887 16.692 120.141 16.692C120.515 16.692 120.845 16.758 121.109 16.846V19.596C120.757 19.508 120.383 19.442 119.877 19.442C118.513 19.442 117.633 20.19 117.215 20.894V28H114.421ZM124.326 14.976C123.336 14.976 122.5 14.14 122.5 13.15C122.5 12.16 123.336 11.324 124.326 11.324C125.316 11.324 126.13 12.16 126.13 13.15C126.13 14.14 125.316 14.976 124.326 14.976ZM122.918 28V16.912H125.712V28H122.918ZM138.831 26.196C137.797 27.582 136.081 28.44 133.991 28.44C130.053 28.44 127.831 25.69 127.831 22.456C127.831 19.178 129.899 16.472 133.529 16.472C136.609 16.472 138.633 18.562 138.633 21.466C138.633 22.082 138.545 22.654 138.457 23.028H130.691C130.955 25.096 132.231 25.932 133.969 25.932C135.179 25.932 136.235 25.404 136.829 24.612L138.831 26.196ZM133.463 18.76C132.033 18.76 131.087 19.552 130.779 21.004H135.883C135.839 19.882 135.025 18.76 133.463 18.76ZM140.923 28V16.912H143.717V18.012C144.443 17.264 145.389 16.692 146.643 16.692C147.017 16.692 147.347 16.758 147.611 16.846V19.596C147.259 19.508 146.885 19.442 146.379 19.442C145.015 19.442 144.135 20.19 143.717 20.894V28H140.923Z" fill="#2845C1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
68
src/frontend/apps/calendars/public/mime-video.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_3239_3207)">
|
||||
<rect x="2.85" y="5.85" width="26.3" height="21.3" rx="3.42778" fill="#FAFAFA"/>
|
||||
<rect x="2.85" y="5.85" width="26.3" height="21.3" rx="3.42778" fill="url(#paint0_linear_3239_3207)"/>
|
||||
<rect x="2.85" y="5.85" width="26.3" height="21.3" rx="3.42778" fill="url(#paint1_linear_3239_3207)" fill-opacity="0.04"/>
|
||||
<rect x="2.85" y="5.85" width="26.3" height="21.3" rx="3.42778" stroke="#CECECE" stroke-width="0.7"/>
|
||||
<rect x="2.85" y="5.85" width="26.3" height="21.3" rx="3.42778" stroke="url(#paint2_linear_3239_3207)" stroke-opacity="0.5" stroke-width="0.7"/>
|
||||
<rect x="2.85" y="5.85" width="26.3" height="21.3" rx="3.42778" stroke="url(#paint3_linear_3239_3207)" stroke-opacity="0.2" stroke-width="0.7"/>
|
||||
<path d="M13.2866 19.2658V13.7304C13.2866 13.487 13.3513 13.3043 13.4806 13.1826C13.61 13.0609 13.7647 13 13.9448 13C14.102 13 14.2567 13.0431 14.4089 13.1293L19.0122 15.8152C19.1872 15.9167 19.3178 16.0194 19.404 16.1234C19.4902 16.2248 19.5334 16.3504 19.5334 16.5C19.5334 16.6446 19.4902 16.7701 19.404 16.8766C19.3178 16.9806 19.1872 17.0821 19.0122 17.181L14.4089 19.8668C14.2567 19.9556 14.102 20 13.9448 20C13.7647 20 13.61 19.9379 13.4806 19.8136C13.3513 19.6918 13.2866 19.5092 13.2866 19.2658Z" fill="#CECECE"/>
|
||||
<path d="M13.2866 19.2658V13.7304C13.2866 13.487 13.3513 13.3043 13.4806 13.1826C13.61 13.0609 13.7647 13 13.9448 13C14.102 13 14.2567 13.0431 14.4089 13.1293L19.0122 15.8152C19.1872 15.9167 19.3178 16.0194 19.404 16.1234C19.4902 16.2248 19.5334 16.3504 19.5334 16.5C19.5334 16.6446 19.4902 16.7701 19.404 16.8766C19.3178 16.9806 19.1872 17.0821 19.0122 17.181L14.4089 19.8668C14.2567 19.9556 14.102 20 13.9448 20C13.7647 20 13.61 19.9379 13.4806 19.8136C13.3513 19.6918 13.2866 19.5092 13.2866 19.2658Z" fill="url(#paint4_linear_3239_3207)" fill-opacity="0.77"/>
|
||||
<g opacity="0.52">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 8.86977C5 8.38941 5.38941 8 5.86977 8H7.71165C8.19201 8 8.58142 8.38941 8.58142 8.86977C8.58142 9.35014 8.19201 9.73955 7.71165 9.73955H5.86977C5.38941 9.73955 5 9.35014 5 8.86977Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 8.86977C5 8.38941 5.38941 8 5.86977 8H7.71165C8.19201 8 8.58142 8.38941 8.58142 8.86977C8.58142 9.35014 8.19201 9.73955 7.71165 9.73955H5.86977C5.38941 9.73955 5 9.35014 5 8.86977Z" fill="#6A6AF4" fill-opacity="0.18"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.60449 8.86977C9.60449 8.38941 9.9939 8 10.4743 8H12.3161C12.7965 8 13.1859 8.38941 13.1859 8.86977C13.1859 9.35014 12.7965 9.73955 12.3161 9.73955H10.4743C9.9939 9.73955 9.60449 9.35014 9.60449 8.86977Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.60449 8.86977C9.60449 8.38941 9.9939 8 10.4743 8H12.3161C12.7965 8 13.1859 8.38941 13.1859 8.86977C13.1859 9.35014 12.7965 9.73955 12.3161 9.73955H10.4743C9.9939 9.73955 9.60449 9.35014 9.60449 8.86977Z" fill="#6A6AF4" fill-opacity="0.18"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2095 8.86977C14.2095 8.38941 14.5989 8 15.0792 8H16.9211C17.4015 8 17.7909 8.38941 17.7909 8.86977C17.7909 9.35014 17.4015 9.73955 16.9211 9.73955H15.0792C14.5989 9.73955 14.2095 9.35014 14.2095 8.86977Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2095 8.86977C14.2095 8.38941 14.5989 8 15.0792 8H16.9211C17.4015 8 17.7909 8.38941 17.7909 8.86977C17.7909 9.35014 17.4015 9.73955 16.9211 9.73955H15.0792C14.5989 9.73955 14.2095 9.35014 14.2095 8.86977Z" fill="#6A6AF4" fill-opacity="0.18"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.814 8.86977C18.814 8.38941 19.2034 8 19.6837 8H21.5256C22.006 8 22.3954 8.38941 22.3954 8.86977C22.3954 9.35014 22.006 9.73955 21.5256 9.73955H19.6837C19.2034 9.73955 18.814 9.35014 18.814 8.86977Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.814 8.86977C18.814 8.38941 19.2034 8 19.6837 8H21.5256C22.006 8 22.3954 8.38941 22.3954 8.86977C22.3954 9.35014 22.006 9.73955 21.5256 9.73955H19.6837C19.2034 9.73955 18.814 9.35014 18.814 8.86977Z" fill="#6A6AF4" fill-opacity="0.18"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4185 8.86977C23.4185 8.38941 23.8079 8 24.2882 8H26.1301C26.6105 8 26.9999 8.38941 26.9999 8.86977C26.9999 9.35014 26.6105 9.73955 26.1301 9.73955H24.2882C23.8079 9.73955 23.4185 9.35014 23.4185 8.86977Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4185 8.86977C23.4185 8.38941 23.8079 8 24.2882 8H26.1301C26.6105 8 26.9999 8.38941 26.9999 8.86977C26.9999 9.35014 26.6105 9.73955 26.1301 9.73955H24.2882C23.8079 9.73955 23.4185 9.35014 23.4185 8.86977Z" fill="#6A6AF4" fill-opacity="0.18"/>
|
||||
</g>
|
||||
<g opacity="0.38">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 24.1298C5 23.6494 5.38941 23.26 5.86977 23.26H7.71165C8.19201 23.26 8.58142 23.6494 8.58142 24.1298C8.58142 24.6101 8.19201 24.9996 7.71165 24.9996H5.86977C5.38941 24.9996 5 24.6101 5 24.1298Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 24.1298C5 23.6494 5.38941 23.26 5.86977 23.26H7.71165C8.19201 23.26 8.58142 23.6494 8.58142 24.1298C8.58142 24.6101 8.19201 24.9996 7.71165 24.9996H5.86977C5.38941 24.9996 5 24.6101 5 24.1298Z" fill="#6A6AF4" fill-opacity="0.44"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.60449 24.1298C9.60449 23.6494 9.9939 23.26 10.4743 23.26H12.3161C12.7965 23.26 13.1859 23.6494 13.1859 24.1298C13.1859 24.6101 12.7965 24.9996 12.3161 24.9996H10.4743C9.9939 24.9996 9.60449 24.6101 9.60449 24.1298Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.60449 24.1298C9.60449 23.6494 9.9939 23.26 10.4743 23.26H12.3161C12.7965 23.26 13.1859 23.6494 13.1859 24.1298C13.1859 24.6101 12.7965 24.9996 12.3161 24.9996H10.4743C9.9939 24.9996 9.60449 24.6101 9.60449 24.1298Z" fill="#6A6AF4" fill-opacity="0.44"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2095 24.1298C14.2095 23.6494 14.5989 23.26 15.0792 23.26H16.9211C17.4015 23.26 17.7909 23.6494 17.7909 24.1298C17.7909 24.6101 17.4015 24.9996 16.9211 24.9996H15.0792C14.5989 24.9996 14.2095 24.6101 14.2095 24.1298Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2095 24.1298C14.2095 23.6494 14.5989 23.26 15.0792 23.26H16.9211C17.4015 23.26 17.7909 23.6494 17.7909 24.1298C17.7909 24.6101 17.4015 24.9996 16.9211 24.9996H15.0792C14.5989 24.9996 14.2095 24.6101 14.2095 24.1298Z" fill="#6A6AF4" fill-opacity="0.44"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.814 24.1298C18.814 23.6494 19.2034 23.26 19.6837 23.26H21.5256C22.006 23.26 22.3954 23.6494 22.3954 24.1298C22.3954 24.6101 22.006 24.9996 21.5256 24.9996H19.6837C19.2034 24.9996 18.814 24.6101 18.814 24.1298Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.814 24.1298C18.814 23.6494 19.2034 23.26 19.6837 23.26H21.5256C22.006 23.26 22.3954 23.6494 22.3954 24.1298C22.3954 24.6101 22.006 24.9996 21.5256 24.9996H19.6837C19.2034 24.9996 18.814 24.6101 18.814 24.1298Z" fill="#6A6AF4" fill-opacity="0.44"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4185 24.1298C23.4185 23.6494 23.8079 23.26 24.2882 23.26H26.1301C26.6105 23.26 26.9999 23.6494 26.9999 24.1298C26.9999 24.6101 26.6105 24.9996 26.1301 24.9996H24.2882C23.8079 24.9996 23.4185 24.6101 23.4185 24.1298Z" fill="#CECECE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4185 24.1298C23.4185 23.6494 23.8079 23.26 24.2882 23.26H26.1301C26.6105 23.26 26.9999 23.6494 26.9999 24.1298C26.9999 24.6101 26.6105 24.9996 26.1301 24.9996H24.2882C23.8079 24.9996 23.4185 24.6101 23.4185 24.1298Z" fill="#6A6AF4" fill-opacity="0.44"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_3239_3207" x="-1.5" y="0.5" width="36" height="36" 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="2"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<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.06 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3239_3207"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3239_3207" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_3239_3207" x1="16" y1="5.5" x2="26" y2="23.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3239_3207" x1="9.75363" y1="1.56622" x2="15.1333" y2="29.9727" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6A6AF4"/>
|
||||
<stop offset="1" stop-color="#6A6AF4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_3239_3207" x1="10" y1="-1" x2="18.5564" y2="36.5113" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6A6AF4" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#6A6AF4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_3239_3207" x1="16" y1="5.5" x2="16" y2="27.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_3239_3207" x1="13" y1="9.5" x2="17.459" y2="24.5122" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6A6AF4" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#6A6AF4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
3
src/frontend/apps/calendars/src/assets/feedback/form.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.297 8.92893C15.8505 8.92893 15.5005 8.79702 15.2468 8.53321C14.9931 8.2694 14.8663 7.89905 14.8663 7.42217V5.82409C14.8663 5.3472 14.9931 4.97685 15.2468 4.71304C15.5005 4.44923 15.8505 4.31733 16.297 4.31733H17.682C17.7327 3.45487 18.0726 2.71925 18.7017 2.11045C19.3409 1.50166 20.1019 1.19727 20.9847 1.19727C21.8674 1.19727 22.6233 1.50166 23.2524 2.11045C23.8917 2.71925 24.2366 3.45487 24.2874 4.31733H25.6876C26.134 4.31733 26.4841 4.44923 26.7378 4.71304C26.9914 4.97685 27.1183 5.3472 27.1183 5.82409V7.42217C27.1183 7.89905 26.9914 8.2694 26.7378 8.53321C26.4841 8.79702 26.134 8.92893 25.6876 8.92893H16.297ZM20.9847 5.76321C21.3601 5.76321 21.6746 5.6313 21.9283 5.36749C22.182 5.10368 22.3088 4.79421 22.3088 4.43908C22.3088 4.06366 22.182 3.74912 21.9283 3.49546C21.6746 3.23165 21.3601 3.09974 20.9847 3.09974C20.6194 3.09974 20.3049 3.23165 20.041 3.49546C19.7874 3.74912 19.6605 4.06366 19.6605 4.43908C19.6605 4.79421 19.7874 5.10368 20.041 5.36749C20.3049 5.6313 20.6194 5.76321 20.9847 5.76321ZM12.903 38.0749C11.3302 38.0749 10.1482 37.6741 9.35674 36.8725C8.57546 36.0811 8.18481 34.8888 8.18481 33.2958V10.2226C8.18481 8.66004 8.56024 7.4729 9.31108 6.66118C10.0721 5.84945 11.2136 5.44359 12.7355 5.44359H13.1008C13.0907 5.50447 13.0856 5.57042 13.0856 5.64145C13.0856 5.70233 13.0856 5.76321 13.0856 5.82409V7.19387C13.0856 7.47797 13.1059 7.71134 13.1465 7.89398H12.7964C12.076 7.89398 11.5332 8.10706 11.1679 8.53321C10.8128 8.95937 10.6352 9.53265 10.6352 10.2531V33.2654C10.6352 34.0264 10.8381 34.6098 11.244 35.0157C11.6499 35.4215 12.2485 35.6245 13.0399 35.6245H28.9446C29.7361 35.6245 30.3296 35.4215 30.7253 35.0157C31.1312 34.6098 31.3341 34.0264 31.3341 33.2654V22.6724L33.7845 20.222V33.2958C33.7845 34.8888 33.3888 36.0811 32.5974 36.8725C31.8161 37.6741 30.6391 38.0749 29.0664 38.0749H12.903ZM31.3341 12.5817V10.2531C31.3341 9.53265 31.1515 8.95937 30.7862 8.53321C30.4311 8.10706 29.8984 7.89398 29.1881 7.89398H28.8229C28.8635 7.71134 28.8838 7.47797 28.8838 7.19387V5.82409C28.8838 5.76321 28.8838 5.70233 28.8838 5.64145C28.8838 5.57042 28.8787 5.50447 28.8685 5.44359H29.2338C30.7152 5.44359 31.8415 5.80887 32.6126 6.53942C33.3939 7.26997 33.7845 8.29984 33.7845 9.62904V10.1313C33.7135 10.2023 33.6425 10.2733 33.5715 10.3444C33.5004 10.4154 33.4294 10.4864 33.3584 10.5574L31.3341 12.5817ZM13.3139 16.478C13.3139 16.2141 13.4052 15.9909 13.5878 15.8083C13.7806 15.6155 14.0089 15.5191 14.2727 15.5191H27.7118C27.9249 15.5191 28.1075 15.5648 28.2597 15.6561L26.4942 17.4216H14.2727C14.0089 17.4216 13.7806 17.3303 13.5878 17.1476C13.4052 16.9548 13.3139 16.7316 13.3139 16.478ZM13.3139 21.8658C13.3139 21.6121 13.4052 21.394 13.5878 21.2113C13.7806 21.0287 14.0089 20.9374 14.2727 20.9374H22.9785L21.0912 22.8246H14.2727C14.0089 22.8246 13.7806 22.7333 13.5878 22.5507C13.4052 22.3579 13.3139 22.1296 13.3139 21.8658ZM14.2727 28.5168C14.0089 28.5168 13.7806 28.4255 13.5878 28.2429C13.4052 28.0501 13.3139 27.8269 13.3139 27.5732C13.3139 27.3094 13.4052 27.0862 13.5878 26.9035C13.7806 26.7209 14.0089 26.6296 14.2727 26.6296H16.3883C16.6521 26.6296 16.8753 26.7209 17.058 26.9035C17.2507 27.0862 17.3471 27.3094 17.3471 27.5732C17.3471 27.8269 17.2507 28.0501 17.058 28.2429C16.8753 28.4255 16.6521 28.5168 16.3883 28.5168H14.2727ZM36.8894 14.2102L34.7738 12.0794L35.9153 10.9379C36.169 10.6843 36.4632 10.5473 36.7981 10.527C37.143 10.5067 37.4373 10.6183 37.6808 10.8618L38.0461 11.2271C38.3099 11.4909 38.4418 11.7953 38.4418 12.1403C38.4418 12.4751 38.3048 12.7795 38.0309 13.0535L36.8894 14.2102ZM19.828 29.5365C19.6859 29.5974 19.554 29.5619 19.4323 29.43C19.3105 29.2981 19.2851 29.1662 19.3562 29.0343L20.8173 26.036L33.6171 13.2361L35.7631 15.3517L22.9328 28.1515L19.828 29.5365Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="42" height="43" viewBox="0 0 42 43" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.588 37.7378C13.0603 37.7378 12.6494 37.5603 12.3552 37.2051C12.0711 36.8602 11.929 36.3934 11.929 35.8049V31.8478H11.1985C9.68662 31.8478 8.41323 31.5789 7.37828 31.0411C6.34334 30.4932 5.55698 29.7018 5.01921 28.6668C4.49159 27.6319 4.22778 26.3686 4.22778 24.8771V13.2187C4.22778 11.7272 4.49159 10.4639 5.01921 9.42899C5.55698 8.39404 6.34334 7.60768 7.37828 7.06992C8.41323 6.522 9.68662 6.24805 11.1985 6.24805H30.8016C32.3134 6.24805 33.5868 6.522 34.6217 7.06992C35.6567 7.60768 36.438 8.39404 36.9656 9.42899C37.5034 10.4639 37.7722 11.7272 37.7722 13.2187V24.8771C37.7722 26.3686 37.5034 27.6319 36.9656 28.6668C36.438 29.7018 35.6567 30.4932 34.6217 31.0411C33.5868 31.5789 32.3134 31.8478 30.8016 31.8478H21.0305L15.7796 36.5202C15.323 36.9261 14.9375 37.2305 14.6229 37.4334C14.3084 37.6364 13.9634 37.7378 13.588 37.7378ZM14.212 34.9526L19.0823 30.1127C19.3664 29.8185 19.6404 29.6257 19.9042 29.5344C20.168 29.443 20.513 29.3974 20.9391 29.3974H30.8016C32.3337 29.3974 33.4701 29.0169 34.2108 28.2559C34.9515 27.4848 35.3218 26.3534 35.3218 24.8619V13.2187C35.3218 11.7373 34.9515 10.6161 34.2108 9.85514C33.4701 9.084 32.3337 8.69844 30.8016 8.69844H11.1985C9.65618 8.69844 8.5147 9.084 7.774 9.85514C7.04345 10.6161 6.67817 11.7373 6.67817 13.2187V24.8619C6.67817 26.3534 7.04345 27.4848 7.774 28.2559C8.5147 29.0169 9.65618 29.3974 11.1985 29.3974H13.0705C13.4865 29.3974 13.7807 29.4836 13.9532 29.6561C14.1257 29.8286 14.212 30.1229 14.212 30.5389V34.9526ZM13.1618 14.8472C12.9081 14.8472 12.6951 14.761 12.5226 14.5885C12.3602 14.416 12.2791 14.208 12.2791 13.9645C12.2791 13.721 12.3602 13.518 12.5226 13.3557C12.6951 13.1832 12.9081 13.097 13.1618 13.097H28.6404C28.894 13.097 29.102 13.1832 29.2644 13.3557C29.4369 13.518 29.5231 13.721 29.5231 13.9645C29.5231 14.208 29.4369 14.416 29.2644 14.5885C29.102 14.761 28.894 14.8472 28.6404 14.8472H13.1618ZM13.1618 19.7937C12.9081 19.7937 12.6951 19.7125 12.5226 19.5502C12.3602 19.3777 12.2791 19.1646 12.2791 18.9109C12.2791 18.6776 12.3602 18.4746 12.5226 18.3021C12.6951 18.1195 12.9081 18.0282 13.1618 18.0282H28.6404C28.894 18.0282 29.102 18.1195 29.2644 18.3021C29.4369 18.4746 29.5231 18.6776 29.5231 18.9109C29.5231 19.1646 29.4369 19.3777 29.2644 19.5502C29.102 19.7125 28.894 19.7937 28.6404 19.7937H13.1618ZM13.1618 24.7553C12.9081 24.7553 12.6951 24.6742 12.5226 24.5118C12.3602 24.3393 12.2791 24.1313 12.2791 23.8878C12.2791 23.6341 12.3602 23.4211 12.5226 23.2486C12.6951 23.0761 12.9081 22.9898 13.1618 22.9898H23.2221C23.4656 22.9898 23.6736 23.0761 23.8461 23.2486C24.0186 23.4211 24.1049 23.6341 24.1049 23.8878C24.1049 24.1313 24.0186 24.3393 23.8461 24.5118C23.6736 24.6742 23.4656 24.7553 23.2221 24.7553H13.1618Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="42" height="43" viewBox="0 0 42 43" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.1288 27.1205C13.8568 27.1205 13.6502 27.0399 13.5092 26.8787C13.3783 26.7074 13.3128 26.4908 13.3128 26.2289C13.3128 25.8562 13.4588 25.3676 13.751 24.7631C14.0431 24.1486 14.4814 23.5341 15.0656 22.9196C15.66 22.3051 16.4105 21.7913 17.3172 21.3783C18.2239 20.9552 19.2867 20.7436 20.5056 20.7436C21.7346 20.7436 22.7975 20.9552 23.694 21.3783C24.6007 21.7913 25.3512 22.3051 25.9456 22.9196C26.5399 23.5341 26.9832 24.1486 27.2754 24.7631C27.5675 25.3676 27.7136 25.8562 27.7136 26.2289C27.7136 26.4908 27.6431 26.7074 27.502 26.8787C27.3711 27.0399 27.1645 27.1205 26.8825 27.1205H14.1288ZM20.5056 19.6103C19.8407 19.6103 19.2363 19.439 18.6923 19.0965C18.1584 18.754 17.7302 18.2906 17.4079 17.7063C17.0855 17.1119 16.9243 16.442 16.9243 15.6965C16.9243 15.0014 17.0855 14.3617 17.4079 13.7774C17.7302 13.1931 18.1584 12.7297 18.6923 12.3872C19.2363 12.0346 19.8407 11.8583 20.5056 11.8583C21.1705 11.8583 21.7699 12.0346 22.3038 12.3872C22.8478 12.7297 23.281 13.1931 23.6034 13.7774C23.9257 14.3617 24.0869 15.0014 24.0869 15.6965C24.0869 16.442 23.9257 17.1119 23.6034 17.7063C23.281 18.2906 22.8478 18.754 22.3038 19.0965C21.7699 19.439 21.1705 19.6103 20.5056 19.6103ZM13.1465 38.1968C12.6227 38.1968 12.2147 38.0205 11.9226 37.6679C11.6405 37.3254 11.4994 36.862 11.4994 36.2777V32.3489H10.7741C9.27309 32.3489 8.0088 32.0819 6.98125 31.548C5.9537 31.004 5.17297 30.2182 4.63904 29.1907C4.1152 28.1631 3.85327 26.9089 3.85327 25.428V13.853C3.85327 12.3721 4.1152 11.1179 4.63904 10.0903C5.17297 9.06278 5.9537 8.28205 6.98125 7.74812C8.0088 7.20413 9.27309 6.93213 10.7741 6.93213H30.2371C31.7381 6.93213 33.0024 7.20413 34.03 7.74812C35.0575 8.28205 35.8332 9.06278 36.3571 10.0903C36.891 11.1179 37.158 12.3721 37.158 13.853V25.428C37.158 26.9089 36.891 28.1631 36.3571 29.1907C35.8332 30.2182 35.0575 31.004 34.03 31.548C33.0024 32.0819 31.7381 32.3489 30.2371 32.3489H20.5358L15.3225 36.9879C14.8692 37.3909 14.4864 37.6931 14.1741 37.8946C13.8618 38.0961 13.5193 38.1968 13.1465 38.1968ZM13.7661 35.4315L18.6016 30.6262C18.8837 30.3341 19.1557 30.1427 19.4176 30.052C19.6795 29.9613 20.0221 29.916 20.4452 29.916H30.2371C31.7583 29.916 32.8866 29.5382 33.622 28.7827C34.3574 28.017 34.7251 26.8938 34.7251 25.4129V13.853C34.7251 12.3822 34.3574 11.269 33.622 10.5134C32.8866 9.74782 31.7583 9.365 30.2371 9.365H10.7741C9.24287 9.365 8.10954 9.74782 7.37414 10.5134C6.64881 11.269 6.28615 12.3822 6.28615 13.853V25.4129C6.28615 26.8938 6.64881 28.017 7.37414 28.7827C8.10954 29.5382 9.24287 29.916 10.7741 29.916H12.6328C13.0458 29.916 13.338 30.0016 13.5092 30.1729C13.6805 30.3441 13.7661 30.6363 13.7661 31.0493V35.4315Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
3
src/frontend/apps/calendars/src/assets/icons/cancel.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.4 17.5L12 13.9L15.6 17.5L17 16.1L13.4 12.5L17 8.9L15.6 7.5L12 11.1L8.4 7.5L7 8.9L10.6 12.5L7 16.1L8.4 17.5ZM12 22.5C10.6167 22.5 9.31667 22.2375 8.1 21.7125C6.88333 21.1875 5.825 20.475 4.925 19.575C4.025 18.675 3.3125 17.6167 2.7875 16.4C2.2625 15.1833 2 13.8833 2 12.5C2 11.1167 2.2625 9.81667 2.7875 8.6C3.3125 7.38333 4.025 6.325 4.925 5.425C5.825 4.525 6.88333 3.8125 8.1 3.2875C9.31667 2.7625 10.6167 2.5 12 2.5C13.3833 2.5 14.6833 2.7625 15.9 3.2875C17.1167 3.8125 18.175 4.525 19.075 5.425C19.975 6.325 20.6875 7.38333 21.2125 8.6C21.7375 9.81667 22 11.1167 22 12.5C22 13.8833 21.7375 15.1833 21.2125 16.4C20.6875 17.6167 19.975 18.675 19.075 19.575C18.175 20.475 17.1167 21.1875 15.9 21.7125C14.6833 22.2375 13.3833 22.5 12 22.5ZM12 20.5C14.2333 20.5 16.125 19.725 17.675 18.175C19.225 16.625 20 14.7333 20 12.5C20 10.2667 19.225 8.375 17.675 6.825C16.125 5.275 14.2333 4.5 12 4.5C9.76667 4.5 7.875 5.275 6.325 6.825C4.775 8.375 4 10.2667 4 12.5C4 14.7333 4.775 16.625 6.325 18.175C7.875 19.725 9.76667 20.5 12 20.5Z" fill="#3A3A3A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.4 17.5L12 13.9L15.6 17.5L17 16.1L13.4 12.5L17 8.9L15.6 7.5L12 11.1L8.4 7.5L7 8.9L10.6 12.5L7 16.1L8.4 17.5ZM12 22.5C10.6167 22.5 9.31667 22.2375 8.1 21.7125C6.88333 21.1875 5.825 20.475 4.925 19.575C4.025 18.675 3.3125 17.6167 2.7875 16.4C2.2625 15.1833 2 13.8833 2 12.5C2 11.1167 2.2625 9.81667 2.7875 8.6C3.3125 7.38333 4.025 6.325 4.925 5.425C5.825 4.525 6.88333 3.8125 8.1 3.2875C9.31667 2.7625 10.6167 2.5 12 2.5C13.3833 2.5 14.6833 2.7625 15.9 3.2875C17.1167 3.8125 18.175 4.525 19.075 5.425C19.975 6.325 20.6875 7.38333 21.2125 8.6C21.7375 9.81667 22 11.1167 22 12.5C22 13.8833 21.7375 15.1833 21.2125 16.4C20.6875 17.6167 19.975 18.675 19.075 19.575C18.175 20.475 17.1167 21.1875 15.9 21.7125C14.6833 22.2375 13.3833 22.5 12 22.5ZM12 20.5C14.2333 20.5 16.125 19.725 17.675 18.175C19.225 16.625 20 14.7333 20 12.5C20 10.2667 19.225 8.375 17.675 6.825C16.125 5.275 14.2333 4.5 12 4.5C9.76667 4.5 7.875 5.275 6.325 6.825C4.775 8.375 4 10.2667 4 12.5C4 14.7333 4.775 16.625 6.325 18.175C7.875 19.725 9.76667 20.5 12 20.5Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.66699 14C4.30033 14 3.98644 13.8694 3.72533 13.6083C3.46421 13.3472 3.33366 13.0333 3.33366 12.6667V4H2.66699V2.66667H6.00033V2H10.0003V2.66667H13.3337V4H12.667V12.6667C12.667 13.0333 12.5364 13.3472 12.2753 13.6083C12.0142 13.8694 11.7003 14 11.3337 14H4.66699ZM6.00033 11.3333H7.33366V5.33333H6.00033V11.3333ZM8.66699 11.3333H10.0003V5.33333H8.66699V11.3333Z" fill="#929292"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 494 B |
3
src/frontend/apps/calendars/src/assets/icons/info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 17H13V11H11V17ZM12 9C12.2833 9 12.5208 8.90417 12.7125 8.7125C12.9042 8.52083 13 8.28333 13 8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8C11 8.28333 11.0958 8.52083 11.2875 8.7125C11.4792 8.90417 11.7167 9 12 9ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22ZM12 20C14.2333 20 16.125 19.225 17.675 17.675C19.225 16.125 20 14.2333 20 12C20 9.76667 19.225 7.875 17.675 6.325C16.125 4.775 14.2333 4 12 4C9.76667 4 7.875 4.775 6.325 6.325C4.775 7.875 4 9.76667 4 12C4 14.2333 4.775 16.125 6.325 17.675C7.875 19.225 9.76667 20 12 20Z" fill="#3A3A3A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z" fill="#3A3A3A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 262 B |
3
src/frontend/apps/calendars/src/assets/icons/trash.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.66699 14C4.30033 14 3.98644 13.8694 3.72533 13.6083C3.46421 13.3472 3.33366 13.0333 3.33366 12.6667V4H2.66699V2.66667H6.00033V2H10.0003V2.66667H13.3337V4H12.667V12.6667C12.667 13.0333 12.5364 13.3472 12.2753 13.6083C12.0142 13.8694 11.7003 14 11.3337 14H4.66699ZM6.00033 11.3333H7.33366V5.33333H6.00033V11.3333ZM8.66699 11.3333H10.0003V5.33333H8.66699V11.3333Z" fill="#929292"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 494 B |
3
src/frontend/apps/calendars/src/assets/icons/undo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 19.5V17.5H14.1C15.15 17.5 16.0625 17.1667 16.8375 16.5C17.6125 15.8333 18 15 18 14C18 13 17.6125 12.1667 16.8375 11.5C16.0625 10.8333 15.15 10.5 14.1 10.5H7.8L10.4 13.1L9 14.5L4 9.5L9 4.5L10.4 5.9L7.8 8.5H14.1C15.7167 8.5 17.1042 9.025 18.2625 10.075C19.4208 11.125 20 12.4333 20 14C20 15.5667 19.4208 16.875 18.2625 17.925C17.1042 18.975 15.7167 19.5 14.1 19.5H7Z" fill="#3A3A3A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 19.5V17.5H14.1C15.15 17.5 16.0625 17.1667 16.8375 16.5C17.6125 15.8333 18 15 18 14C18 13 17.6125 12.1667 16.8375 11.5C16.0625 10.8333 15.15 10.5 14.1 10.5H7.8L10.4 13.1L9 14.5L4 9.5L9 4.5L10.4 5.9L7.8 8.5H14.1C15.7167 8.5 17.1042 9.025 18.2625 10.075C19.4208 11.125 20 12.4333 20 14C20 15.5667 19.4208 16.875 18.2625 17.925C17.1042 18.975 15.7167 19.5 14.1 19.5H7Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
454
src/frontend/apps/calendars/src/assets/logo-gouv.svg
Normal file
|
After Width: | Height: | Size: 35 KiB |
4
src/frontend/apps/calendars/src/assets/logo-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="65" viewBox="0 0 64 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M55.3531 19.9525H16.8394C12.465 19.9525 8.06606 23.3947 7.01421 27.6408L4 39.8087V16.1976C4 13.6559 4.58282 11.7415 5.74845 10.4546C6.92902 9.15152 8.56539 8.5 10.6575 8.5H17.0013C17.7783 8.5 18.4508 8.5563 19.0187 8.66891C19.5866 8.78152 20.1096 8.97457 20.5878 9.24805C21.066 9.50544 21.5666 9.8674 22.0897 10.3339L23.3674 11.4681C23.995 11.9989 24.5853 12.377 25.1383 12.6022C25.6912 12.8274 26.3711 12.94 27.1781 12.94H48.025C50.4309 12.94 52.2541 13.6076 53.4945 14.9428C54.607 16.1262 55.2265 17.7961 55.3531 19.9525Z" fill="#C9191E"/>
|
||||
<path d="M11.3531 54.5C8.93219 54.5 7.27319 53.8071 6.37613 52.4213C5.47493 51.0522 5.3552 49.032 6.01696 46.3606L10.6542 27.6409C11.2228 25.3457 13.6005 23.4851 15.9651 23.4851H58.7796C61.1442 23.4851 62.6002 25.3457 62.0316 27.6409L57.3944 46.3606C56.7326 49.032 55.6344 51.0522 54.0997 52.4213C52.5609 53.8071 50.723 54.5 48.586 54.5H11.3531Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1018 B |
@@ -0,0 +1,7 @@
|
||||
export const AnalyticsProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
57
src/frontend/apps/calendars/src/features/api/APIError.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import i18n from "@/features/i18n/initI18n";
|
||||
import { AppError } from "../errors/AppError";
|
||||
|
||||
export class APIError extends Error {
|
||||
data?: any;
|
||||
code: number;
|
||||
|
||||
constructor(code: number, data?: any) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export const errorToString = (error: unknown): string => {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof APIError) {
|
||||
const data = error.data;
|
||||
// If there is a data, it means that the error is a JSON object
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (
|
||||
data?.errors &&
|
||||
Array.isArray(data?.errors) &&
|
||||
data?.errors?.length > 0
|
||||
) {
|
||||
return data.errors[0].detail;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
/**
|
||||
* This is made to handle full text errors from the API like:
|
||||
*
|
||||
* "title": [
|
||||
* "The title field is required."
|
||||
* ]
|
||||
*/
|
||||
return Object.entries(error.data)
|
||||
.map(([, value]) => `${value}`)
|
||||
.join("\n");
|
||||
}
|
||||
// If there is no data, it means that the error is a string, probably a complicated html error.
|
||||
return i18n.t("api.error.unexpected");
|
||||
}
|
||||
// We want to show the error message from the AppError only. Not message from the Error class as they
|
||||
// can be really technical and not helpful for the user. For those we show the generic error message.
|
||||
if (error instanceof AppError) {
|
||||
return error.message;
|
||||
}
|
||||
return i18n.t("api.error.unexpected");
|
||||
};
|
||||
67
src/frontend/apps/calendars/src/features/api/fetchApi.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { baseApiUrl, isJson } from "./utils";
|
||||
import { APIError } from "./APIError";
|
||||
|
||||
/**
|
||||
* Retrieves the CSRF token from the document's cookies.
|
||||
*
|
||||
* @returns {string|null} The CSRF token if found in the cookies, or null if not present.
|
||||
*/
|
||||
function getCSRFToken() {
|
||||
return document.cookie
|
||||
.split(";")
|
||||
.filter((cookie) => cookie.trim().startsWith("csrftoken="))
|
||||
.map((cookie) => cookie.split("=")[1])
|
||||
.pop();
|
||||
}
|
||||
|
||||
export const SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL =
|
||||
"redirect_after_login_url";
|
||||
|
||||
const redirect = (url: string, saveRedirectAfterLoginUrl = true) => {
|
||||
if (saveRedirectAfterLoginUrl) {
|
||||
sessionStorage.setItem(
|
||||
SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL,
|
||||
window.location.href
|
||||
);
|
||||
}
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
export interface fetchAPIOptions {
|
||||
}
|
||||
|
||||
export const fetchAPI = async (
|
||||
input: string,
|
||||
init?: RequestInit & { params?: Record<string, string | number> },
|
||||
options?: fetchAPIOptions
|
||||
) => {
|
||||
const apiUrl = new URL(`${baseApiUrl("1.0")}${input}`);
|
||||
if (init?.params) {
|
||||
Object.entries(init.params).forEach(([key, value]) => {
|
||||
apiUrl.searchParams.set(key, String(value));
|
||||
});
|
||||
}
|
||||
const csrfToken = getCSRFToken();
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
...init,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
...init?.headers,
|
||||
"Content-Type": "application/json",
|
||||
...(csrfToken && { "X-CSRFToken": csrfToken }),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
|
||||
if (isJson(data)) {
|
||||
throw new APIError(response.status, JSON.parse(data));
|
||||
}
|
||||
|
||||
throw new APIError(response.status);
|
||||
};
|
||||
18
src/frontend/apps/calendars/src/features/api/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface ApiConfig {
|
||||
FRONTEND_THEME?: string;
|
||||
FRONTEND_HIDE_GAUFRE?: boolean;
|
||||
FRONTEND_FEEDBACK_BUTTON_SHOW?: boolean;
|
||||
FRONTEND_FEEDBACK_BUTTON_IDLE?: boolean;
|
||||
FRONTEND_FEEDBACK_ITEMS?: Record<string, { url: string }>;
|
||||
FRONTEND_MORE_LINK?: string;
|
||||
FRONTEND_FEEDBACK_MESSAGES_WIDGET_API_URL?: string;
|
||||
FRONTEND_FEEDBACK_MESSAGES_WIDGET_PATH?: string;
|
||||
FRONTEND_FEEDBACK_MESSAGES_WIDGET_CHANNEL?: string;
|
||||
theme_customization?: ThemeCustomization;
|
||||
}
|
||||
|
||||
export interface ThemeCustomization {
|
||||
footer?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
39
src/frontend/apps/calendars/src/features/api/utils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export const errorCauses = async (response: Response, data?: unknown) => {
|
||||
const errorsBody = (await response.json()) as Record<
|
||||
string,
|
||||
string | string[]
|
||||
> | null;
|
||||
|
||||
const causes = errorsBody
|
||||
? Object.entries(errorsBody)
|
||||
.map(([, value]) => value)
|
||||
.flat()
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
cause: causes,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrigin = () => {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_API_ORIGIN ||
|
||||
(typeof window !== "undefined" ? window.location.origin : "")
|
||||
);
|
||||
};
|
||||
export const baseApiUrl = (apiVersion: string = "1.0") => {
|
||||
const origin = getOrigin();
|
||||
return `${origin}/api/v${apiVersion}/`;
|
||||
};
|
||||
|
||||
export const isJson = (str: string) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
76
src/frontend/apps/calendars/src/features/auth/Auth.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { PropsWithChildren, useEffect, useState } from "react";
|
||||
|
||||
import { fetchAPI } from "@/features/api/fetchApi";
|
||||
import { User } from "@/features/auth/types";
|
||||
import { baseApiUrl } from "../api/utils";
|
||||
import { APIError } from "../api/APIError";
|
||||
import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage";
|
||||
|
||||
export const logout = () => {
|
||||
window.location.replace(new URL("logout/", baseApiUrl()).href);
|
||||
};
|
||||
|
||||
export const login = (returnTo?: string) => {
|
||||
const url = new URL("authenticate/", baseApiUrl());
|
||||
if (returnTo) {
|
||||
url.searchParams.set("returnTo", returnTo);
|
||||
}
|
||||
window.location.replace(url.href);
|
||||
};
|
||||
|
||||
interface AuthContextInterface {
|
||||
user?: User | null;
|
||||
init?: () => Promise<User | null>;
|
||||
refreshUser?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const AuthContext = React.createContext<AuthContextInterface>({});
|
||||
|
||||
export const useAuth = () => React.useContext(AuthContext);
|
||||
|
||||
export const Auth = ({
|
||||
children,
|
||||
redirect,
|
||||
}: PropsWithChildren & { redirect?: boolean }) => {
|
||||
const [user, setUser] = useState<User | null>();
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const response = await fetchAPI(`users/me/`);
|
||||
const data = (await response.json()) as User;
|
||||
setUser(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (redirect && error instanceof APIError && error.code === 401) {
|
||||
login();
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshUser = async () => {
|
||||
void init();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void init();
|
||||
}, []);
|
||||
|
||||
if (user === undefined) {
|
||||
return <SpinnerPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
init,
|
||||
refreshUser,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Button } from "@openfun/cunningham-react";
|
||||
import { login } from "../Auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL } from "@/features/api/fetchApi";
|
||||
|
||||
export const LoginButton = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
className="calendars__header__login-button"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
sessionStorage.setItem(
|
||||
SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL,
|
||||
window.location.href
|
||||
);
|
||||
login();
|
||||
}}
|
||||
>
|
||||
{t("login")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Button } from "@openfun/cunningham-react";
|
||||
import { logout } from "../Auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const LogoutButton = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button variant="tertiary" onClick={logout} fullWidth={true}>
|
||||
{t("logout")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
12
src/frontend/apps/calendars/src/features/auth/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Represents user retrieved from the API.
|
||||
* @interface User
|
||||
* @property {string} id - The id of the user.
|
||||
* @property {string} email - The email of the user.
|
||||
* @property {string} name - The name of the user.
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
language: string;
|
||||
}
|
||||
51
src/frontend/apps/calendars/src/features/calendar/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* API functions for calendar operations.
|
||||
*/
|
||||
|
||||
import { fetchAPI } from "@/features/api/fetchApi";
|
||||
|
||||
export interface Calendar {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
is_default: boolean;
|
||||
is_visible: boolean;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch all calendars accessible by the current user.
|
||||
*/
|
||||
export const getCalendars = async (): Promise<Calendar[]> => {
|
||||
const response = await fetchAPI("calendars/");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new calendar.
|
||||
*/
|
||||
export const createCalendar = async (data: {
|
||||
name: string;
|
||||
color?: string;
|
||||
}): Promise<Calendar> => {
|
||||
const response = await fetchAPI("calendars/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle calendar visibility.
|
||||
*/
|
||||
export const toggleCalendarVisibility = async (
|
||||
calendarId: string
|
||||
): Promise<{ is_visible: boolean }> => {
|
||||
const response = await fetchAPI(`calendars/${calendarId}/toggle_visibility/`, {
|
||||
method: "PATCH",
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
.calendar-list {
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&__section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
color: var(--c--theme--colors--primary-500);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.25rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-50);
|
||||
}
|
||||
|
||||
// Override Cunningham checkbox styles for compact display
|
||||
.c__checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c__checkbox__multi-wrapper {
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__color {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-size: 0.625rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
border-radius: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* CalendarList component - List of calendars with visibility toggles.
|
||||
*/
|
||||
|
||||
import { Button, Checkbox } from "@openfun/cunningham-react";
|
||||
|
||||
import { Calendar } from "../api";
|
||||
import { useToggleCalendarVisibility } from "../hooks/useCalendars";
|
||||
|
||||
interface CalendarListProps {
|
||||
calendars: Calendar[];
|
||||
onCreateCalendar: () => void;
|
||||
}
|
||||
|
||||
export const CalendarList = ({
|
||||
calendars,
|
||||
onCreateCalendar,
|
||||
}: CalendarListProps) => {
|
||||
const toggleVisibility = useToggleCalendarVisibility();
|
||||
|
||||
// Ensure calendars is an array
|
||||
const calendarsArray = Array.isArray(calendars) ? calendars : [];
|
||||
|
||||
const ownedCalendars = calendarsArray.filter(
|
||||
(cal) => !cal.name.includes("(partagé)")
|
||||
);
|
||||
const sharedCalendars = calendarsArray.filter((cal) =>
|
||||
cal.name.includes("(partagé)")
|
||||
);
|
||||
|
||||
const handleToggle = (calendarId: string) => {
|
||||
toggleVisibility.mutate(calendarId);
|
||||
};
|
||||
|
||||
const renderCalendarItem = (calendar: Calendar) => (
|
||||
<div key={calendar.id} className="calendar-list__item">
|
||||
<Checkbox
|
||||
checked={calendar.is_visible}
|
||||
onChange={() => handleToggle(calendar.id)}
|
||||
label=""
|
||||
aria-label={`Afficher ${calendar.name}`}
|
||||
/>
|
||||
<span
|
||||
className="calendar-list__color"
|
||||
style={{ backgroundColor: calendar.color }}
|
||||
/>
|
||||
<span className="calendar-list__name" title={calendar.name}>
|
||||
{calendar.name}
|
||||
</span>
|
||||
{calendar.is_default && (
|
||||
<span className="calendar-list__badge">Par défaut</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="calendar-list">
|
||||
<div className="calendar-list__section">
|
||||
<div className="calendar-list__section-header">
|
||||
<span className="calendar-list__section-title">Mes calendriers</span>
|
||||
<button
|
||||
className="calendar-list__add-btn"
|
||||
onClick={onCreateCalendar}
|
||||
title="Créer un calendrier"
|
||||
>
|
||||
<span className="material-icons">add</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="calendar-list__items">
|
||||
{ownedCalendars.map(renderCalendarItem)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sharedCalendars.length > 0 && (
|
||||
<div className="calendar-list__section">
|
||||
<div className="calendar-list__section-header">
|
||||
<span className="calendar-list__section-title">
|
||||
Calendriers partagés
|
||||
</span>
|
||||
</div>
|
||||
<div className="calendar-list__items">
|
||||
{sharedCalendars.map(renderCalendarItem)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
.calendar-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
&__loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 2rem;
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&--loading,
|
||||
&--error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--c--theme--colors--primary-500);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--c--theme--colors--primary-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open-calendar overrides to match La Suite theme
|
||||
.calendar-view__container {
|
||||
// Calendar toolbar
|
||||
.ec-toolbar {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
}
|
||||
|
||||
// Today button
|
||||
.ec-button {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--c--theme--colors--greyscale-300);
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
&.ec-active {
|
||||
background: var(--c--theme--colors--primary-500);
|
||||
border-color: var(--c--theme--colors--primary-500);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
.ec-prev,
|
||||
.ec-next {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// Title
|
||||
.ec-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--c--theme--colors--greyscale-900);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
// Days header
|
||||
.ec-days,
|
||||
.ec-day {
|
||||
border-color: var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
|
||||
.ec-day-head {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
|
||||
// Time slots
|
||||
.ec-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
}
|
||||
|
||||
// Events
|
||||
.ec-event {
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 4px;
|
||||
border: none;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Today highlight
|
||||
.ec-today {
|
||||
background: rgba(var(--c--theme--colors--primary-rgb, 49, 116, 173), 0.1);
|
||||
}
|
||||
|
||||
// Now indicator
|
||||
.ec-now-indicator {
|
||||
border-color: var(--c--theme--colors--danger-500);
|
||||
|
||||
&::before {
|
||||
background: var(--c--theme--colors--danger-500);
|
||||
}
|
||||
}
|
||||
|
||||
// Popup styles
|
||||
.ec-popup {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* CalendarView component using open-calendar (Algoo).
|
||||
* Renders a CalDAV-connected calendar view.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useAuth } from "@/features/auth/Auth";
|
||||
import { createEventModalHandlers, type ModalState } from "./EventModalAdapter";
|
||||
import { useEventModal } from "../hooks/useEventModal";
|
||||
import type { IcsEvent } from 'ts-ics';
|
||||
|
||||
interface CalendarViewProps {
|
||||
selectedDate?: Date;
|
||||
onSelectDate?: (date: Date) => void;
|
||||
}
|
||||
|
||||
export const CalendarView = ({
|
||||
selectedDate = new Date(),
|
||||
onSelectDate,
|
||||
}: CalendarViewProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const calendarRef = useRef<unknown>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [modalState, setModalState] = useState<ModalState>({
|
||||
isOpen: false,
|
||||
mode: 'create',
|
||||
event: null,
|
||||
calendarUrl: '',
|
||||
calendars: [],
|
||||
handleSave: null,
|
||||
handleDelete: null,
|
||||
});
|
||||
const { user } = useAuth();
|
||||
|
||||
// Use the modal hook with state from adapter
|
||||
const modal = useEventModal({
|
||||
calendars: modalState.calendars,
|
||||
initialEvent: modalState.event,
|
||||
initialCalendarUrl: modalState.calendarUrl,
|
||||
onSubmit: async (event: IcsEvent, calendarUrl: string) => {
|
||||
if (modalState.handleSave) {
|
||||
await modalState.handleSave({ calendarUrl, event });
|
||||
setModalState(prev => ({ ...prev, isOpen: false }));
|
||||
}
|
||||
},
|
||||
onDelete: async (event: IcsEvent, calendarUrl: string) => {
|
||||
if (modalState.handleDelete) {
|
||||
await modalState.handleDelete({ calendarUrl, event });
|
||||
setModalState(prev => ({ ...prev, isOpen: false }));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Sync modal state with adapter state
|
||||
useEffect(() => {
|
||||
if (modalState.isOpen) {
|
||||
// Always call open to update all state (event, mode, calendarUrl, calendars)
|
||||
modal.open(modalState.event, modalState.mode, modalState.calendarUrl, modalState.calendars);
|
||||
} else if (modal.isOpen) {
|
||||
modal.close();
|
||||
}
|
||||
}, [modalState.isOpen, modalState.event, modalState.mode, modalState.calendarUrl, modalState.calendars]);
|
||||
|
||||
useEffect(() => {
|
||||
const initCalendar = async () => {
|
||||
if (!containerRef.current || !user) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Dynamically import open-calendar to avoid SSR issues (uses browser-only globals)
|
||||
const { createCalendar } = await import("open-dav-calendar");
|
||||
|
||||
// Clear previous calendar instance
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = "";
|
||||
}
|
||||
|
||||
// CalDAV server URL - proxied through Django backend
|
||||
// The proxy handles authentication via session cookies
|
||||
// open-calendar will discover calendars from this URL
|
||||
const caldavServerUrl = `${process.env.NEXT_PUBLIC_API_ORIGIN}/api/v1.0/caldav/`;
|
||||
|
||||
// Create calendar with CalDAV source
|
||||
// Use fetchOptions with credentials to include cookies
|
||||
const calendar = await createCalendar(
|
||||
[
|
||||
{
|
||||
serverUrl: caldavServerUrl,
|
||||
fetchOptions: {
|
||||
credentials: "include" as RequestCredentials,
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[], // No address books for now
|
||||
containerRef.current,
|
||||
{
|
||||
view: "timeGridWeek",
|
||||
views: ["dayGridMonth", "timeGridWeek", "timeGridDay", "listWeek"],
|
||||
locale: "fr",
|
||||
date: selectedDate,
|
||||
editable: true,
|
||||
// Use custom EventEditHandlers that update React state
|
||||
...createEventModalHandlers(setModalState, []),
|
||||
onEventCreated: (info) => {
|
||||
console.log("Event created:", info);
|
||||
},
|
||||
onEventUpdated: (info) => {
|
||||
console.log("Event updated:", info);
|
||||
},
|
||||
onEventDeleted: (info) => {
|
||||
console.log("Event deleted:", info);
|
||||
},
|
||||
},
|
||||
{
|
||||
// French translations
|
||||
calendar: {
|
||||
today: "Aujourd'hui",
|
||||
month: "Mois",
|
||||
week: "Semaine",
|
||||
day: "Jour",
|
||||
list: "Liste",
|
||||
},
|
||||
event: {
|
||||
edit: "Modifier",
|
||||
delete: "Supprimer",
|
||||
save: "Enregistrer",
|
||||
cancel: "Annuler",
|
||||
title: "Titre",
|
||||
description: "Description",
|
||||
location: "Lieu",
|
||||
startDate: "Date de début",
|
||||
endDate: "Date de fin",
|
||||
allDay: "Journée entière",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
calendarRef.current = calendar;
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to initialize calendar:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Erreur lors du chargement du calendrier"
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initCalendar();
|
||||
|
||||
return () => {
|
||||
// Cleanup
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = "";
|
||||
}
|
||||
calendarRef.current = null;
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
// Update calendar date when selectedDate changes
|
||||
useEffect(() => {
|
||||
if (calendarRef.current && selectedDate) {
|
||||
// open-calendar may have a method to set date
|
||||
// This depends on the CalendarElement API
|
||||
}
|
||||
}, [selectedDate]);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="calendar-view calendar-view--loading">
|
||||
<p>Connexion requise pour afficher le calendrier</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="calendar-view calendar-view--error">
|
||||
<p>{error}</p>
|
||||
<button onClick={() => window.location.reload()}>Réessayer</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="calendar-view">
|
||||
{isLoading && (
|
||||
<div className="calendar-view__loading">
|
||||
<p>Chargement du calendrier...</p>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="calendar-view__container"
|
||||
style={{ opacity: isLoading ? 0 : 1 }}
|
||||
/>
|
||||
{modal.Modal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Modal component for creating a new calendar.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalSize,
|
||||
useModal,
|
||||
} from "@openfun/cunningham-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCreateCalendar } from "../hooks/useCalendars";
|
||||
import { addToast, ToasterItem } from "@/features/ui/components/toaster/Toaster";
|
||||
import { errorToString } from "@/features/api/APIError";
|
||||
|
||||
export const useCreateCalendarModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const modal = useModal();
|
||||
const createCalendar = useCreateCalendar();
|
||||
const [name, setName] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (modal.isOpen) {
|
||||
setName("");
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [modal.isOpen]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await createCalendar.mutateAsync({
|
||||
name: name.trim(),
|
||||
});
|
||||
addToast(
|
||||
<ToasterItem>
|
||||
<span>{t("calendar.created_success", { name: name.trim(), defaultValue: `Calendrier "${name.trim()}" créé avec succès` })}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
setName("");
|
||||
modal.close();
|
||||
} catch (error) {
|
||||
console.error("Failed to create calendar:", error);
|
||||
const errorMessage = errorToString(error);
|
||||
addToast(
|
||||
<ToasterItem type="error">
|
||||
<span>{errorMessage || t("calendar.created_error", { defaultValue: "Erreur lors de la création du calendrier" })}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setName("");
|
||||
modal.close();
|
||||
};
|
||||
|
||||
return {
|
||||
...modal,
|
||||
Modal: (
|
||||
<Modal
|
||||
{...modal}
|
||||
title={t("calendar.create_modal.title", { defaultValue: "Créer un nouveau calendrier" })}
|
||||
size={ModalSize.SMALL}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
label={t("calendar.create_modal.name_label", { defaultValue: "Nom du calendrier" })}
|
||||
value={name}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
placeholder={t("calendar.create_modal.name_placeholder", { defaultValue: "Mon calendrier" })}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: "1rem", justifyContent: "flex-end", marginTop: "1.5rem" }}>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("common.cancel", { defaultValue: "Annuler" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={!name.trim() || isSubmitting}
|
||||
>
|
||||
{isSubmitting
|
||||
? t("common.creating", { defaultValue: "Création..." })
|
||||
: t("common.create", { defaultValue: "Créer" })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
.event-modal {
|
||||
&__feature-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 2rem;
|
||||
border: none;
|
||||
background: #f1f3f4;
|
||||
color: #5f6368;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #e8eaeb;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
|
||||
&:hover {
|
||||
background: #d2e3fc;
|
||||
}
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility classes for layout
|
||||
.event-modal-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&--wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--justify-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--gap-2rem {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
&--gap-1rem {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&--gap-0-5rem {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&--margin-bottom-1rem {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&--margin-top-2rem {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
&--margin-left-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&--margin-left-2rem {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
&--margin-top-0-5rem {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import { Modal, Button, Input, TextArea, Select } from '@openfun/cunningham-react';
|
||||
import type { IcsEvent, IcsRecurrenceRule } from 'ts-ics';
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { RecurrenceEditor } from './RecurrenceEditor';
|
||||
|
||||
// Helper function to check if event is all-day (same logic as open-dav-calendar)
|
||||
function isEventAllDay(event: IcsEvent): boolean {
|
||||
return event.start.type === 'DATE' || (event.end?.type === 'DATE');
|
||||
}
|
||||
|
||||
// Calendar type from open-calendar (exported for use in other components)
|
||||
export interface Calendar {
|
||||
url: string;
|
||||
uid?: unknown;
|
||||
displayName?: string;
|
||||
calendarColor?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface EventModalProps {
|
||||
isOpen: boolean;
|
||||
mode: 'create' | 'edit';
|
||||
calendars: Calendar[];
|
||||
selectedEvent?: IcsEvent | null;
|
||||
calendarUrl: string;
|
||||
onSubmit: (event: IcsEvent, calendarUrl: string) => Promise<void>;
|
||||
onAllDayChange: (isAllDay: boolean) => void;
|
||||
onClose: () => void;
|
||||
onDelete?: (event: IcsEvent, calendarUrl: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const VISIBILITY_OPTIONS = [
|
||||
{ value: 'default', label: 'Par défaut' },
|
||||
{ value: 'public', label: 'Public' },
|
||||
{ value: 'private', label: 'Privé' },
|
||||
] as const;
|
||||
|
||||
export function EventModal({
|
||||
isOpen,
|
||||
mode,
|
||||
calendars,
|
||||
selectedEvent,
|
||||
calendarUrl,
|
||||
onSubmit,
|
||||
onAllDayChange,
|
||||
onClose,
|
||||
onDelete,
|
||||
}: EventModalProps) {
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
const [formData, setFormData] = useState<Partial<IcsEvent & { startTime?: string; endTime?: string }>>({});
|
||||
const [activeFeatures, setActiveFeatures] = useState<Set<string>>(new Set());
|
||||
const [selectedCalendarUrl, setSelectedCalendarUrl] = useState<string>(calendarUrl);
|
||||
const [allDay, setAllDay] = useState<boolean>(false);
|
||||
|
||||
// Helper to get local date from IcsDateObject
|
||||
const getLocalDate = (dateObj: { date: Date; local?: { date: Date; timezone: string } }): Date => {
|
||||
return dateObj.local?.date || dateObj.date;
|
||||
};
|
||||
|
||||
// Format date for input
|
||||
const formatDateForInput = (date: Date): string => {
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// Format time for input
|
||||
const formatTimeForInput = (date: Date): string => {
|
||||
return date.toTimeString().slice(0, 5);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEvent) {
|
||||
const eventAllDay = isEventAllDay(selectedEvent);
|
||||
const startDate = getLocalDate(selectedEvent.start);
|
||||
const endDate = selectedEvent.end ? getLocalDate(selectedEvent.end) : startDate;
|
||||
|
||||
setAllDay(eventAllDay);
|
||||
setFormData({
|
||||
...selectedEvent,
|
||||
summary: selectedEvent.summary || '',
|
||||
startTime: eventAllDay ? undefined : formatTimeForInput(startDate),
|
||||
endTime: eventAllDay ? undefined : formatTimeForInput(endDate),
|
||||
});
|
||||
setSelectedCalendarUrl(calendarUrl);
|
||||
} else {
|
||||
const now = new Date();
|
||||
const endTime = new Date(now.getTime() + 30 * 60 * 1000);
|
||||
setAllDay(false);
|
||||
setFormData({
|
||||
summary: '',
|
||||
start: {
|
||||
type: 'DATE-TIME',
|
||||
date: now,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
end: {
|
||||
type: 'DATE-TIME',
|
||||
date: endTime,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
startTime: formatTimeForInput(now),
|
||||
endTime: formatTimeForInput(endTime),
|
||||
});
|
||||
setSelectedCalendarUrl(calendars[0]?.url || '');
|
||||
}
|
||||
}, [selectedEvent, calendars, calendarUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
titleInputRef.current?.focus();
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const startDate = formData.start ? getLocalDate(formData.start) : new Date();
|
||||
const endDate = formData.end ? getLocalDate(formData.end) : startDate;
|
||||
|
||||
const toggleFeature = (feature: string) => {
|
||||
if (feature === 'allDay') {
|
||||
const newAllDay = !allDay;
|
||||
setAllDay(newAllDay);
|
||||
onAllDayChange(newAllDay);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveFeatures(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(feature)) {
|
||||
next.delete(feature);
|
||||
} else {
|
||||
next.add(feature);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.summary) return;
|
||||
|
||||
const startDateValue = new Date(`${formatDateForInput(startDate)}T${allDay ? '00:00:00' : formData.startTime || '00:00:00'}`);
|
||||
const endDateValue = new Date(`${formatDateForInput(endDate)}T${allDay ? '00:00:00' : formData.endTime || '00:00:00'}`);
|
||||
|
||||
const updatedEvent: IcsEvent = {
|
||||
...(selectedEvent || {}),
|
||||
uid: selectedEvent?.uid || `event-${Date.now()}`,
|
||||
summary: formData.summary || '',
|
||||
location: formData.location || undefined,
|
||||
description: formData.description || formData.notes || undefined,
|
||||
start: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: startDateValue,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
end: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: endDateValue,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
recurrenceRule: formData.recurrenceRule,
|
||||
};
|
||||
|
||||
await onSubmit(updatedEvent, selectedCalendarUrl);
|
||||
};
|
||||
|
||||
const renderFeatureContent = () => {
|
||||
return (
|
||||
<>
|
||||
{activeFeatures.has('calendar') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<Select
|
||||
label="Calendrier"
|
||||
name="calendar-select"
|
||||
value={selectedCalendarUrl}
|
||||
onChange={(e) => setSelectedCalendarUrl(e.target.value)}
|
||||
options={calendars.map(cal => ({
|
||||
value: cal.url,
|
||||
label: cal.displayName || cal.url
|
||||
}))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFeatures.has('repeat') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<RecurrenceEditor
|
||||
value={formData.recurrenceRule}
|
||||
onChange={(recurrenceRule) => {
|
||||
setFormData(prev => ({ ...prev, recurrenceRule }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFeatures.has('location') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<Input
|
||||
label="Lieu"
|
||||
placeholder="Ajouter un lieu"
|
||||
value={formData.location || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, location: e.target.value }))}
|
||||
icon={<span className="material-icons">location_on</span>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFeatures.has('notes') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<TextArea
|
||||
label="Notes"
|
||||
placeholder="Ajouter des notes"
|
||||
value={formData.description || formData.notes || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value, notes: e.target.value }))}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const selectedCalendar = calendars.find(cal => cal.url === selectedCalendarUrl);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={mode === 'create' ? 'Créer un événement' : 'Modifier l\'événement'}
|
||||
>
|
||||
<div className="event-modal-layout event-modal-layout--gap-2rem">
|
||||
<Input
|
||||
label="Titre"
|
||||
value={formData.summary || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, summary: e.target.value }))}
|
||||
ref={titleInputRef}
|
||||
/>
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-1rem">
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="date"
|
||||
label="Date de début"
|
||||
name="event-start-date"
|
||||
value={formatDateForInput(startDate)}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
const currentTime = allDay ? '00:00:00' : (formData.startTime || formatTimeForInput(startDate));
|
||||
const [hours, minutes] = currentTime.split(':');
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
start: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
startTime: allDay ? undefined : currentTime,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{!allDay && (
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
label="Heure de début"
|
||||
name="event-start-time"
|
||||
value={formData.startTime || formatTimeForInput(startDate)}
|
||||
onChange={(e) => {
|
||||
const [hours, minutes] = e.target.value.split(':');
|
||||
const newDate = new Date(startDate);
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
start: {
|
||||
type: 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
startTime: e.target.value,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-1rem">
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="date"
|
||||
label="Date de fin"
|
||||
name="event-end-date"
|
||||
value={formatDateForInput(endDate)}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
const currentTime = allDay ? '00:00:00' : (formData.endTime || formatTimeForInput(endDate));
|
||||
const [hours, minutes] = currentTime.split(':');
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
end: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
endTime: allDay ? undefined : currentTime,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{!allDay && (
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
label="Heure de fin"
|
||||
name="event-end-time"
|
||||
value={formData.endTime || formatTimeForInput(endDate)}
|
||||
onChange={(e) => {
|
||||
const [hours, minutes] = e.target.value.split(':');
|
||||
const newDate = new Date(endDate);
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
end: {
|
||||
type: 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
endTime: e.target.value,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderFeatureContent()}
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-0-5rem event-modal-layout--wrap">
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('calendar') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('calendar')}
|
||||
style={selectedCalendar?.calendarColor ? {
|
||||
backgroundColor: selectedCalendar.calendarColor,
|
||||
color: 'white',
|
||||
} : undefined}
|
||||
>
|
||||
<span className="material-icons">event_note</span>
|
||||
{selectedCalendar?.displayName || selectedCalendar?.url || 'Calendrier'}
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('visibility') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('visibility')}
|
||||
>
|
||||
<span className="material-icons">visibility</span>
|
||||
Visibilité
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('repeat') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('repeat')}
|
||||
>
|
||||
<span className="material-icons">repeat</span>
|
||||
Répéter
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${allDay ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('allDay')}
|
||||
>
|
||||
<span className="material-icons">today</span>
|
||||
Toute la journée
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('invites') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('invites')}
|
||||
>
|
||||
<span className="material-icons">people</span>
|
||||
Invités
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('location') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('location')}
|
||||
>
|
||||
<span className="material-icons">location_on</span>
|
||||
Lieu
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('visio') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('visio')}
|
||||
>
|
||||
<span className="material-icons">videocam</span>
|
||||
Visio
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('notification') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('notification')}
|
||||
>
|
||||
<span className="material-icons">notifications</span>
|
||||
Rappel
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('notes') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('notes')}
|
||||
>
|
||||
<span className="material-icons">notes</span>
|
||||
Notes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--justify-space-between event-modal-layout--margin-top-2rem">
|
||||
{mode === 'edit' && selectedEvent && (
|
||||
<Button
|
||||
type="button"
|
||||
color="danger"
|
||||
onClick={() => onDelete?.(selectedEvent, selectedCalendarUrl)}
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-1rem event-modal-layout--margin-left-auto">
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
{mode === 'create' ? 'Créer' : 'Enregistrer'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Adapter that bridges open-calendar's EventEditHandlers with React state.
|
||||
* The handlers update state, and the modal is rendered normally in React tree.
|
||||
*/
|
||||
|
||||
import type { IcsEvent } from 'ts-ics';
|
||||
import type { OpenCalendar } from '../hooks/useEventModal';
|
||||
|
||||
interface EventEditHandlers {
|
||||
onCreateEvent: (info: EventEditCreateInfo) => void;
|
||||
onSelectEvent: (info: EventEditSelectInfo) => void;
|
||||
onMoveResizeEvent: (info: EventEditMoveResizeInfo) => void;
|
||||
onDeleteEvent: (info: EventEditDeleteInfo) => void;
|
||||
}
|
||||
|
||||
interface EventEditCreateInfo {
|
||||
jsEvent: Event;
|
||||
userContact?: any;
|
||||
event: IcsEvent;
|
||||
calendars: OpenCalendar[];
|
||||
vCards: any[];
|
||||
handleCreate: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
interface EventEditSelectInfo {
|
||||
jsEvent: Event;
|
||||
userContact?: any;
|
||||
calendarUrl: string;
|
||||
event: IcsEvent;
|
||||
recurringEvent?: IcsEvent;
|
||||
calendars: OpenCalendar[];
|
||||
vCards: any[];
|
||||
handleUpdate: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
handleDelete: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
interface EventEditMoveResizeInfo {
|
||||
jsEvent: Event;
|
||||
calendarUrl: string;
|
||||
userContact?: any;
|
||||
event: IcsEvent;
|
||||
recurringEvent?: IcsEvent;
|
||||
start: Date;
|
||||
end: Date;
|
||||
handleUpdate: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
interface EventEditDeleteInfo {
|
||||
jsEvent: Event;
|
||||
calendarUrl: string;
|
||||
userContact?: any;
|
||||
event: IcsEvent;
|
||||
recurringEvent?: IcsEvent;
|
||||
handleDelete: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
export interface ModalState {
|
||||
isOpen: boolean;
|
||||
mode: 'create' | 'edit';
|
||||
event: IcsEvent | null;
|
||||
calendarUrl: string;
|
||||
calendars: OpenCalendar[];
|
||||
handleSave: ((event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>) | null;
|
||||
handleDelete: ((event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>) | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates EventEditHandlers that update the provided state setter.
|
||||
*/
|
||||
export function createEventModalHandlers(
|
||||
setState: (state: ModalState) => void,
|
||||
calendars: OpenCalendar[] | (() => OpenCalendar[])
|
||||
): EventEditHandlers {
|
||||
const getCalendars = (): OpenCalendar[] => {
|
||||
return typeof calendars === 'function' ? calendars() : calendars;
|
||||
};
|
||||
|
||||
return {
|
||||
onCreateEvent: ({ event, calendars: calList, handleCreate }: EventEditCreateInfo) => {
|
||||
setState({
|
||||
isOpen: true,
|
||||
mode: 'create',
|
||||
event,
|
||||
calendarUrl: calList[0]?.url || '',
|
||||
calendars: calList,
|
||||
handleSave: handleCreate,
|
||||
handleDelete: null,
|
||||
});
|
||||
},
|
||||
onSelectEvent: ({ calendarUrl, event, calendars: calList, handleUpdate, handleDelete }: EventEditSelectInfo) => {
|
||||
setState({
|
||||
isOpen: true,
|
||||
mode: 'edit',
|
||||
event,
|
||||
calendarUrl,
|
||||
calendars: calList,
|
||||
handleSave: handleUpdate,
|
||||
handleDelete,
|
||||
});
|
||||
},
|
||||
onMoveResizeEvent: ({ calendarUrl, event, start, end, handleUpdate }: EventEditMoveResizeInfo) => {
|
||||
const newEvent = { ...event };
|
||||
const startDelta = start.getTime() - event.start.date.getTime();
|
||||
newEvent.start = { ...newEvent.start, date: new Date(event.start.date.getTime() + startDelta) };
|
||||
if (event.end) {
|
||||
const endDelta = end.getTime() - event.end.date.getTime();
|
||||
newEvent.end = { ...newEvent.end, date: new Date(event.end.date.getTime() + endDelta) };
|
||||
}
|
||||
handleUpdate({ calendarUrl, event: newEvent });
|
||||
},
|
||||
onDeleteEvent: ({ calendarUrl, event, handleDelete }: EventEditDeleteInfo) => {
|
||||
handleDelete({ calendarUrl, event });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.calendar-left-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--c--theme--colors--greyscale-000);
|
||||
border-right: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
overflow-y: auto;
|
||||
|
||||
&__create {
|
||||
padding: 1rem 0.75rem;
|
||||
|
||||
.c__button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
background-color: var(--c--theme--colors--greyscale-200);
|
||||
margin: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* LeftPanel component - Calendar sidebar with mini calendar and calendar list.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@openfun/cunningham-react";
|
||||
|
||||
import { Calendar } from "../api";
|
||||
import { CalendarList } from "./CalendarList";
|
||||
import { MiniCalendar } from "./MiniCalendar";
|
||||
|
||||
interface LeftPanelProps {
|
||||
calendars: Calendar[];
|
||||
selectedDate: Date;
|
||||
onDateSelect: (date: Date) => void;
|
||||
onCreateEvent: () => void;
|
||||
onCreateCalendar: () => void;
|
||||
}
|
||||
|
||||
export const LeftPanel = ({
|
||||
calendars,
|
||||
selectedDate,
|
||||
onDateSelect,
|
||||
onCreateEvent,
|
||||
onCreateCalendar,
|
||||
}: LeftPanelProps) => {
|
||||
return (
|
||||
<div className="calendar-left-panel">
|
||||
<div className="calendar-left-panel__create">
|
||||
<Button onClick={onCreateEvent} icon={<span className="material-icons">add</span>}>
|
||||
Créer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<MiniCalendar selectedDate={selectedDate} onDateSelect={onDateSelect} />
|
||||
|
||||
<div className="calendar-left-panel__divider" />
|
||||
|
||||
<CalendarList calendars={calendars} onCreateCalendar={onCreateCalendar} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
.mini-calendar {
|
||||
padding: 0.75rem;
|
||||
user-select: none;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__month-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
}
|
||||
|
||||
&__nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&__weekday {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.5rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
&__day {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
&--outside {
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
}
|
||||
|
||||
&--today {
|
||||
background-color: var(--c--theme--colors--primary-100);
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background-color: var(--c--theme--colors--primary-500);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--primary-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* MiniCalendar component - A small month calendar for date navigation.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
addMonths,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
format,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
subMonths,
|
||||
} from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
|
||||
interface MiniCalendarProps {
|
||||
selectedDate: Date;
|
||||
onDateSelect: (date: Date) => void;
|
||||
}
|
||||
|
||||
export const MiniCalendar = ({
|
||||
selectedDate,
|
||||
onDateSelect,
|
||||
}: MiniCalendarProps) => {
|
||||
const [viewDate, setViewDate] = useState(selectedDate);
|
||||
|
||||
const days = useMemo(() => {
|
||||
const monthStart = startOfMonth(viewDate);
|
||||
const monthEnd = endOfMonth(viewDate);
|
||||
const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 });
|
||||
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
|
||||
|
||||
return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
|
||||
}, [viewDate]);
|
||||
|
||||
const weekDays = ["L", "M", "M", "J", "V", "S", "D"];
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
setViewDate(subMonths(viewDate, 1));
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
setViewDate(addMonths(viewDate, 1));
|
||||
};
|
||||
|
||||
const handleDayClick = (day: Date) => {
|
||||
onDateSelect(day);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mini-calendar">
|
||||
<div className="mini-calendar__header">
|
||||
<button
|
||||
className="mini-calendar__nav-btn"
|
||||
onClick={handlePrevMonth}
|
||||
aria-label="Mois précédent"
|
||||
>
|
||||
<span className="material-icons">chevron_left</span>
|
||||
</button>
|
||||
<span className="mini-calendar__month-title">
|
||||
{format(viewDate, "MMMM yyyy", { locale: fr })}
|
||||
</span>
|
||||
<button
|
||||
className="mini-calendar__nav-btn"
|
||||
onClick={handleNextMonth}
|
||||
aria-label="Mois suivant"
|
||||
>
|
||||
<span className="material-icons">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mini-calendar__weekdays">
|
||||
{weekDays.map((day, index) => (
|
||||
<div key={index} className="mini-calendar__weekday">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mini-calendar__days">
|
||||
{days.map((day, index) => {
|
||||
const isCurrentMonth = isSameMonth(day, viewDate);
|
||||
const isSelected = isSameDay(day, selectedDate);
|
||||
const isToday = isSameDay(day, new Date());
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={`mini-calendar__day ${
|
||||
!isCurrentMonth ? "mini-calendar__day--outside" : ""
|
||||
} ${isSelected ? "mini-calendar__day--selected" : ""} ${
|
||||
isToday ? "mini-calendar__day--today" : ""
|
||||
}`}
|
||||
onClick={() => handleDayClick(day)}
|
||||
>
|
||||
{format(day, "d")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
.recurrence-editor {
|
||||
&__weekday-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f3f4;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #e8eaeb;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: #1a73e8;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #1557b0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility classes for layout
|
||||
.recurrence-editor-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--gap-1rem {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&--gap-0-5rem {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&--margin-left-2rem {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
&--margin-top-0-5rem {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { Select, Input } from '@openfun/cunningham-react';
|
||||
import { useState } from 'react';
|
||||
import type { IcsRecurrenceRule } from 'ts-ics';
|
||||
|
||||
const WEEKDAYS = [
|
||||
{ value: 'MO', label: 'L' },
|
||||
{ value: 'TU', label: 'M' },
|
||||
{ value: 'WE', label: 'M' },
|
||||
{ value: 'TH', label: 'J' },
|
||||
{ value: 'FR', label: 'V' },
|
||||
{ value: 'SA', label: 'S' },
|
||||
{ value: 'SU', label: 'D' },
|
||||
] as const;
|
||||
|
||||
const RECURRENCE_OPTIONS = [
|
||||
{ value: 'NONE', label: 'Non' },
|
||||
{ value: 'DAILY', label: 'Tous les jours' },
|
||||
{ value: 'WEEKLY', label: 'Toutes les semaines' },
|
||||
{ value: 'MONTHLY', label: 'Tous les mois' },
|
||||
{ value: 'YEARLY', label: 'Tous les ans' },
|
||||
{ value: 'CUSTOM', label: 'Personnalisé...' },
|
||||
] as const;
|
||||
|
||||
interface RecurrenceEditorProps {
|
||||
value?: IcsRecurrenceRule;
|
||||
onChange: (rule: IcsRecurrenceRule | undefined) => void;
|
||||
}
|
||||
|
||||
export function RecurrenceEditor({ value, onChange }: RecurrenceEditorProps) {
|
||||
const [isCustom, setIsCustom] = useState(() => {
|
||||
if (!value) return false;
|
||||
return value.interval !== 1 || value.byDay?.length || value.count || value.until;
|
||||
});
|
||||
|
||||
const getSimpleValue = () => {
|
||||
if (!value) return 'NONE';
|
||||
if (isCustom) return 'CUSTOM';
|
||||
return value.freq;
|
||||
};
|
||||
|
||||
const handleSimpleChange = (newValue: string) => {
|
||||
if (newValue === 'NONE') {
|
||||
setIsCustom(false);
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === 'CUSTOM') {
|
||||
setIsCustom(true);
|
||||
onChange({
|
||||
freq: 'WEEKLY',
|
||||
interval: 1
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCustom(false);
|
||||
onChange({
|
||||
freq: newValue as IcsRecurrenceRule['freq'],
|
||||
interval: 1,
|
||||
byDay: undefined,
|
||||
count: undefined,
|
||||
until: undefined
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (updates: Partial<IcsRecurrenceRule>) => {
|
||||
onChange({
|
||||
freq: 'WEEKLY',
|
||||
interval: 1,
|
||||
...value,
|
||||
...updates
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--gap-1rem">
|
||||
<Select
|
||||
label="Répéter"
|
||||
value={getSimpleValue()}
|
||||
options={RECURRENCE_OPTIONS}
|
||||
onChange={(e) => handleSimpleChange(e.target.value)}
|
||||
/>
|
||||
|
||||
{isCustom && (
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--gap-1rem recurrence-editor-layout--margin-left-2rem">
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--row recurrence-editor-layout--align-center recurrence-editor-layout--gap-1rem">
|
||||
<span>Répéter tous les</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={value?.interval || 1}
|
||||
onChange={(e) => handleChange({ interval: parseInt(e.target.value) })}
|
||||
style={{ width: '80px' }}
|
||||
/>
|
||||
<Select
|
||||
value={value?.freq || 'WEEKLY'}
|
||||
options={[
|
||||
{ value: 'DAILY', label: 'jours' },
|
||||
{ value: 'WEEKLY', label: 'semaines' },
|
||||
{ value: 'MONTHLY', label: 'mois' },
|
||||
{ value: 'YEARLY', label: 'années' },
|
||||
]}
|
||||
onChange={(e) => handleChange({ freq: e.target.value as IcsRecurrenceRule['freq'] })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{value?.freq === 'WEEKLY' && (
|
||||
<div className="recurrence-editor-layout">
|
||||
<span>Répéter le</span>
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--row recurrence-editor-layout--gap-0-5rem recurrence-editor-layout--margin-top-0-5rem">
|
||||
{WEEKDAYS.map(day => {
|
||||
const isSelected = value?.byDay?.includes(day.value) || false;
|
||||
return (
|
||||
<button
|
||||
key={day.value}
|
||||
className={`recurrence-editor__weekday-button ${isSelected ? 'recurrence-editor__weekday-button--selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const byDay = value?.byDay || [];
|
||||
handleChange({
|
||||
byDay: byDay.includes(day.value)
|
||||
? byDay.filter(d => d !== day.value)
|
||||
: [...byDay, day.value]
|
||||
});
|
||||
}}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="recurrence-editor-layout">
|
||||
<span>Se termine</span>
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--gap-0-5rem recurrence-editor-layout--margin-top-0-5rem">
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--row recurrence-editor-layout--align-center recurrence-editor-layout--gap-0-5rem">
|
||||
<input
|
||||
type="radio"
|
||||
name="end-type"
|
||||
checked={!value?.count && !value?.until}
|
||||
onChange={() => handleChange({ count: undefined, until: undefined })}
|
||||
/>
|
||||
<span>Jamais</span>
|
||||
</div>
|
||||
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--row recurrence-editor-layout--align-center recurrence-editor-layout--gap-0-5rem">
|
||||
<input
|
||||
type="radio"
|
||||
name="end-type"
|
||||
checked={!!value?.until}
|
||||
onChange={() => handleChange({ until: new Date(), count: undefined })}
|
||||
/>
|
||||
<span>Le</span>
|
||||
{value?.until && (
|
||||
<Input
|
||||
type="date"
|
||||
value={value.until.date ? new Date(value.until.date).toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => handleChange({
|
||||
until: {
|
||||
type: 'DATE-TIME',
|
||||
date: new Date(e.target.value),
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="recurrence-editor-layout recurrence-editor-layout--row recurrence-editor-layout--align-center recurrence-editor-layout--gap-0-5rem">
|
||||
<input
|
||||
type="radio"
|
||||
name="end-type"
|
||||
checked={!!value?.count}
|
||||
onChange={() => handleChange({ count: 1, until: undefined })}
|
||||
/>
|
||||
<span>Après</span>
|
||||
{value?.count !== undefined && (
|
||||
<>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={value.count}
|
||||
onChange={(e) => handleChange({ count: parseInt(e.target.value) })}
|
||||
style={{ width: '80px' }}
|
||||
/>
|
||||
<span>occurrences</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { CalendarList } from "./CalendarList";
|
||||
export { CalendarView } from "./CalendarView";
|
||||
export { LeftPanel } from "./LeftPanel";
|
||||
export { MiniCalendar } from "./MiniCalendar";
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* React Query hooks for calendar operations.
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
Calendar,
|
||||
createCalendar,
|
||||
getCalendars,
|
||||
toggleCalendarVisibility,
|
||||
} from "../api";
|
||||
|
||||
const CALENDARS_KEY = ["calendars"];
|
||||
|
||||
/**
|
||||
* Hook to fetch all calendars.
|
||||
*/
|
||||
export const useCalendars = () => {
|
||||
return useQuery<Calendar[]>({
|
||||
queryKey: CALENDARS_KEY,
|
||||
queryFn: getCalendars,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to create a new calendar.
|
||||
*/
|
||||
export const useCreateCalendar = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createCalendar,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CALENDARS_KEY });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to toggle calendar visibility.
|
||||
*/
|
||||
export const useToggleCalendarVisibility = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: toggleCalendarVisibility,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CALENDARS_KEY });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Simple wrapper around useEventModal for the "Créer" button.
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { IcsEvent } from 'ts-ics';
|
||||
import { useEventModal, apiCalendarToOpenCalendar } from './useEventModal';
|
||||
import { Calendar as ApiCalendar } from '../api';
|
||||
|
||||
interface UseCreateEventModalProps {
|
||||
calendars?: ApiCalendar[] | null;
|
||||
selectedDate?: Date;
|
||||
}
|
||||
|
||||
export const useCreateEventModal = ({ calendars, selectedDate }: UseCreateEventModalProps) => {
|
||||
const openCalendars = useMemo(() => {
|
||||
if (!Array.isArray(calendars)) return [];
|
||||
return calendars.map(apiCalendarToOpenCalendar);
|
||||
}, [calendars]);
|
||||
|
||||
const initialEvent = useMemo((): IcsEvent | null => {
|
||||
if (!selectedDate) return null;
|
||||
const start = new Date(selectedDate);
|
||||
start.setHours(9, 0, 0, 0);
|
||||
const end = new Date(start);
|
||||
end.setHours(10, 0, 0, 0);
|
||||
return {
|
||||
uid: `event-${Date.now()}`,
|
||||
summary: '',
|
||||
start: { type: 'DATE-TIME', date: start, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone },
|
||||
end: { type: 'DATE-TIME', date: end, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone },
|
||||
};
|
||||
}, [selectedDate]);
|
||||
|
||||
const modal = useEventModal({
|
||||
calendars: openCalendars,
|
||||
initialEvent,
|
||||
initialCalendarUrl: Array.isArray(calendars) && calendars[0]?.id ? calendars[0].id : '',
|
||||
});
|
||||
|
||||
return {
|
||||
...modal,
|
||||
open: () => modal.open(initialEvent, 'create', Array.isArray(calendars) && calendars[0]?.id ? calendars[0].id : ''),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Unified hook for managing EventModal state.
|
||||
* Can be used both in React components and adapted for open-calendar.
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
||||
import type { IcsEvent } from 'ts-ics';
|
||||
import { EventModal } from '../components/EventModal';
|
||||
import { Calendar as ApiCalendar } from '../api';
|
||||
|
||||
// Re-export Calendar type from EventModal for consistency
|
||||
export type { Calendar as OpenCalendar } from '../components/EventModal';
|
||||
|
||||
// Convert API Calendar to open-calendar Calendar format
|
||||
export function apiCalendarToOpenCalendar(cal: ApiCalendar): OpenCalendar {
|
||||
return {
|
||||
url: cal.id,
|
||||
uid: cal.id,
|
||||
displayName: cal.name,
|
||||
calendarColor: cal.color,
|
||||
description: cal.description,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
interface UseEventModalProps {
|
||||
calendars?: OpenCalendar[];
|
||||
initialEvent?: IcsEvent | null;
|
||||
initialCalendarUrl?: string;
|
||||
onSubmit?: (event: IcsEvent, calendarUrl: string) => Promise<void>;
|
||||
onDelete?: (event: IcsEvent, calendarUrl: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useEventModal = ({
|
||||
calendars,
|
||||
initialEvent = null,
|
||||
initialCalendarUrl = '',
|
||||
onSubmit: customOnSubmit,
|
||||
onDelete: customOnDelete,
|
||||
}: UseEventModalProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [mode, setMode] = useState<'create' | 'edit'>('create');
|
||||
const [event, setEvent] = useState<IcsEvent | null>(initialEvent);
|
||||
const [currentCalendars, setCurrentCalendars] = useState<OpenCalendar[]>(calendars || []);
|
||||
const [calendarUrl, setCalendarUrl] = useState<string>(initialCalendarUrl || calendars[0]?.url || '');
|
||||
const [allDay, setAllDay] = useState(false);
|
||||
|
||||
// Update calendars when they change
|
||||
useEffect(() => {
|
||||
if (calendars && calendars.length > 0) {
|
||||
setCurrentCalendars(calendars);
|
||||
}
|
||||
}, [calendars]);
|
||||
|
||||
const handleSubmit = useCallback(async (event: IcsEvent, calendarUrl: string) => {
|
||||
if (customOnSubmit) {
|
||||
await customOnSubmit(event, calendarUrl);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}, [customOnSubmit]);
|
||||
|
||||
const handleDelete = useCallback(async (event: IcsEvent, calendarUrl: string) => {
|
||||
if (customOnDelete) {
|
||||
await customOnDelete(event, calendarUrl);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}, [customOnDelete]);
|
||||
|
||||
const open = useCallback((event?: IcsEvent | null, mode: 'create' | 'edit' = 'create', calendarUrl?: string, calendars?: OpenCalendar[]) => {
|
||||
setEvent(event || null);
|
||||
setMode(mode);
|
||||
if (calendarUrl !== undefined) setCalendarUrl(calendarUrl);
|
||||
if (calendars && calendars.length > 0) setCurrentCalendars(calendars);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
mode,
|
||||
event,
|
||||
calendarUrl,
|
||||
open,
|
||||
close,
|
||||
Modal: (
|
||||
<EventModal
|
||||
isOpen={isOpen}
|
||||
mode={mode}
|
||||
calendars={currentCalendars}
|
||||
selectedEvent={event}
|
||||
calendarUrl={calendarUrl}
|
||||
onSubmit={handleSubmit}
|
||||
onAllDayChange={setAllDay}
|
||||
onClose={close}
|
||||
onDelete={mode === 'edit' ? handleDelete : undefined}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
34
src/frontend/apps/calendars/src/features/calendar/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface RecurrenceRule {
|
||||
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
interval?: number;
|
||||
until?: Date;
|
||||
count?: number;
|
||||
byDay?: string[]; // e.g., ['MO', 'WE', 'FR']
|
||||
byMonth?: number[]; // 1-12
|
||||
byMonthDay?: number[]; // 1-31
|
||||
}
|
||||
|
||||
export interface Calendar {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description?: string;
|
||||
is_default?: boolean;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
id?: string;
|
||||
title: string;
|
||||
date: string; // ISO date string (YYYY-MM-DD)
|
||||
endDate: string; // ISO date string (YYYY-MM-DD)
|
||||
time?: string; // Time string (HH:mm)
|
||||
endTime?: string; // Time string (HH:mm)
|
||||
allDay: boolean;
|
||||
calendarId: string;
|
||||
location?: string;
|
||||
notes?: string;
|
||||
description?: string;
|
||||
recurrence?: RecurrenceRule;
|
||||
visibility?: 'default' | 'public' | 'private';
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Spinner } from "@gouvfr-lasuite/ui-kit";
|
||||
import { useApiConfig } from "./useApiConfig";
|
||||
import { ApiConfig } from "@/features/api/types";
|
||||
import { createContext, useContext, useEffect } from "react";
|
||||
import { useAppContext } from "@/pages/_app";
|
||||
|
||||
export interface ConfigContextType {
|
||||
config: ApiConfig;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export const useConfig = () => {
|
||||
const context = useContext(ConfigContext);
|
||||
if (!context) {
|
||||
throw new Error("useConfig must be used within a ConfigProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { data: config } = useApiConfig();
|
||||
const { setTheme } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.FRONTEND_THEME) {
|
||||
setTheme(config.FRONTEND_THEME);
|
||||
}
|
||||
}, [config?.FRONTEND_THEME, setTheme]);
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="global-loader">
|
||||
<Spinner size="xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchAPI } from "@/features/api/fetchApi";
|
||||
import { ApiConfig } from "@/features/api/types";
|
||||
|
||||
export function useApiConfig() {
|
||||
return useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: async () => {
|
||||
const response = await fetchAPI("config/");
|
||||
return (await response.json()) as ApiConfig;
|
||||
},
|
||||
staleTime: 1000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export class AppError extends Error {}
|
||||
@@ -0,0 +1,75 @@
|
||||
@use "sass:map";
|
||||
@use "@/styles/cunningham-tokens-sass" as *;
|
||||
|
||||
$tablet: map.get($themes, "default", "globals", "breakpoints", "tablet");
|
||||
|
||||
.c__feedback {
|
||||
&__footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: var(--c--contextuals--background--surface--tertiary);
|
||||
border-top: 1px solid
|
||||
var(--c--contextuals--border--semantic--neutral--tertiary);
|
||||
display: none;
|
||||
|
||||
@media (max-width: $tablet) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__modal {
|
||||
&__buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid
|
||||
var(--c--contextuals--border--semantic--neutral--tertiary);
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
gap: 20px;
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
|
||||
&__title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--c--contextuals--border--semantic--info--tertiary);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--info--tertiary
|
||||
);
|
||||
color: var(--c--contextuals--content--semantic--info--secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is made to avoid having the feedback footer to hide the content.
|
||||
* To remove when the feedback footer is removed.
|
||||
*/
|
||||
|
||||
@media (max-width: $tablet) {
|
||||
.calendars__home--feedback,
|
||||
.c__main-layout__content__center__children {
|
||||
padding-bottom: 57px;
|
||||
}
|
||||
}
|
||||
193
src/frontend/apps/calendars/src/features/feedback/Feedback.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal,
|
||||
ModalSize,
|
||||
useModal,
|
||||
} from "@openfun/cunningham-react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfig } from "../config/ConfigProvider";
|
||||
import { useMessagesWidget } from "./useMessagesWidget";
|
||||
|
||||
export const Feedback = (props: { buttonProps?: Partial<ButtonProps> }) => {
|
||||
const { t } = useTranslation();
|
||||
const modal = useModal();
|
||||
const { config } = useConfig();
|
||||
const { showWidget } = useMessagesWidget();
|
||||
|
||||
const FEEDBACK_BUTTONS = useMemo(() => {
|
||||
return config?.FRONTEND_FEEDBACK_ITEMS
|
||||
? Object.entries(config.FRONTEND_FEEDBACK_ITEMS).map(([key, value]) => {
|
||||
let Icon: React.ReactElement;
|
||||
switch (key) {
|
||||
case "form":
|
||||
Icon = <FormIcon />;
|
||||
break;
|
||||
case "tchap":
|
||||
Icon = <TchapIcon />;
|
||||
break;
|
||||
case "visio":
|
||||
Icon = <VisioIcon />;
|
||||
break;
|
||||
default:
|
||||
Icon = <FormIcon />;
|
||||
break;
|
||||
}
|
||||
return {
|
||||
icon: Icon,
|
||||
title: `feedback.modal.buttons.${key}.title`,
|
||||
description: `feedback.modal.buttons.${key}.description`,
|
||||
href: value.url,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
}, []);
|
||||
|
||||
const showFeedbackButton = () => {
|
||||
if (!config?.FRONTEND_FEEDBACK_BUTTON_SHOW) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For idle mode, there is no feedback buttons displayed as the modal will never show up,
|
||||
// so we show the button even if there is no href.
|
||||
if (
|
||||
!config?.FRONTEND_FEEDBACK_BUTTON_IDLE &&
|
||||
FEEDBACK_BUTTONS.filter((button) => !!button.href).length === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
if (config?.FRONTEND_FEEDBACK_BUTTON_IDLE) {
|
||||
return;
|
||||
}
|
||||
if (config?.FRONTEND_FEEDBACK_MESSAGES_WIDGET_ENABLED) {
|
||||
showWidget();
|
||||
return;
|
||||
}
|
||||
modal.open();
|
||||
};
|
||||
|
||||
if (!showFeedbackButton()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={<Icon name="feedback" type={IconType.OUTLINED} />}
|
||||
color="info"
|
||||
className="c__feedback__button"
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
{...props.buttonProps}
|
||||
>
|
||||
{t("feedback.button")}
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
{...modal}
|
||||
title={t("feedback.modal.title")}
|
||||
size={ModalSize.MEDIUM}
|
||||
>
|
||||
<p className="fs-m clr-greyscale-600">
|
||||
{t("feedback.modal.description")}
|
||||
</p>
|
||||
<div className="c__feedback__modal__buttons">
|
||||
{FEEDBACK_BUTTONS.filter((button) => !!button.href).map((button) => (
|
||||
<FeedbackButton
|
||||
key={button.title}
|
||||
{...button}
|
||||
href={button.href!}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeedbackFooterMobile = () => {
|
||||
return (
|
||||
<div className="c__feedback__footer">
|
||||
<Feedback buttonProps={{ fullWidth: true }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeedbackButton = (props: {
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a href={props.href} target="_blank" className="c__feedback__modal__button">
|
||||
<div className="c__feedback__modal__button__icon">{props.icon}</div>
|
||||
<div className="c__feedback__modal__button__content">
|
||||
<div className="c__feedback__modal__button__title">
|
||||
{t(props.title)}
|
||||
</div>
|
||||
<div className="c__feedback__modal__button__description">
|
||||
{t(props.description)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="42"
|
||||
height="42"
|
||||
viewBox="0 0 42 42"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16.297 8.92893C15.8505 8.92893 15.5005 8.79702 15.2468 8.53321C14.9931 8.2694 14.8663 7.89905 14.8663 7.42217V5.82409C14.8663 5.3472 14.9931 4.97685 15.2468 4.71304C15.5005 4.44923 15.8505 4.31733 16.297 4.31733H17.682C17.7327 3.45487 18.0726 2.71925 18.7017 2.11045C19.3409 1.50166 20.1019 1.19727 20.9847 1.19727C21.8674 1.19727 22.6233 1.50166 23.2524 2.11045C23.8917 2.71925 24.2366 3.45487 24.2874 4.31733H25.6876C26.134 4.31733 26.4841 4.44923 26.7378 4.71304C26.9914 4.97685 27.1183 5.3472 27.1183 5.82409V7.42217C27.1183 7.89905 26.9914 8.2694 26.7378 8.53321C26.4841 8.79702 26.134 8.92893 25.6876 8.92893H16.297ZM20.9847 5.76321C21.3601 5.76321 21.6746 5.6313 21.9283 5.36749C22.182 5.10368 22.3088 4.79421 22.3088 4.43908C22.3088 4.06366 22.182 3.74912 21.9283 3.49546C21.6746 3.23165 21.3601 3.09974 20.9847 3.09974C20.6194 3.09974 20.3049 3.23165 20.041 3.49546C19.7874 3.74912 19.6605 4.06366 19.6605 4.43908C19.6605 4.79421 19.7874 5.10368 20.041 5.36749C20.3049 5.6313 20.6194 5.76321 20.9847 5.76321ZM12.903 38.0749C11.3302 38.0749 10.1482 37.6741 9.35674 36.8725C8.57546 36.0811 8.18481 34.8888 8.18481 33.2958V10.2226C8.18481 8.66004 8.56024 7.4729 9.31108 6.66118C10.0721 5.84945 11.2136 5.44359 12.7355 5.44359H13.1008C13.0907 5.50447 13.0856 5.57042 13.0856 5.64145C13.0856 5.70233 13.0856 5.76321 13.0856 5.82409V7.19387C13.0856 7.47797 13.1059 7.71134 13.1465 7.89398H12.7964C12.076 7.89398 11.5332 8.10706 11.1679 8.53321C10.8128 8.95937 10.6352 9.53265 10.6352 10.2531V33.2654C10.6352 34.0264 10.8381 34.6098 11.244 35.0157C11.6499 35.4215 12.2485 35.6245 13.0399 35.6245H28.9446C29.7361 35.6245 30.3296 35.4215 30.7253 35.0157C31.1312 34.6098 31.3341 34.0264 31.3341 33.2654V22.6724L33.7845 20.222V33.2958C33.7845 34.8888 33.3888 36.0811 32.5974 36.8725C31.8161 37.6741 30.6391 38.0749 29.0664 38.0749H12.903ZM31.3341 12.5817V10.2531C31.3341 9.53265 31.1515 8.95937 30.7862 8.53321C30.4311 8.10706 29.8984 7.89398 29.1881 7.89398H28.8229C28.8635 7.71134 28.8838 7.47797 28.8838 7.19387V5.82409C28.8838 5.76321 28.8838 5.70233 28.8838 5.64145C28.8838 5.57042 28.8787 5.50447 28.8685 5.44359H29.2338C30.7152 5.44359 31.8415 5.80887 32.6126 6.53942C33.3939 7.26997 33.7845 8.29984 33.7845 9.62904V10.1313C33.7135 10.2023 33.6425 10.2733 33.5715 10.3444C33.5004 10.4154 33.4294 10.4864 33.3584 10.5574L31.3341 12.5817ZM13.3139 16.478C13.3139 16.2141 13.4052 15.9909 13.5878 15.8083C13.7806 15.6155 14.0089 15.5191 14.2727 15.5191H27.7118C27.9249 15.5191 28.1075 15.5648 28.2597 15.6561L26.4942 17.4216H14.2727C14.0089 17.4216 13.7806 17.3303 13.5878 17.1476C13.4052 16.9548 13.3139 16.7316 13.3139 16.478ZM13.3139 21.8658C13.3139 21.6121 13.4052 21.394 13.5878 21.2113C13.7806 21.0287 14.0089 20.9374 14.2727 20.9374H22.9785L21.0912 22.8246H14.2727C14.0089 22.8246 13.7806 22.7333 13.5878 22.5507C13.4052 22.3579 13.3139 22.1296 13.3139 21.8658ZM14.2727 28.5168C14.0089 28.5168 13.7806 28.4255 13.5878 28.2429C13.4052 28.0501 13.3139 27.8269 13.3139 27.5732C13.3139 27.3094 13.4052 27.0862 13.5878 26.9035C13.7806 26.7209 14.0089 26.6296 14.2727 26.6296H16.3883C16.6521 26.6296 16.8753 26.7209 17.058 26.9035C17.2507 27.0862 17.3471 27.3094 17.3471 27.5732C17.3471 27.8269 17.2507 28.0501 17.058 28.2429C16.8753 28.4255 16.6521 28.5168 16.3883 28.5168H14.2727ZM36.8894 14.2102L34.7738 12.0794L35.9153 10.9379C36.169 10.6843 36.4632 10.5473 36.7981 10.527C37.143 10.5067 37.4373 10.6183 37.6808 10.8618L38.0461 11.2271C38.3099 11.4909 38.4418 11.7953 38.4418 12.1403C38.4418 12.4751 38.3048 12.7795 38.0309 13.0535L36.8894 14.2102ZM19.828 29.5365C19.6859 29.5974 19.554 29.5619 19.4323 29.43C19.3105 29.2981 19.2851 29.1662 19.3562 29.0343L20.8173 26.036L33.6171 13.2361L35.7631 15.3517L22.9328 28.1515L19.828 29.5365Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TchapIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="42"
|
||||
height="43"
|
||||
viewBox="0 0 42 43"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13.588 37.7378C13.0603 37.7378 12.6494 37.5603 12.3552 37.2051C12.0711 36.8602 11.929 36.3934 11.929 35.8049V31.8478H11.1985C9.68662 31.8478 8.41323 31.5789 7.37828 31.0411C6.34334 30.4932 5.55698 29.7018 5.01921 28.6668C4.49159 27.6319 4.22778 26.3686 4.22778 24.8771V13.2187C4.22778 11.7272 4.49159 10.4639 5.01921 9.42899C5.55698 8.39404 6.34334 7.60768 7.37828 7.06992C8.41323 6.522 9.68662 6.24805 11.1985 6.24805H30.8016C32.3134 6.24805 33.5868 6.522 34.6217 7.06992C35.6567 7.60768 36.438 8.39404 36.9656 9.42899C37.5034 10.4639 37.7722 11.7272 37.7722 13.2187V24.8771C37.7722 26.3686 37.5034 27.6319 36.9656 28.6668C36.438 29.7018 35.6567 30.4932 34.6217 31.0411C33.5868 31.5789 32.3134 31.8478 30.8016 31.8478H21.0305L15.7796 36.5202C15.323 36.9261 14.9375 37.2305 14.6229 37.4334C14.3084 37.6364 13.9634 37.7378 13.588 37.7378ZM14.212 34.9526L19.0823 30.1127C19.3664 29.8185 19.6404 29.6257 19.9042 29.5344C20.168 29.443 20.513 29.3974 20.9391 29.3974H30.8016C32.3337 29.3974 33.4701 29.0169 34.2108 28.2559C34.9515 27.4848 35.3218 26.3534 35.3218 24.8619V13.2187C35.3218 11.7373 34.9515 10.6161 34.2108 9.85514C33.4701 9.084 32.3337 8.69844 30.8016 8.69844H11.1985C9.65618 8.69844 8.5147 9.084 7.774 9.85514C7.04345 10.6161 6.67817 11.7373 6.67817 13.2187V24.8619C6.67817 26.3534 7.04345 27.4848 7.774 28.2559C8.5147 29.0169 9.65618 29.3974 11.1985 29.3974H13.0705C13.4865 29.3974 13.7807 29.4836 13.9532 29.6561C14.1257 29.8286 14.212 30.1229 14.212 30.5389V34.9526ZM13.1618 14.8472C12.9081 14.8472 12.6951 14.761 12.5226 14.5885C12.3602 14.416 12.2791 14.208 12.2791 13.9645C12.2791 13.721 12.3602 13.518 12.5226 13.3557C12.6951 13.1832 12.9081 13.097 13.1618 13.097H28.6404C28.894 13.097 29.102 13.1832 29.2644 13.3557C29.4369 13.518 29.5231 13.721 29.5231 13.9645C29.5231 14.208 29.4369 14.416 29.2644 14.5885C29.102 14.761 28.894 14.8472 28.6404 14.8472H13.1618ZM13.1618 19.7937C12.9081 19.7937 12.6951 19.7125 12.5226 19.5502C12.3602 19.3777 12.2791 19.1646 12.2791 18.9109C12.2791 18.6776 12.3602 18.4746 12.5226 18.3021C12.6951 18.1195 12.9081 18.0282 13.1618 18.0282H28.6404C28.894 18.0282 29.102 18.1195 29.2644 18.3021C29.4369 18.4746 29.5231 18.6776 29.5231 18.9109C29.5231 19.1646 29.4369 19.3777 29.2644 19.5502C29.102 19.7125 28.894 19.7937 28.6404 19.7937H13.1618ZM13.1618 24.7553C12.9081 24.7553 12.6951 24.6742 12.5226 24.5118C12.3602 24.3393 12.2791 24.1313 12.2791 23.8878C12.2791 23.6341 12.3602 23.4211 12.5226 23.2486C12.6951 23.0761 12.9081 22.9898 13.1618 22.9898H23.2221C23.4656 22.9898 23.6736 23.0761 23.8461 23.2486C24.0186 23.4211 24.1049 23.6341 24.1049 23.8878C24.1049 24.1313 24.0186 24.3393 23.8461 24.5118C23.6736 24.6742 23.4656 24.7553 23.2221 24.7553H13.1618Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const VisioIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="42"
|
||||
height="43"
|
||||
viewBox="0 0 42 43"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.1288 27.1205C13.8568 27.1205 13.6502 27.0399 13.5092 26.8787C13.3783 26.7074 13.3128 26.4908 13.3128 26.2289C13.3128 25.8562 13.4588 25.3676 13.751 24.7631C14.0431 24.1486 14.4814 23.5341 15.0656 22.9196C15.66 22.3051 16.4105 21.7913 17.3172 21.3783C18.2239 20.9552 19.2867 20.7436 20.5056 20.7436C21.7346 20.7436 22.7975 20.9552 23.694 21.3783C24.6007 21.7913 25.3512 22.3051 25.9456 22.9196C26.5399 23.5341 26.9832 24.1486 27.2754 24.7631C27.5675 25.3676 27.7136 25.8562 27.7136 26.2289C27.7136 26.4908 27.6431 26.7074 27.502 26.8787C27.3711 27.0399 27.1645 27.1205 26.8825 27.1205H14.1288ZM20.5056 19.6103C19.8407 19.6103 19.2363 19.439 18.6923 19.0965C18.1584 18.754 17.7302 18.2906 17.4079 17.7063C17.0855 17.1119 16.9243 16.442 16.9243 15.6965C16.9243 15.0014 17.0855 14.3617 17.4079 13.7774C17.7302 13.1931 18.1584 12.7297 18.6923 12.3872C19.2363 12.0346 19.8407 11.8583 20.5056 11.8583C21.1705 11.8583 21.7699 12.0346 22.3038 12.3872C22.8478 12.7297 23.281 13.1931 23.6034 13.7774C23.9257 14.3617 24.0869 15.0014 24.0869 15.6965C24.0869 16.442 23.9257 17.1119 23.6034 17.7063C23.281 18.2906 22.8478 18.754 22.3038 19.0965C21.7699 19.439 21.1705 19.6103 20.5056 19.6103ZM13.1465 38.1968C12.6227 38.1968 12.2147 38.0205 11.9226 37.6679C11.6405 37.3254 11.4994 36.862 11.4994 36.2777V32.3489H10.7741C9.27309 32.3489 8.0088 32.0819 6.98125 31.548C5.9537 31.004 5.17297 30.2182 4.63904 29.1907C4.1152 28.1631 3.85327 26.9089 3.85327 25.428V13.853C3.85327 12.3721 4.1152 11.1179 4.63904 10.0903C5.17297 9.06278 5.9537 8.28205 6.98125 7.74812C8.0088 7.20413 9.27309 6.93213 10.7741 6.93213H30.2371C31.7381 6.93213 33.0024 7.20413 34.03 7.74812C35.0575 8.28205 35.8332 9.06278 36.3571 10.0903C36.891 11.1179 37.158 12.3721 37.158 13.853V25.428C37.158 26.9089 36.891 28.1631 36.3571 29.1907C35.8332 30.2182 35.0575 31.004 34.03 31.548C33.0024 32.0819 31.7381 32.3489 30.2371 32.3489H20.5358L15.3225 36.9879C14.8692 37.3909 14.4864 37.6931 14.1741 37.8946C13.8618 38.0961 13.5193 38.1968 13.1465 38.1968ZM13.7661 35.4315L18.6016 30.6262C18.8837 30.3341 19.1557 30.1427 19.4176 30.052C19.6795 29.9613 20.0221 29.916 20.4452 29.916H30.2371C31.7583 29.916 32.8866 29.5382 33.622 28.7827C34.3574 28.017 34.7251 26.8938 34.7251 25.4129V13.853C34.7251 12.3822 34.3574 11.269 33.622 10.5134C32.8866 9.74782 31.7583 9.365 30.2371 9.365H10.7741C9.24287 9.365 8.10954 9.74782 7.37414 10.5134C6.64881 11.269 6.28615 12.3822 6.28615 13.853V25.4129C6.28615 26.8938 6.64881 28.017 7.37414 28.7827C8.10954 29.5382 9.24287 29.916 10.7741 29.916H12.6328C13.0458 29.916 13.338 30.0016 13.5092 30.1729C13.6805 30.3441 13.7661 30.6363 13.7661 31.0493V35.4315Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../auth/Auth";
|
||||
import { useConfig } from "../config/ConfigProvider";
|
||||
|
||||
/**
|
||||
* Hook that opens the feedback widget
|
||||
*/
|
||||
export const useMessagesWidget = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { config } = useConfig();
|
||||
|
||||
const apiUrl = config?.FRONTEND_FEEDBACK_MESSAGES_WIDGET_API_URL;
|
||||
const widgetPath = config?.FRONTEND_FEEDBACK_MESSAGES_WIDGET_PATH;
|
||||
const channel = config?.FRONTEND_FEEDBACK_MESSAGES_WIDGET_CHANNEL;
|
||||
|
||||
const title: string = t("feedback_widget.title");
|
||||
const placeholder: string = t("feedback_widget.placeholder");
|
||||
const emailPlaceholder: string = t("feedback_widget.email_placeholder");
|
||||
const submitText: string = t("feedback_widget.submit_text");
|
||||
const successText: string = t("feedback_widget.success_text");
|
||||
const successText2: string = t("feedback_widget.success_text2");
|
||||
|
||||
const showWidget = () => {
|
||||
if (!channel || !apiUrl || !widgetPath) {
|
||||
throw new Error(
|
||||
"FRONTEND_FEEDBACK_MESSAGES_WIDGET_API_URL, FRONTEND_FEEDBACK_MESSAGES_WIDGET_PATH or FRONTEND_FEEDBACK_MESSAGES_WIDGET_CHANNEL is not set"
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize the widget array if it doesn't exist
|
||||
if (typeof window !== "undefined" && widgetPath) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any)._stmsg_widget = (window as any)._stmsg_widget || [];
|
||||
|
||||
// Construct script URLs from the base path
|
||||
const feedbackScript = `${widgetPath}feedback.js`;
|
||||
|
||||
// Push the widget configuration
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any)._stmsg_widget.push([
|
||||
"feedback",
|
||||
"init",
|
||||
{
|
||||
title,
|
||||
api: apiUrl,
|
||||
channel,
|
||||
placeholder,
|
||||
emailPlaceholder,
|
||||
submitText,
|
||||
successText,
|
||||
successText2,
|
||||
// Add email parameter if user is logged in
|
||||
...(user?.email && { email: user.email }),
|
||||
},
|
||||
]);
|
||||
|
||||
// Load the loader script if not already loaded
|
||||
if (!document.querySelector(`script[src="${feedbackScript}"]`)) {
|
||||
const script = document.createElement("script");
|
||||
script.async = true;
|
||||
script.src = feedbackScript;
|
||||
const firstScript = document.getElementsByTagName("script")[0];
|
||||
if (firstScript && firstScript.parentNode) {
|
||||
firstScript.parentNode.insertBefore(script, firstScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { showWidget };
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { Input, TextArea, TextAreaProps } from "@openfun/cunningham-react";
|
||||
import { InputProps } from "@openfun/cunningham-react";
|
||||
|
||||
export const RhfInput = (props: InputProps & { name: string }) => {
|
||||
const { control, setValue } = useFormContext();
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={props.name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
aria-invalid={!!fieldState.error}
|
||||
state={fieldState.error ? "error" : "default"}
|
||||
text={fieldState.error?.message}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(e) => setValue(field.name, e.target.value)}
|
||||
value={field.value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RhfTextarea = (props: TextAreaProps & { name: string }) => {
|
||||
const { control, setValue } = useFormContext();
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={props.name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<TextArea
|
||||
{...props}
|
||||
aria-invalid={!!fieldState.error}
|
||||
state={fieldState.error ? "error" : "default"}
|
||||
text={fieldState.error?.message}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(e) => setValue(field.name, e.target.value)}
|
||||
value={field.value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
2
src/frontend/apps/calendars/src/features/i18n/conf.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const LANGUAGES_ALLOWED = ["en-us", "fr-fr"];
|
||||
export const LANGUAGE_LOCAL_STORAGE = "main-language";
|
||||
52
src/frontend/apps/calendars/src/features/i18n/initI18n.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import { LANGUAGES_ALLOWED, LANGUAGE_LOCAL_STORAGE } from "./conf";
|
||||
import resources from "./translations.json";
|
||||
|
||||
/**
|
||||
* How language works:
|
||||
*
|
||||
* - First visit on the website, the user is not logged in, we detect the browser current language with LanguageDetector
|
||||
* - When the user logs in, its language attribute is null
|
||||
* - Because the user language is null, we use the language detected by LanguageDetector
|
||||
* - If the user changes the language via the language picker, we update the language of the user via a request to the backend
|
||||
* - Now, the language used is the language of the user ( when user is fetched, LanguagePicker calls LanguagePicker via useEffect )
|
||||
*
|
||||
* This way we ensure that we use the most probable language of the user.
|
||||
*/
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: "en",
|
||||
detection: {
|
||||
order: ["cookie", "navigator"],
|
||||
caches: ["cookie"],
|
||||
lookupCookie: "calendars_language",
|
||||
cookieMinutes: 525600,
|
||||
cookieOptions: {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
},
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
preload: LANGUAGES_ALLOWED,
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error("i18n initialization failed");
|
||||
});
|
||||
|
||||
// Save language in local storage
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(LANGUAGE_LOCAL_STORAGE, lng);
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
634
src/frontend/apps/calendars/src/features/i18n/translations.json
Normal file
@@ -0,0 +1,634 @@
|
||||
{
|
||||
"en": {
|
||||
"translation": {
|
||||
"401": {
|
||||
"title": "You need to be logged in to access this application.",
|
||||
"button": "Login"
|
||||
},
|
||||
"403": {
|
||||
"title": "You don't have the necessary permissions to access this page.",
|
||||
"button": "Home"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"clipboard": {
|
||||
"success": "Copied to clipboard",
|
||||
"error": "Failed to copy to clipboard"
|
||||
},
|
||||
"app_title": "Calendars",
|
||||
"app_description": "Calendars application",
|
||||
"welcome": "Welcome",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"my_account": "My Account",
|
||||
"api": {
|
||||
"error": {
|
||||
"unexpected": "An unexpected error occurred."
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "Easy storage and sharing",
|
||||
"subtitle": "A modern calendar application built for French public teams. Sovereign hosting in France, strong security, and seamless sync with all LaSuite apps.",
|
||||
"main_button": "Sign in",
|
||||
"more": "Learn more"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Give feedback",
|
||||
"modal": {
|
||||
"title": "New feedback",
|
||||
"description": "Calendars is under development: your feedback matters! Choose how to share your ideas:",
|
||||
"buttons": {
|
||||
"form": {
|
||||
"title": "Give quick feedback",
|
||||
"description": "30 sec to tell us what you think or report a bug"
|
||||
},
|
||||
"tchap": {
|
||||
"title": "Write on Tchap",
|
||||
"description": "Direct exchange with our team"
|
||||
},
|
||||
"visio": {
|
||||
"title": "Talk on video",
|
||||
"description": "20 min for an in-depth discussion"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedback_widget": {
|
||||
"shortTitle": "Feedback?",
|
||||
"title": "Do you have any feedback?",
|
||||
"placeholder": "Share your feedback here...",
|
||||
"email_placeholder": "Your email...",
|
||||
"submit_text": "Send Feedback",
|
||||
"success_text": "Thank you for your feedback!",
|
||||
"success_text2": "In case of questions, we'll get back to you soon."
|
||||
},
|
||||
"authentication": {
|
||||
"error": {
|
||||
"alpha": "This application is in alpha and its access is restricted.",
|
||||
"user_cannot_access_app": "You do not have the necessary permissions to access this application. Contact your administrator."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"administrator": "Administrator",
|
||||
"editor": "Editor",
|
||||
"reader": "Reader",
|
||||
"owner": "Owner"
|
||||
},
|
||||
"time": {
|
||||
"years_ago_one": "{{count}} year ago",
|
||||
"years_ago_other": "{{count}} years ago",
|
||||
"months_ago_one": "{{count}} month ago",
|
||||
"months_ago_other": "{{count}} months ago",
|
||||
"weeks_ago_one": "{{count}} week ago",
|
||||
"weeks_ago_other": "{{count}} weeks ago",
|
||||
"days_ago_one": "{{count}} day ago",
|
||||
"days_ago_other": "{{count}} days ago",
|
||||
"hours_ago_one": "{{count}} hour ago",
|
||||
"hours_ago_other": "{{count}} hours ago",
|
||||
"minutes_ago_one": "{{count}} minute ago",
|
||||
"minutes_ago_other": "{{count}} minutes ago",
|
||||
"seconds_ago": "few seconds ago"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"translation": {
|
||||
"entitlements": {
|
||||
"can_upload": {
|
||||
"cannot_upload": "Vous n'avez pas les droits nécessaires pour transférer des fichiers."
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"title": "Connectez-vous pour accéder aux documents.",
|
||||
"button": "Se connecter"
|
||||
},
|
||||
"403": {
|
||||
"title": "Vous n'avez pas les permissions nécessaires pour accéder à cette page.",
|
||||
"button": "Accueil"
|
||||
},
|
||||
"file_download_modal": {
|
||||
"error": {
|
||||
"no_url_or_title": "Ce fichier n'a pas d'URL ou de titre. "
|
||||
},
|
||||
"suspicious": {
|
||||
"title": "Ce fichier est suspect"
|
||||
},
|
||||
"file_too_large_to_analyze": {
|
||||
"title": "Ce fichier est trop volumineux pour être analysé"
|
||||
},
|
||||
"analyzing": {
|
||||
"title": "Ce fichier est en cours d'analyse"
|
||||
},
|
||||
"description": "Êtes-vous sûr de vouloir télécharger ce fichier ?",
|
||||
"download": "Télécharger quand même",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"file_preview": {
|
||||
"suspicious": {
|
||||
"title": "Fichier suspect",
|
||||
"description": "Ce fichier semble suspect. Il n'est visible que pour vous.",
|
||||
"download": "Télécharger le fichier"
|
||||
},
|
||||
"error": {
|
||||
"title": "Une erreur est survenue lors du chargement du document.",
|
||||
"description": "Veuillez réessayer plus tard."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Type de fichier non supporté pour la prévisualisation",
|
||||
"description": "Pour visualiser le fichier, téléchargez-le sur votre appareil.",
|
||||
"download": "Télécharger le fichier",
|
||||
"heic_title": "Les fichiers HEIC ne sont pas encore supportés pour la prévisualisation."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"clipboard": {
|
||||
"success": "Copié dans le presse-papiers",
|
||||
"error": "Échec de la copie dans le presse-papiers"
|
||||
},
|
||||
"app_title": "Calendriers",
|
||||
"app_description": "Application de calendriers",
|
||||
"welcome": "Bienvenue",
|
||||
"logout": "Déconnexion",
|
||||
"login": "Connexion",
|
||||
"my_account": "Mon Compte",
|
||||
"api": {
|
||||
"error": {
|
||||
"unexpected": "Une erreur inattendue est survenue."
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "Stocker et partager facilement",
|
||||
"subtitle": "Une application de calendrier moderne pour les équipes publiques françaises. Hébergement en France, sécurité renforcée, et synchronisation fluide avec toutes les applications LaSuite.",
|
||||
"main_button": "Se connecter",
|
||||
"more": "En savoir plus"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Faire un retour",
|
||||
"modal": {
|
||||
"title": "Nouveau retour",
|
||||
"description": "Calendrier est en cours de développement : vos retours comptent ! Choisissez comment partager vos idées :",
|
||||
"buttons": {
|
||||
"form": {
|
||||
"title": "Donner un avis rapide",
|
||||
"description": "30 sec pour nous dire ce que vous pensez ou signaler un bug"
|
||||
},
|
||||
"tchap": {
|
||||
"title": "Écrire sur Tchap",
|
||||
"description": "Échange direct avec notre équipe"
|
||||
},
|
||||
"visio": {
|
||||
"title": "Parler en visio",
|
||||
"description": "20 min pour aller en profondeur"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedback_widget": {
|
||||
"shortTitle": "Faire un retour",
|
||||
"title": "Partager un retour ou une question",
|
||||
"placeholder": "Saisir votre message...",
|
||||
"email_placeholder": "Renseigner votre email...",
|
||||
"submit_text": "Envoyer le message",
|
||||
"success_text": "Merci pour votre message.",
|
||||
"success_text2": "En cas de questions, nous vous répondrons dans les meilleurs délais sur l'email renseigné."
|
||||
},
|
||||
"authentication": {
|
||||
"error": {
|
||||
"alpha": "Cette application est en version alpha et son accès est restreint.",
|
||||
"user_cannot_access_app": "Vous n'avez pas les permissions nécessaires pour accéder à cette application. Contactez votre administrateur."
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"calc": "Tableur",
|
||||
"doc": "Document",
|
||||
"image": "Image",
|
||||
"other": "Autre",
|
||||
"pdf": "PDF",
|
||||
"powerpoint": "Présentation",
|
||||
"audio": "Audio",
|
||||
"video": "Vidéo",
|
||||
"archive": "Archive"
|
||||
},
|
||||
"roles": {
|
||||
"administrator": "Administrateur",
|
||||
"editor": "Éditeur",
|
||||
"reader": "Lecteur",
|
||||
"owner": "Propriétaire"
|
||||
},
|
||||
"time": {
|
||||
"breadcrumbs": {
|
||||
"spaces": "Espaces"
|
||||
},
|
||||
"modal": {
|
||||
"move": {
|
||||
"title": "Déplacer",
|
||||
"move_button": "Déplacer ici",
|
||||
"description_one_item": "Choisissez le nouvel emplacement pour <strong>{{name}}</strong>",
|
||||
"description_multiple_items": "Choisissez le nouvel emplacement pour les <strong>{{count}}</strong> éléments sélectionnés"
|
||||
}
|
||||
},
|
||||
"rightPanel": {
|
||||
"suspicious": {
|
||||
"text": "Ce fichier semble suspect. Il n'est visible que pour vous."
|
||||
},
|
||||
"file_too_large_to_analyze": {
|
||||
"text": "Ce fichier est trop volumineux pour être analysé. Il peut être dangereux de le télécharger."
|
||||
},
|
||||
"multipleSelection": {
|
||||
"text": "Plusieurs fichiers sélectionnés",
|
||||
"alt": "Image de plusieurs fichiers sélectionnés"
|
||||
},
|
||||
"empty": {
|
||||
"text": "Aucun fichier sélectionné",
|
||||
"alt": "Image d'un fichier sélectionné"
|
||||
},
|
||||
"sharing": "Partage",
|
||||
"share": "Partager",
|
||||
"format": "Format",
|
||||
"updated_at": "Modification",
|
||||
"created_at": "Création",
|
||||
"size": "Poids",
|
||||
"created_by": "Créé par",
|
||||
"description": "Description"
|
||||
},
|
||||
"grid": {
|
||||
"name": "Nom",
|
||||
"last_update": "Dernière modification",
|
||||
"empty": {
|
||||
"default": "Aucun dossier trouvé dans le dossier sélectionné",
|
||||
"caption": "Déposez vos fichiers ici",
|
||||
"caption_no_create": "Vous n'avez pas les droits pour ajouter des fichiers",
|
||||
"cta": "Ou cliquez sur \"Créer\"",
|
||||
"cta_no_create": "Les lecteurs ne peuvent pas ajouter de fichiers ou créer de dossiers"
|
||||
},
|
||||
"no_url": "Cette ressource n'a pas d'URL",
|
||||
"actions": {
|
||||
"button_aria_label": "Plus d'actions pour {{name}}",
|
||||
"info": "Informations",
|
||||
"share": "Partager",
|
||||
"download": "Télécharger",
|
||||
"rename": "Renommer",
|
||||
"move": "Déplacer",
|
||||
"duplicate": "Dupliquer",
|
||||
"delete": "Supprimer",
|
||||
"restore": "Restaurer",
|
||||
"hard_delete": "Supprimer définitivement"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"scopes": {
|
||||
"label": "Emplacement",
|
||||
"options": {
|
||||
"all": "Calendars",
|
||||
"trash": "Corbeille"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"options": {
|
||||
"all": "Tous",
|
||||
"file": "Fichier",
|
||||
"folder": "Dossier",
|
||||
"reset": "Réinitialiser"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"label": "Espace"
|
||||
}
|
||||
},
|
||||
"tree": {
|
||||
"trash": "Corbeille",
|
||||
"createFolder": "Créer",
|
||||
"personalSpace": "Espace personnel",
|
||||
"shared_space": "Espace partagé",
|
||||
"public_space": "Espace public",
|
||||
"search": "Rechercher",
|
||||
"import": {
|
||||
"label": "Importer",
|
||||
"files": "Importer des fichiers",
|
||||
"folders": "Importer des dossiers"
|
||||
},
|
||||
"create": {
|
||||
"label": "Créer",
|
||||
"folder": "Nouveau dossier",
|
||||
"workspace": "Nouvel espace"
|
||||
},
|
||||
"workspace": {
|
||||
"options": {
|
||||
"info": "Informations",
|
||||
"settings_workspace": "Paramètre de l'espace",
|
||||
"settings_folder": "Paramètres du dossier",
|
||||
"delete_workspace": "Supprimer l'espace",
|
||||
"delete_folder": "Supprimer le dossier",
|
||||
"share": "Partager",
|
||||
"share_view": "Voir les accès"
|
||||
},
|
||||
"move": {
|
||||
"confirmation_modal": {
|
||||
"title": "Transfert de droits",
|
||||
"description": "Vous êtes sur le point de déplacer l'élément <strong>{{sourceItem}}</strong> vers le dossier <strong>{{targetItem}}</strong>, votre document héritera du partage du dossier cible.",
|
||||
"description_multiple": "Vous êtes sur le point de déplacer <strong>{{count}}</strong> éléments vers le dossier <strong>{{targetItem}}</strong>, vos documents hériteront du partage du dossier cible.",
|
||||
"confirm_button": "Déplacer quand même",
|
||||
"cancel_button": "Annuler"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trash": {
|
||||
"title": "Corbeille",
|
||||
"description": "Suppression automatique après 30 jours",
|
||||
"navigate": {
|
||||
"modal": {
|
||||
"title": "Ce dossier est dans la corbeille",
|
||||
"description": "Pour afficher ce dossier, vous devez d'abord le restaurer."
|
||||
}
|
||||
},
|
||||
"hard_delete": {
|
||||
"title": "Supprimer définitivement",
|
||||
"content_one": "Êtes-vous sûr de vouloir supprimer cet élément définitivement?",
|
||||
"content_other": "Êtes-vous sûr de vouloir supprimer ces {{count}} éléments définitivement?",
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Supprimer définitivement"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"modal": {
|
||||
"title": "Rechercher",
|
||||
"description": "La fonction de recherche est en cours de développement et sera bientôt disponible.",
|
||||
"placeholder": "Taper un mot clé ou le nom d'un fichier",
|
||||
"results": "Sélectionnez un document",
|
||||
"filters": {
|
||||
"reset": "Réinitialiser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectionBar": {
|
||||
"caption_one": "1 élément sélectionné",
|
||||
"caption_other": "{{count}} éléments sélectionnés",
|
||||
"download": "Télécharger",
|
||||
"move": "Déplacer",
|
||||
"delete": "Supprimer",
|
||||
"reset_selection": "Réinitialiser la sélection"
|
||||
},
|
||||
"folders": {
|
||||
"edit": {
|
||||
"title": "Modifier le dossier",
|
||||
"description": "Modifier les informations du dossier",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Mettre à jour"
|
||||
},
|
||||
"form": {
|
||||
"title": "Nom du dossier",
|
||||
"description": "Description",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Mettre à jour"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"not_found": {
|
||||
"description": "Le fichier que vous recherchez n'existe pas."
|
||||
}
|
||||
},
|
||||
"workspaces": {
|
||||
"mainWorkspace": "Mon espace",
|
||||
"form": {
|
||||
"title": "Nom de l'espace",
|
||||
"description": "Description",
|
||||
"settings": "Paramètres de l'espace"
|
||||
},
|
||||
"create": {
|
||||
"title": "Nouvel espace",
|
||||
"description": "Un espace vous permet d'organiser vos fichiers en équipe.",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Créer l'espace"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Modifier l'espace",
|
||||
"description": "Modifier les informations de l'espace",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Mettre à jour"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"createFolder": {
|
||||
"modal": {
|
||||
"title": "Créer un dossier",
|
||||
"label": "Nom du dossier",
|
||||
"placeholder": "Entrez le nom du dossier",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Créer"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
"toast": "Déposez vos fichiers ici pour les transférer dans {{title}}",
|
||||
"toast_no_rights": "Vous n'avez pas les droits nécessaires pour transférer des fichiers dans {{title}}",
|
||||
"steps": {
|
||||
"preparing": "Préparation du transfert...",
|
||||
"create_folders": "Création des dossiers en cours..."
|
||||
},
|
||||
"files": {
|
||||
"description_one": "{{count}} fichier en cours de transfert",
|
||||
"description_other": "{{count}} fichiers en cours de transfert",
|
||||
"description_done_one": "{{count}} fichier transféré",
|
||||
"description_done_other": "{{count}} fichiers transférés"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"toast_one": "Élément supprimé",
|
||||
"toast_other": "{{count}} éléments supprimés",
|
||||
"low_rights_toast": "Vous ne pouvez pas restaurer ces éléments car vous n'avez pas les droits nécessaires.",
|
||||
"toast_error_one": "Une erreur est survenue lors de la suppression de l'élément.",
|
||||
"toast_error_other": "Une erreur est survenue lors de la suppression des éléments."
|
||||
},
|
||||
"hard_delete": {
|
||||
"toast_one": "Élément supprimé définitivement",
|
||||
"toast_other": "{{count}} éléments supprimés définitivement"
|
||||
},
|
||||
"restore": {
|
||||
"toast_one": "Élément restauré",
|
||||
"toast_other": "{{count}} éléments restaurés"
|
||||
},
|
||||
"move": {
|
||||
"toast_one": "{{count}} élément déplacé",
|
||||
"toast_other": "{{count}} éléments déplacés"
|
||||
},
|
||||
"share": {
|
||||
"modal": {
|
||||
"title": "Partager l'espace"
|
||||
}
|
||||
},
|
||||
"rename": {
|
||||
"modal": {
|
||||
"title": "Renommer",
|
||||
"label": "Nouveau nom",
|
||||
"placeholder": "Entrez le nouveau nom",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Renommer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"years_ago_one": "il y a {{count}} an",
|
||||
"years_ago_other": "il y a {{count}} ans",
|
||||
"months_ago_one": "il y a {{count}} mois",
|
||||
"months_ago_other": "il y a {{count}} mois",
|
||||
"weeks_ago_one": "il y a {{count}} semaine",
|
||||
"weeks_ago_other": "il y a {{count}} semaines",
|
||||
"days_ago_one": "il y a {{count}} jour",
|
||||
"days_ago_other": "il y a {{count}} jours",
|
||||
"hours_ago_one": "il y a {{count}} heure",
|
||||
"hours_ago_other": "il y a {{count}} heures",
|
||||
"minutes_ago_one": "il y a {{count}} minute",
|
||||
"minutes_ago_other": "il y a {{count}} minutes",
|
||||
"seconds_ago": "il y a quelques secondes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"translation": {
|
||||
"entitlements": {
|
||||
"can_upload": {
|
||||
"cannot_upload": "U heeft niet de benodigde rechten om bestanden te uploaden."
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"title": "U moet ingelogd zijn om toegang te krijgen tot de bestanden.",
|
||||
"button": "Login"
|
||||
},
|
||||
"403": {
|
||||
"title": "U beschikt niet over de benodigde rechten om dit bestand te openen.",
|
||||
"button": "Home"
|
||||
},
|
||||
"file_download_modal": {
|
||||
"error": {
|
||||
"no_url_or_title": "Dit bestand heeft geen URL of titel."
|
||||
},
|
||||
"suspicious": {
|
||||
"title": "Dit bestand is verdacht"
|
||||
},
|
||||
"file_too_large_to_analyze": {
|
||||
"title": "Dit bestand is te groot om te worden gescand"
|
||||
},
|
||||
"analyzing": {
|
||||
"title": "Dit bestand wordt nog steeds gescand"
|
||||
},
|
||||
"description": "Weet u zeker dat u dit bestand wilt downloaden?",
|
||||
"download": "Toch downloaden",
|
||||
"cancel": "Annuleren"
|
||||
},
|
||||
"file_preview": {
|
||||
"suspicious": {
|
||||
"title": "Verdacht bestand",
|
||||
"description": "Dit bestand is verdacht. Het is alleen voor jou zichtbaar.",
|
||||
"download": "Bestand downloaden"
|
||||
},
|
||||
"error": {
|
||||
"title": "Er is een fout opgetreden tijdens het laden van het document.",
|
||||
"description": "Probeer het later opnieuw."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Bestandstype niet ondersteund voor voorbeeld",
|
||||
"description": "Om het bestand te bekijken, downloadt u het naar uw apparaat.",
|
||||
"download": "Bestand downloaden",
|
||||
"heic_title": "HEIC bestanden worden nog niet ondersteund voor voorbeeld."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Annuleren"
|
||||
},
|
||||
"clipboard": {
|
||||
"success": "Gekopieërd naar klembord",
|
||||
"error": "Kopiëren naar klembord mislukt"
|
||||
},
|
||||
"app_title": "Calendars",
|
||||
"app_description": "Kalenderbeheer",
|
||||
"welcome": "Welkom",
|
||||
"logout": "Uitloggen",
|
||||
"login": "Inloggen",
|
||||
"my_account": "Mijn account",
|
||||
"api": {
|
||||
"error": {
|
||||
"unexpected": "Er is een onverwachte fout opgetreden."
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "Eenvoudige opslag en delen",
|
||||
"subtitle": "Een moderne kalenderapplicatie ontworpen voor Franse publieke teams. Soevereine hosting in Frankrijk, sterke beveiliging en naadloze synchronisatie met alle LaSuite-apps.",
|
||||
"main_button": "Inloggen",
|
||||
"more": "Meer informatie"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Geef feedback",
|
||||
"modal": {
|
||||
"title": "Nieuwe feedback",
|
||||
"description": "Calendars is in ontwikkeling: uw feedback telt! Kies hoe u uw ideeën wilt delen:",
|
||||
"buttons": {
|
||||
"form": {
|
||||
"title": "Geef snel feedback",
|
||||
"description": "30 seconden om ons te vertellen wat u ervan vindt of een bug te melden"
|
||||
},
|
||||
"tchap": {
|
||||
"title": "Schrijf op Tchap",
|
||||
"description": "Directe uitwisseling met ons team"
|
||||
},
|
||||
"visio": {
|
||||
"title": "Praten op Videogesprek",
|
||||
"description": "20 min voor een diepgaand gesprek"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedback_widget": {
|
||||
"shortTitle": "Feedback?",
|
||||
"title": "Heeft u feedback?",
|
||||
"placeholder": "Deel hier uw feedback...",
|
||||
"email_placeholder": "Uw e-mailadres...",
|
||||
"submit_text": "Feedback verzenden",
|
||||
"success_text": "Bedankt voor uw feedback!",
|
||||
"success_text2": "Indien u vragen heeft, nemen wij spoedig contact met u op."
|
||||
},
|
||||
"authentication": {
|
||||
"error": {
|
||||
"alpha": "Deze applicatie bevindt zich in de alfa-fase en de toegang ertoe is beperkt.",
|
||||
"user_cannot_access_app": "U heeft niet de benodigde rechten om toegang te krijgen tot deze applicatie. Neem contact op met uw beheerder."
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"calc": "Spreadsheet",
|
||||
"doc": "Document",
|
||||
"image": "Afbeelding",
|
||||
"other": "overige",
|
||||
"pdf": "PDF",
|
||||
"powerpoint": "Presentatie",
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"archive": "Archief"
|
||||
},
|
||||
"roles": {
|
||||
"administrator": "Beheerder",
|
||||
"editor": "Redacteur",
|
||||
"reader": "Lezer",
|
||||
"owner": "Eigenaar"
|
||||
},
|
||||
"time": {
|
||||
"years_ago_one": "{{count}} jaar geleden",
|
||||
"years_ago_other": "{{count}} jaren geleden",
|
||||
"months_ago_one": "{{count}} maand geleden",
|
||||
"months_ago_other": "{{count}} maanden geleden",
|
||||
"weeks_ago_one": "{{count}} week geleden",
|
||||
"weeks_ago_other": "{{count}} weken geleden",
|
||||
"days_ago_one": "{{count}} dag geleden",
|
||||
"days_ago_other": "{{count}} dagen geleden",
|
||||
"hours_ago_one": "{{count}} uur geleden",
|
||||
"hours_ago_other": "{{count}} uren geleden",
|
||||
"minutes_ago_one": "{{count}} minuut geleden",
|
||||
"minutes_ago_other": "{{count}} minuten geleden",
|
||||
"seconds_ago": "een paar seconden geleden"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/frontend/apps/calendars/src/features/i18n/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const splitLocaleCode = (language: string) => {
|
||||
const locale = language.split(/[-_]/);
|
||||
return {
|
||||
language: locale[0],
|
||||
region: locale.length === 2 ? locale[1] : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const capitalizeRegion = (language: string) => {
|
||||
const { language: lang, region } = splitLocaleCode(language);
|
||||
return lang + (region ? "-" + region.toUpperCase() : "");
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
nav {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
background-color: #fff;
|
||||
border: 1px grey solid;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Auth } from "@/features/auth/Auth";
|
||||
|
||||
/**
|
||||
* This layout is used for the global contexts (auth, etc).
|
||||
*/
|
||||
export const GlobalLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <Auth>{children}</Auth>;
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { LanguagePicker, useResponsive } from "@gouvfr-lasuite/ui-kit";
|
||||
import { useAuth } from "@/features/auth/Auth";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchAPI } from "@/features/api/fetchApi";
|
||||
import { Feedback } from "@/features/feedback/Feedback";
|
||||
import { Gaufre } from "@/features/ui/components/gaufre/Gaufre";
|
||||
import { UserProfile } from "@/features/ui/components/user/UserProfile";
|
||||
|
||||
export const HeaderIcon = () => {
|
||||
return (
|
||||
<div className="calendars__header__left">
|
||||
<div className="calendars__header__logo" />
|
||||
<Feedback />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeaderRight = () => {
|
||||
const { user } = useAuth();
|
||||
const { isTablet } = useResponsive();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isTablet && (
|
||||
<>
|
||||
<Gaufre />
|
||||
<UserProfile />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LanguagePickerUserMenu = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const { user, refreshUser } = useAuth();
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(user?.language);
|
||||
|
||||
// We must set the language to lowercase because django does not use "en-US", but "en-us".
|
||||
|
||||
const languages = [
|
||||
{
|
||||
label: "Français",
|
||||
value: "fr-fr",
|
||||
shortLabel: "FR",
|
||||
isChecked: selectedLanguage === "fr-fr",
|
||||
},
|
||||
{
|
||||
label: "English",
|
||||
value: "en-us",
|
||||
shortLabel: "EN",
|
||||
isChecked: selectedLanguage === "en-us",
|
||||
},
|
||||
{
|
||||
label: "Nederlands",
|
||||
value: "nl-nl",
|
||||
shortLabel: "NL",
|
||||
isChecked: selectedLanguage === "nl-nl",
|
||||
},
|
||||
{
|
||||
label: "Deutsch",
|
||||
value: "de-de",
|
||||
shortLabel: "DE",
|
||||
isChecked: selectedLanguage === "de-de",
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setSelectedLanguage(value);
|
||||
i18n.changeLanguage(value).catch((err) => {
|
||||
console.error("Error changing language", err);
|
||||
});
|
||||
if (user) {
|
||||
fetchAPI(`users/${user.id}/`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ language: value }),
|
||||
}).then(() => {
|
||||
void refreshUser?.();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguagePicker
|
||||
languages={languages}
|
||||
size="small"
|
||||
onChange={onChange}
|
||||
compact
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
@use "sass:map";
|
||||
@use "@/styles/cunningham-tokens-sass" as *;
|
||||
|
||||
$tablet: map.get($themes, "default", "globals", "breakpoints", "tablet");
|
||||
|
||||
@media (max-width: $tablet) {
|
||||
.c__header {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.c__header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
@media (max-width: $tablet) {
|
||||
.calendars__header__login-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c__feedback__button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c__dropdown-menu-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lasuite-gaufre-btn {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.calendars__home__left-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.calendars__home__left-panel__gaufre {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Gaufre } from "@/features/ui/components/gaufre/Gaufre";
|
||||
import { UserProfile } from "@/features/ui/components/user/UserProfile";
|
||||
import { useResponsive } from "@gouvfr-lasuite/ui-kit";
|
||||
|
||||
export const LeftPanelMobile = () => {
|
||||
const { isTablet } = useResponsive();
|
||||
|
||||
if (!isTablet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="calendars__home__left-panel">
|
||||
<div className="calendars__home__left-panel__gaufre">
|
||||
<Gaufre />
|
||||
<UserProfile />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { MainLayout } from "@gouvfr-lasuite/ui-kit";
|
||||
import { GlobalLayout } from "../global/GlobalLayout";
|
||||
import { HeaderRight } from "../header/Header";
|
||||
import { Toaster } from "@/features/ui/components/toaster/Toaster";
|
||||
import { LeftPanelMobile } from "@/features/layouts/components/left-panel/LeftPanelMobile";
|
||||
|
||||
export const getSimpleLayout = (page: React.ReactElement) => {
|
||||
return <SimpleLayout>{page}</SimpleLayout>;
|
||||
};
|
||||
|
||||
/**
|
||||
* This layout is used for the simple pages.
|
||||
* It is used to display the header and provide
|
||||
* Auth context to the children.
|
||||
*/
|
||||
export const SimpleLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div>
|
||||
<GlobalLayout>
|
||||
<MainLayout
|
||||
enableResize
|
||||
hideLeftPanelOnDesktop={true}
|
||||
leftPanelContent={<LeftPanelMobile />}
|
||||
rightHeaderContent={<HeaderRight />}
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</MainLayout>
|
||||
</GlobalLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Button } from "@openfun/cunningham-react";
|
||||
import React, { ReactElement, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type BreadcrumbItem = {
|
||||
content: ReactNode;
|
||||
};
|
||||
|
||||
export interface BreadcrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
onBack?: () => void;
|
||||
displayBack?: boolean;
|
||||
}
|
||||
|
||||
export const Breadcrumbs = ({
|
||||
items,
|
||||
onBack,
|
||||
displayBack = false,
|
||||
}: BreadcrumbsProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="c__breadcrumbs">
|
||||
{displayBack && (
|
||||
<Button
|
||||
icon={<span className="material-icons">arrow_back</span>}
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
className="mr-t"
|
||||
onClick={onBack}
|
||||
disabled={items.length <= 1}
|
||||
>
|
||||
{t("Précédent")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 && (
|
||||
<span className="material-icons c__breadcrumbs__separator">
|
||||
chevron_right
|
||||
</span>
|
||||
)}
|
||||
{React.cloneElement(item.content as ReactElement<HTMLDivElement>, {
|
||||
className: `${
|
||||
(
|
||||
(item.content as ReactElement<HTMLDivElement>).props as {
|
||||
className?: string;
|
||||
}
|
||||
).className || ""
|
||||
} ${index === items.length - 1 ? "active" : ""}`,
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
.c__breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
|
||||
&__separator {
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
}
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.c__breadcrumbs__button {
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 400;
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--neutral--tertiary
|
||||
);
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { CheckIcon } from "../icon/Icon";
|
||||
|
||||
interface CircularProgressProps {
|
||||
progress: number;
|
||||
size?: number;
|
||||
strokeWidth?: number;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
transitionDuration?: number;
|
||||
}
|
||||
|
||||
export const CircularProgress = ({
|
||||
progress,
|
||||
primaryColor = "#1a237e",
|
||||
secondaryColor = "#f0f0f0",
|
||||
transitionDuration = 0.3,
|
||||
}: CircularProgressProps) => {
|
||||
if (progress > 100) {
|
||||
progress = 100;
|
||||
}
|
||||
|
||||
const strokeWidth = 2;
|
||||
|
||||
// Fixed size of 24px for the component
|
||||
const fixedSize = 24;
|
||||
// Fixed size of 20px for the circle
|
||||
const circleSize = 20;
|
||||
|
||||
// Calculate the radius based on the circle size
|
||||
const radius = circleSize / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
// Calculate the dash offset based on progress
|
||||
const dashOffset = circumference - (progress / 100) * circumference;
|
||||
|
||||
// Determine if we should show the check mark
|
||||
const isComplete = progress >= 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: `${fixedSize}px`,
|
||||
height: `${fixedSize}px`,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{!isComplete && (
|
||||
<svg
|
||||
width={fixedSize}
|
||||
height={fixedSize}
|
||||
viewBox={`0 0 ${fixedSize} ${fixedSize}`}
|
||||
style={{ transform: isComplete ? "rotate(0deg)" : "rotate(-90deg)" }}
|
||||
>
|
||||
{/* Background circle - centered in the 24x24 container */}
|
||||
<circle
|
||||
cx={fixedSize / 2}
|
||||
cy={fixedSize / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={secondaryColor}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
|
||||
{/* Progress circle - centered in the 24x24 container */}
|
||||
{!isComplete && (
|
||||
<circle
|
||||
cx={fixedSize / 2}
|
||||
cy={fixedSize / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={primaryColor}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={dashOffset}
|
||||
strokeLinecap="round"
|
||||
style={{
|
||||
transition: `stroke-dashoffset ${transitionDuration}s ease-in-out`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)}
|
||||
{/* Check mark when complete */}
|
||||
{isComplete && <CheckIcon />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { LaGaufreV2 } from "@gouvfr-lasuite/ui-kit";
|
||||
import {
|
||||
removeQuotes,
|
||||
useCunninghamTheme,
|
||||
} from "../../cunningham/useCunninghamTheme";
|
||||
import { useConfig } from "@/features/config/ConfigProvider";
|
||||
import { useAppContext } from "@/pages/_app";
|
||||
|
||||
export const Gaufre = () => {
|
||||
const { config } = useConfig();
|
||||
const { theme: themeName } = useAppContext();
|
||||
const hideGaufre = config?.FRONTEND_HIDE_GAUFRE;
|
||||
const theme = useCunninghamTheme();
|
||||
const widgetPath = removeQuotes(theme.components.gaufre.widgetPath);
|
||||
const apiUrl = removeQuotes(theme.components.gaufre.apiUrl);
|
||||
|
||||
if (hideGaufre) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LaGaufreV2
|
||||
widgetPath={widgetPath}
|
||||
apiUrl={apiUrl}
|
||||
showMoreLimit={themeName === "anct" ? 100 : 6}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
.calendars__generic-disclaimer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - var(--header-height));
|
||||
padding: 0 0.5rem;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
&__image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export const GenericDisclaimer = ({
|
||||
message,
|
||||
imageSrc,
|
||||
children,
|
||||
}: {
|
||||
message: string;
|
||||
imageSrc: string;
|
||||
} & PropsWithChildren) => {
|
||||
return (
|
||||
<div className="calendars__generic-disclaimer">
|
||||
<div className="calendars__generic-disclaimer__content">
|
||||
<img
|
||||
className="calendars__generic-disclaimer__content__image"
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
/>
|
||||
<p>{message}</p>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export const CheckIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.1 14.6L16.15 7.55L14.75 6.15L9.1 11.8L6.25 8.95L4.85 10.35L9.1 14.6ZM10.5 20C9.11667 20 7.81667 19.7375 6.6 19.2125C5.38333 18.6875 4.325 17.975 3.425 17.075C2.525 16.175 1.8125 15.1167 1.2875 13.9C0.7625 12.6833 0.5 11.3833 0.5 10C0.5 8.61667 0.7625 7.31667 1.2875 6.1C1.8125 4.88333 2.525 3.825 3.425 2.925C4.325 2.025 5.38333 1.3125 6.6 0.7875C7.81667 0.2625 9.11667 0 10.5 0C11.8833 0 13.1833 0.2625 14.4 0.7875C15.6167 1.3125 16.675 2.025 17.575 2.925C18.475 3.825 19.1875 4.88333 19.7125 6.1C20.2375 7.31667 20.5 8.61667 20.5 10C20.5 11.3833 20.2375 12.6833 19.7125 13.9C19.1875 15.1167 18.475 16.175 17.575 17.075C16.675 17.975 15.6167 18.6875 14.4 19.2125C13.1833 19.7375 11.8833 20 10.5 20Z"
|
||||
fill="#000091"
|
||||
opacity="0"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
from="0"
|
||||
to="1"
|
||||
dur="0.3s"
|
||||
begin="0s"
|
||||
fill="freeze"
|
||||
/>
|
||||
</path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
.infinite-scroll {
|
||||
&__loading-component {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__trigger {
|
||||
min-height: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useRef, useCallback, ReactNode } from "react";
|
||||
import { Loader, useCunningham } from "@openfun/cunningham-react";
|
||||
|
||||
interface InfiniteScrollProps {
|
||||
/** Whether there are more items to load */
|
||||
hasNextPage: boolean;
|
||||
/** Whether currently fetching the next page */
|
||||
isFetchingNextPage: boolean;
|
||||
/** Function to call when more items should be loaded */
|
||||
fetchNextPage: () => void;
|
||||
/** Children to render */
|
||||
children: ReactNode;
|
||||
/** Optional loading component to show at the bottom */
|
||||
loadingComponent?: ReactNode;
|
||||
/** Distance from bottom to trigger loading (in pixels) */
|
||||
rootMargin?: string;
|
||||
/** Intersection threshold (0-1) */
|
||||
threshold?: number;
|
||||
/** Additional CSS class for the container */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* InfiniteScroll component that automatically loads more content when the user
|
||||
* scrolls near the bottom of the container.
|
||||
*
|
||||
* Uses Intersection Observer API to detect when the trigger element comes into view
|
||||
* and automatically calls fetchNextPage when more content is available.
|
||||
*/
|
||||
export const InfiniteScroll = ({
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
children,
|
||||
loadingComponent,
|
||||
rootMargin = "300px",
|
||||
threshold = 1,
|
||||
className,
|
||||
}: InfiniteScrollProps) => {
|
||||
const { t: tc } = useCunningham();
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleIntersection = useCallback(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
const [entry] = entries;
|
||||
|
||||
if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
[hasNextPage, isFetchingNextPage, fetchNextPage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
threshold,
|
||||
rootMargin,
|
||||
});
|
||||
|
||||
if (loadMoreRef.current) {
|
||||
observer.observe(loadMoreRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (loadMoreRef.current) {
|
||||
observer.unobserve(loadMoreRef.current);
|
||||
}
|
||||
};
|
||||
}, [handleIntersection, threshold, rootMargin]);
|
||||
|
||||
const defaultLoadingComponent = (
|
||||
<div className="infinite-scroll__loading-component">
|
||||
<Loader size="small" aria-label={tc("components.datagrid.loader_aria")} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{children}
|
||||
{/* Infinite scroll trigger and loading indicator */}
|
||||
<div ref={loadMoreRef} className="infinite-scroll__trigger">
|
||||
{isFetchingNextPage && (loadingComponent || defaultLoadingComponent)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { InfiniteScroll } from "./InfiniteScroll";
|
||||
@@ -0,0 +1,19 @@
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--c--globals--spacings--3xs, 0.25rem) 0;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: var(--c--globals--font--sizes--sm);
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__right-content {
|
||||
&__string {
|
||||
font-size: var(--c--globals--font--sizes--sm);
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
type InfoRowProps = {
|
||||
label: string;
|
||||
rightContent: React.ReactNode | string;
|
||||
};
|
||||
|
||||
export const InfoRow = ({ label, rightContent }: InfoRowProps) => {
|
||||
return (
|
||||
<div className="info-row">
|
||||
<div className="info-row__label">{label}</div>
|
||||
<div
|
||||
className={clsx("info-row__right-content", {
|
||||
"info-row__right-content__string": typeof rightContent === "string",
|
||||
})}
|
||||
>
|
||||
{rightContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export const ResponsiveDivs = () => {
|
||||
return (
|
||||
<>
|
||||
<div id="responsive-tablet"></div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const isTablet = () => {
|
||||
return (
|
||||
getComputedStyle(
|
||||
document.querySelector("#responsive-tablet")!
|
||||
).getPropertyValue("display") === "block"
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
@use "sass:map";
|
||||
@use "@/styles/cunningham-tokens-sass" as *;
|
||||
|
||||
$tablet: map.get($themes, "default", "globals", "breakpoints", "tablet");
|
||||
|
||||
#responsive-tablet {
|
||||
display: none;
|
||||
|
||||
@media (max-width: $tablet) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||