From d00ba9cfe8edaafcf974732dcb128c8d42308c11 Mon Sep 17 00:00:00 2001 From: Atomys Date: Thu, 19 Oct 2023 02:04:20 +0200 Subject: [PATCH] refactor: campus management with generators (#530) **Describe the pull request** Previously, our campus management system followed a simplistic naming convention that could not accommodate more intricate naming patterns. This refactor provides the flexibility to adopt complex naming schemes, ensuring our system remains adaptive and accommodating to diverse naming requirements like spacing name or special characters. **Checklist** - [x] I have made the modifications or added tests related to my PR - [x] I have run the tests and linters locally and they pass - [x] I have added/updated the documentation for my RP --------- Signed-off-by: Atomys --- web/ui/generators/generateCampusTypes.ts | 99 +++++++++++++ web/ui/jest.setup.ts | 1 + web/ui/package.json | 3 +- .../ClusterMap/ClusterContainer.tsx | 10 +- web/ui/src/components/ClusterMap/types.d.ts | 13 +- web/ui/src/components/Search/Search.tsx | 2 +- .../containers/clusters/ClusterSidebar.tsx | 65 +++++---- web/ui/src/lib/clustersMap/campus.ts | 14 +- web/ui/src/lib/clustersMap/campus/helsinki.ts | 4 +- web/ui/src/lib/clustersMap/campus/lausanne.ts | 4 +- web/ui/src/lib/clustersMap/campus/madrid.ts | 4 +- web/ui/src/lib/clustersMap/campus/malaga.ts | 4 +- web/ui/src/lib/clustersMap/campus/mulhouse.ts | 4 +- web/ui/src/lib/clustersMap/campus/paris.ts | 4 +- web/ui/src/lib/clustersMap/campus/seoul.ts | 4 +- web/ui/src/lib/clustersMap/campus/tokyo.ts | 4 +- web/ui/src/lib/clustersMap/campus/vienna.ts | 4 +- .../src/lib/clustersMap/campus/wolfsburg.ts | 4 +- .../src/lib/clustersMap/campuses.generated.ts | 37 +++++ web/ui/src/lib/clustersMap/index.ts | 44 +----- web/ui/src/lib/clustersMap/types.d.ts | 23 +--- .../src/lib/clustersMap/types.generated.d.ts | 19 +++ web/ui/src/lib/clustersMap/utils.ts | 21 +++ web/ui/src/lib/prototypes/string.js | 22 +++ web/ui/src/lib/searchEngine.ts | 19 +-- .../[clusterSlug].tsx} | 67 +++++---- web/ui/src/pages/clusters/index.tsx | 16 ++- web/ui/src/types/globals.d.ts | 3 + web/ui/tsconfig.json | 3 +- web/ui/yarn.lock | 130 ++++++++++++++++-- 30 files changed, 473 insertions(+), 178 deletions(-) create mode 100644 web/ui/generators/generateCampusTypes.ts create mode 100644 web/ui/src/lib/clustersMap/campuses.generated.ts create mode 100644 web/ui/src/lib/clustersMap/types.generated.d.ts create mode 100644 web/ui/src/lib/clustersMap/utils.ts rename web/ui/src/pages/clusters/{[campusName]/[cluster].tsx => [campusSlug]/[clusterSlug].tsx} (65%) diff --git a/web/ui/generators/generateCampusTypes.ts b/web/ui/generators/generateCampusTypes.ts new file mode 100644 index 00000000..9f5c870e --- /dev/null +++ b/web/ui/generators/generateCampusTypes.ts @@ -0,0 +1,99 @@ +const fs = require('fs'); +const path = require('path'); +const c = require('console'); +require('../src/lib/prototypes/string'); + +// Generic warning to add at the top of each the generated files +const GeneratedWarning = `// DO NOT EDIT THIS FILE MANUALLY - IT IS GENERATED FROM THE CONTENT OF THE CAMPUS FOLDER\n// RUN \`yarn generate:campus\` TO REGENERATE IT\n\n`; + +// Paths to the files used in the generation +const rootPath = path.join(__dirname, '../src/lib/clustersMap'); +const directoryPath = path.join(rootPath, './campus'); + +/** + * generateCampusTypes generates the types.generated.ts file containing the + * CampusIdentifier enum. This enum is used to identify the campus in the + * interface. + * @param campusIdentifiers list of campus identifiers (e.g. ['helsinki', 'paris', ...]) + */ +const generateCampusTypes = (campusIdentifiers: any[]): void => { + c.log('⠙ Generate campus types...'); + + const enumContent = `${GeneratedWarning}/** + * List of all campus names present in the interface as their identifier. + * Identifier must be in camelCase without spaces or special characters. It + * must be unique in the list. + */ +export type CampusIdentifier = +${campusIdentifiers.map((id) => ` | '${id}'`).join('\n')}; +`; + + fs.writeFileSync(path.join(rootPath, 'types.generated.d.ts'), enumContent); + + c.log('✔ Generated campus types.generated.ts file successfully!'); +}; + +/** + * generateCampusClasses generates the campuses.generated.ts file containing the + * CampusIdentifier enum. This enum is used to identify the campus in the + * interface. + * @param campusIdentifiers list of campus identifiers (e.g. ['helsinki', 'paris', ...]) + */ +const generateCampusClasses = (campusIdentifiers: any[]): void => { + c.log('⠙ Generate campus types...'); + + const campusesContent = `${GeneratedWarning}import { ICampus } from './types'; +import { CampusIdentifier } from './types.generated'; + +${campusIdentifiers + .map((id) => `import { ${id.toTitleCase()} } from './campus/${id}';`) + .join('\n')} + +/** + * Campuses represents the list of campuses present in the application. + * Particulary, used in the cluster map. + * + * It is a const, so it can be accessed from anywhere in the application. + * You can add a new campus by define the campus in the \`campus\` folder + * (see \`campus/paris.ts\` for an example) and run \`yarn generate:campus\` + */ +export const Campuses: Record = { +${campusIdentifiers + .map((id) => ` ${id}: new ${id.toTitleCase()}(),`) + .join('\n')} +}; +`; + + fs.writeFileSync( + path.join(rootPath, 'campuses.generated.ts'), + campusesContent, + ); + + c.log('✔ Generated campuses.generated.ts file successfully!'); +}; + +/** + * Read the campus directory and generate the types and classes files. + * The campus directory contains a file for each campus. The file name is the + * campus identifier. The file contains the class definition of the campus. + * The class must implement the ICampus interface. + * The campus identifier is used to identify the campus in the interface. + * The campus identifier must be in camelCase without spaces or special + * characters. It must be unique in the list. + */ +c.log('⠙ Read campus directory...'); +fs.readdir(directoryPath, (err: any, files: any[]) => { + if (err) { + c.error('cannot read directory:', err); + return; + } + + const campusIdentifiers = files + .filter((file) => path.extname(file) === '.ts') + .map((file) => path.basename(file, path.extname(file))); + + generateCampusTypes(campusIdentifiers); + generateCampusClasses(campusIdentifiers); +}); + +c.log('✔ Generated campus successfully!'); diff --git a/web/ui/jest.setup.ts b/web/ui/jest.setup.ts index 306452c5..3a8d996f 100644 --- a/web/ui/jest.setup.ts +++ b/web/ui/jest.setup.ts @@ -4,3 +4,4 @@ // Used for __tests__/testing-library.js // Learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/extend-expect'; +import './src/lib/prototypes/string'; diff --git a/web/ui/package.json b/web/ui/package.json index b123617d..7d2ef2fc 100644 --- a/web/ui/package.json +++ b/web/ui/package.json @@ -6,8 +6,9 @@ "node": ">=18.0.0" }, "scripts": { - "generate": "yarn run generate:gql", + "generate": "yarn run generate:gql && yarn run generate:campus", "generate:gql": "graphql-codegen --config graphqlcodegen.yml", + "generate:campus": "ts-node generators/generateCampusTypes.ts", "dev": "next dev", "dev:watch": "concurrently \"yarn run dev\" \"yarn run generate --watch\"", "build": "next build", diff --git a/web/ui/src/components/ClusterMap/ClusterContainer.tsx b/web/ui/src/components/ClusterMap/ClusterContainer.tsx index 7756a2cf..943c1061 100644 --- a/web/ui/src/components/ClusterMap/ClusterContainer.tsx +++ b/web/ui/src/components/ClusterMap/ClusterContainer.tsx @@ -53,7 +53,10 @@ export const ClusterContainer: ClusterContainerComponent = ({ } = useRouter(); const [highlight, setHighlight] = useState(false); const { data, error, networkStatus } = useClusterViewQuery({ - variables: { campusName: campus, identifierPrefix: cluster }, + variables: { + campusName: campus.name(), + identifierPrefix: cluster.identifier(), + }, fetchPolicy: 'network-only', // This is a workaround due to missing websocket implementation. // TODO: Remove this when websocket is implemented. @@ -87,10 +90,7 @@ export const ClusterContainer: ClusterContainerComponent = ({ highlightedIdentifier === identifier ? 'HIGHLIGHT' : 'DIMMED', }} > - + JSX.Element; - }; -}[CampusNames]; + campus: ICampus; + cluster: ICluster; + children: (props: ClusterContainerChildrenProps) => JSX.Element; +}; type ClusterContainerComponent = (props: ClusterContainerProps) => JSX.Element; diff --git a/web/ui/src/components/Search/Search.tsx b/web/ui/src/components/Search/Search.tsx index 6d13820b..88b4c293 100644 --- a/web/ui/src/components/Search/Search.tsx +++ b/web/ui/src/components/Search/Search.tsx @@ -120,7 +120,7 @@ export const Search: SearchComponent = ({ diff --git a/web/ui/src/containers/clusters/ClusterSidebar.tsx b/web/ui/src/containers/clusters/ClusterSidebar.tsx index acb393f6..a4731499 100644 --- a/web/ui/src/containers/clusters/ClusterSidebar.tsx +++ b/web/ui/src/containers/clusters/ClusterSidebar.tsx @@ -3,47 +3,50 @@ import useSidebar, { Menu, MenuCategory, MenuItem } from '@components/Sidebar'; import { useMe } from '@ctx/currentUser'; import { useClusterSidebarDataQuery } from '@graphql.d'; import { isFirstLoading } from '@lib/apollo'; -import Campuses, { CampusNames } from '@lib/clustersMap'; +import Campuses, { + CampusIdentifier, + ICampus, + ICluster, +} from '@lib/clustersMap'; import '@lib/prototypes/string'; import { clusterURL } from '@lib/searchEngine'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React from 'react'; +import '@lib/prototypes/string'; /** * ClusterSidebar is the sidebar for the cluster page. It contains the cluster * menu statically defined. Used accross all cluster pages. - * @param {string} activeCampusName - The campus used to match the current campus - * @param {string} activeClusterIdentifier - The cluster used to match the current cluster + * @param {string} activeCampus - The campus used to match the current campus + * @param {string} activeCluster - The cluster used to match the current cluster * @returns {JSX.Element} The sub sidebar component */ export const ClusterSidebar = ({ - activeCampusName, - activeClusterIdentifier, + activeCampus, + activeCluster, }: { - activeCampusName: CampusNames; - activeClusterIdentifier: string; + activeCampus: ICampus; + activeCluster: ICluster; }) => { const { Sidebar } = useSidebar(); const router = useRouter(); - const campusKeys = Object.keys(Campuses) as Array; - const currentCampusData = Campuses[activeCampusName]; + const campusKeys = Object.keys(Campuses) as Array; const { me } = useMe(); const { data: { locationsStatsByPrefixes = [] } = {}, networkStatus } = useClusterSidebarDataQuery({ variables: { - campusName: activeCampusName, - clusterPrefixes: currentCampusData - .clusters() - .map((c) => c.identifier()), + campusName: activeCampus.name(), + clusterPrefixes: activeCampus.clusters().map((c) => c.identifier()), }, }); - const myCampusName = me?.currentCampus?.name?.toLowerCase() || ''; + const myCampusidentifier = + me?.currentCampus?.name?.removeAccents().toCamelCase() || ''; const freePlacesPerCluster: { [key: string]: number } = locationsStatsByPrefixes .map((l) => { const totalWorkspaces = - currentCampusData.cluster(l.prefix)?.totalWorkspaces() || 0; + activeCampus.cluster(l.prefix)?.totalWorkspaces() || 0; return [l.prefix, totalWorkspaces - l.occupiedWorkspace]; }) .reduce( @@ -95,38 +98,42 @@ export const ClusterSidebar = ({ .sort((a, b) => { // Sort the campus list in alphabetical order and put the current // campus at the top. - return a?.equalsIgnoreCase(myCampusName) + return a?.equalsIgnoreCase(myCampusidentifier) ? -1 - : b?.equalsIgnoreCase(myCampusName) + : b?.equalsIgnoreCase(myCampusidentifier) ? 1 : a.localeCompare(b); }) - .map((campusName) => { - const campusData = Campuses[campusName]; - const isMyCampus = campusName?.equalsIgnoreCase(myCampusName); - const activeCampus = activeCampusName == campusName; + .map((campusIdentifier) => { + const campusData = Campuses[campusIdentifier]; + const isMyCampus = + campusIdentifier?.equalsIgnoreCase(myCampusidentifier); + const isActiveCampus = + activeCampus.identifier() == campusData.identifier(); return ( {campusData.clusters().map((cluster) => { const clusterIdentifier = cluster.identifier(); return ( {freePlacesPerCluster[clusterIdentifier]} diff --git a/web/ui/src/lib/clustersMap/campus.ts b/web/ui/src/lib/clustersMap/campus.ts index 3d23227d..455da445 100644 --- a/web/ui/src/lib/clustersMap/campus.ts +++ b/web/ui/src/lib/clustersMap/campus.ts @@ -1,5 +1,7 @@ -import { CampusNames, ICampus, ICluster } from './types'; - +import Campuses from '.'; +import { ICampus, ICluster } from './types'; +import { CampusIdentifier } from './types.generated'; +import '@lib/prototypes/string'; /** * Campus class represents a campus in the cluster map. It contains the * campus name, emoji, extractor function, and the list of clusters. @@ -9,10 +11,16 @@ export class Campus implements ICampus { throw new Error('Method not implemented.'); } - name(): CampusNames { + name(): string { throw new Error('Method not implemented.'); } + identifier(): CampusIdentifier { + return Object.keys(Campuses).find( + (key) => Campuses[key as CampusIdentifier] === this, + ) as CampusIdentifier; + } + extractorRegexp(): RegExp { throw new Error('Method not implemented.'); } diff --git a/web/ui/src/lib/clustersMap/campus/helsinki.ts b/web/ui/src/lib/clustersMap/campus/helsinki.ts index fd7a26db..83d62db5 100644 --- a/web/ui/src/lib/clustersMap/campus/helsinki.ts +++ b/web/ui/src/lib/clustersMap/campus/helsinki.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Helsinki extends Campus implements ICampus { emoji = (): string => '🇫🇮'; - name = (): CampusNames => 'helsinki'; + name = (): string => 'Helsinki'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?p(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/lausanne.ts b/web/ui/src/lib/clustersMap/campus/lausanne.ts index d95ff1d2..ee10c681 100644 --- a/web/ui/src/lib/clustersMap/campus/lausanne.ts +++ b/web/ui/src/lib/clustersMap/campus/lausanne.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Lausanne extends Campus implements ICampus { emoji = (): string => '🇨🇭'; - name = (): CampusNames => 'lausanne'; + name = (): string => 'Lausanne'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?s(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/madrid.ts b/web/ui/src/lib/clustersMap/campus/madrid.ts index 1e6fedcf..90da496f 100644 --- a/web/ui/src/lib/clustersMap/campus/madrid.ts +++ b/web/ui/src/lib/clustersMap/campus/madrid.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Madrid extends Campus implements ICampus { emoji = (): string => '🇪🇸'; - name = (): CampusNames => 'madrid'; + name = (): string => 'Madrid'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?s(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/malaga.ts b/web/ui/src/lib/clustersMap/campus/malaga.ts index cdbc4966..0589d4b0 100644 --- a/web/ui/src/lib/clustersMap/campus/malaga.ts +++ b/web/ui/src/lib/clustersMap/campus/malaga.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Malaga extends Campus implements ICampus { emoji = (): string => '🇪🇸'; - name = (): CampusNames => 'malaga'; + name = (): string => 'Malaga'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?p(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/mulhouse.ts b/web/ui/src/lib/clustersMap/campus/mulhouse.ts index 322c92df..aa409baa 100644 --- a/web/ui/src/lib/clustersMap/campus/mulhouse.ts +++ b/web/ui/src/lib/clustersMap/campus/mulhouse.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Mulhouse extends Campus implements ICampus { emoji = (): string => '🇫🇷'; - name = (): CampusNames => 'mulhouse'; + name = (): string => 'Mulhouse'; extractorRegexp = (): RegExp => /(?k(?\d+))(?r(?\d+))(?p(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/paris.ts b/web/ui/src/lib/clustersMap/campus/paris.ts index c737a19e..a6fd951e 100644 --- a/web/ui/src/lib/clustersMap/campus/paris.ts +++ b/web/ui/src/lib/clustersMap/campus/paris.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Paris extends Campus implements ICampus { emoji = (): string => '🇫🇷'; - name = (): CampusNames => 'paris'; + name = (): string => 'Paris'; extractorRegexp = (): RegExp => /(?(?bess|paul|made)-f(?\d+(?:A|B|C|D)?))(?r(?\d+))(?s(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/seoul.ts b/web/ui/src/lib/clustersMap/campus/seoul.ts index 706b1e84..dcc059af 100644 --- a/web/ui/src/lib/clustersMap/campus/seoul.ts +++ b/web/ui/src/lib/clustersMap/campus/seoul.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Seoul extends Campus implements ICampus { emoji = (): string => '🇰🇷'; - name = (): CampusNames => 'seoul'; + name = (): string => 'Seoul'; extractorRegexp = (): RegExp => /(?cx?(?[\d]+))(?r(?\d+))(?s(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/tokyo.ts b/web/ui/src/lib/clustersMap/campus/tokyo.ts index e8e5ddcc..f18cf114 100644 --- a/web/ui/src/lib/clustersMap/campus/tokyo.ts +++ b/web/ui/src/lib/clustersMap/campus/tokyo.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Tokyo extends Campus implements ICampus { emoji = (): string => '🇯🇵'; - name = (): CampusNames => 'tokyo'; + name = (): string => 'Tokyo'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?s(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/vienna.ts b/web/ui/src/lib/clustersMap/campus/vienna.ts index 620d480e..a1dd6f7c 100644 --- a/web/ui/src/lib/clustersMap/campus/vienna.ts +++ b/web/ui/src/lib/clustersMap/campus/vienna.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Vienna extends Campus implements ICampus { emoji = (): string => '🇦🇹'; - name = (): CampusNames => 'vienna'; + name = (): string => 'Vienna'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?p(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campus/wolfsburg.ts b/web/ui/src/lib/clustersMap/campus/wolfsburg.ts index 950b44bd..4385428b 100644 --- a/web/ui/src/lib/clustersMap/campus/wolfsburg.ts +++ b/web/ui/src/lib/clustersMap/campus/wolfsburg.ts @@ -1,12 +1,12 @@ import { Campus } from '../campus'; import { Cluster } from '../cluster'; -import { CampusNames, ICampus } from '../types'; +import { ICampus } from '../types'; // export class Wolfsburg extends Campus implements ICampus { emoji = (): string => '🇩🇪'; - name = (): CampusNames => 'wolfsburg'; + name = (): string => 'Wolfsburg'; extractorRegexp = (): RegExp => /(?c(?\d+))(?r(?\d+))(?p(?\d+))/i; diff --git a/web/ui/src/lib/clustersMap/campuses.generated.ts b/web/ui/src/lib/clustersMap/campuses.generated.ts new file mode 100644 index 00000000..3510aa48 --- /dev/null +++ b/web/ui/src/lib/clustersMap/campuses.generated.ts @@ -0,0 +1,37 @@ +// DO NOT EDIT THIS FILE MANUALLY - IT IS GENERATED FROM THE CONTENT OF THE CAMPUS FOLDER +// RUN `yarn generate:campus` TO REGENERATE IT + +import { ICampus } from './types'; +import { CampusIdentifier } from './types.generated'; + +import { Helsinki } from './campus/helsinki'; +import { Lausanne } from './campus/lausanne'; +import { Madrid } from './campus/madrid'; +import { Malaga } from './campus/malaga'; +import { Mulhouse } from './campus/mulhouse'; +import { Paris } from './campus/paris'; +import { Seoul } from './campus/seoul'; +import { Tokyo } from './campus/tokyo'; +import { Vienna } from './campus/vienna'; +import { Wolfsburg } from './campus/wolfsburg'; + +/** + * Campuses represents the list of campuses present in the application. + * Particulary, used in the cluster map. + * + * It is a const, so it can be accessed from anywhere in the application. + * You can add a new campus by define the campus in the `campus` folder + * (see `campus/paris.ts` for an example) and run `yarn generate:campus` + */ +export const Campuses: Record = { + helsinki: new Helsinki(), + lausanne: new Lausanne(), + madrid: new Madrid(), + malaga: new Malaga(), + mulhouse: new Mulhouse(), + paris: new Paris(), + seoul: new Seoul(), + tokyo: new Tokyo(), + vienna: new Vienna(), + wolfsburg: new Wolfsburg(), +}; diff --git a/web/ui/src/lib/clustersMap/index.ts b/web/ui/src/lib/clustersMap/index.ts index 44a8c4ab..4b118c14 100644 --- a/web/ui/src/lib/clustersMap/index.ts +++ b/web/ui/src/lib/clustersMap/index.ts @@ -1,41 +1,5 @@ -import { Helsinki } from './campus/helsinki'; -import { Lausanne } from './campus/lausanne'; -import { Madrid } from './campus/madrid'; -import { Malaga } from './campus/malaga'; -import { Mulhouse } from './campus/mulhouse'; -import { Paris } from './campus/paris'; -import { Seoul } from './campus/seoul'; -import { Tokyo } from './campus/tokyo'; -import { Vienna } from './campus/vienna'; -import { Wolfsburg } from './campus/wolfsburg'; -import { CampusNames, ICampus } from './types'; - -/** - * Campuses represents the list of campuses present in the application. - * Particulary, used in the cluster map. - * - * It is a const, so it can be accessed from anywhere in the application. - * You can add a new campus by adding it to the list of campuses. - * And define the campus in the `campus` folder (see `campus/paris.ts` for - * an example). Don't forget to add the campus to the `CampusNames` type in - * `types.d.ts`. - * - */ -export const Campuses: { - [key in CampusNames]: ICampus; -} = { - helsinki: new Helsinki(), - lausanne: new Lausanne(), - madrid: new Madrid(), - malaga: new Malaga(), - paris: new Paris(), - mulhouse: new Mulhouse(), - seoul: new Seoul(), - tokyo: new Tokyo(), - vienna: new Vienna(), - wolfsburg: new Wolfsburg(), -}; - export { countryEmoji } from './countryEmoji'; -export type { CampusNames, ClusterMapEntity } from './types'; -export default Campuses; +export { Campuses, Campuses as default } from './campuses.generated'; +export { findCampusPerSafeLink } from './utils'; +export type { CampusIdentifier } from './types.generated'; +export type { ClusterMapEntity, ICampus, ICluster } from './types'; diff --git a/web/ui/src/lib/clustersMap/types.d.ts b/web/ui/src/lib/clustersMap/types.d.ts index 6abd5587..b34e16cd 100644 --- a/web/ui/src/lib/clustersMap/types.d.ts +++ b/web/ui/src/lib/clustersMap/types.d.ts @@ -1,17 +1,4 @@ -/** - * List of all campus names present in the application (interface only) - */ -export type CampusNames = - | 'paris' - | 'helsinki' - | 'madrid' - | 'malaga' - | 'vienna' - | 'seoul' - | 'lausanne' - | 'tokyo' - | 'mulhouse' - | 'wolfsburg'; +import { CampusIdentifier } from './types.generated'; /** * Types of Cluster Map entities @@ -56,8 +43,12 @@ export interface ICluster { export interface ICampus { // Campus emoji (e.g. 🇫🇷) emoji(): string; - // Campus name (e.g. "paris") - name(): CampusNames; + // Campus name (e.g. "Paris") used as display on UI. + name(): string; + // identifier is the name of the campus used in api communication, links, etc. + // basically, it's the name of the campus without spaces and special + // characters (e.g. "saoPaulo"). + identifier(): CampusIdentifier; // extrator function will extract the cluster, row and workspace from a given // identifier for this campus. It will return an object with the data present // in the identifier. diff --git a/web/ui/src/lib/clustersMap/types.generated.d.ts b/web/ui/src/lib/clustersMap/types.generated.d.ts new file mode 100644 index 00000000..4e8f4e7c --- /dev/null +++ b/web/ui/src/lib/clustersMap/types.generated.d.ts @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE MANUALLY - IT IS GENERATED FROM THE CONTENT OF THE CAMPUS FOLDER +// RUN `yarn generate:campus` TO REGENERATE IT + +/** + * List of all campus names present in the interface as their identifier. + * Identifier must be in camelCase without spaces or special characters. It + * must be unique in the list. + */ +export type CampusIdentifier = + | 'helsinki' + | 'lausanne' + | 'madrid' + | 'malaga' + | 'mulhouse' + | 'paris' + | 'seoul' + | 'tokyo' + | 'vienna' + | 'wolfsburg'; diff --git a/web/ui/src/lib/clustersMap/utils.ts b/web/ui/src/lib/clustersMap/utils.ts new file mode 100644 index 00000000..2d9fe469 --- /dev/null +++ b/web/ui/src/lib/clustersMap/utils.ts @@ -0,0 +1,21 @@ +import type { ICampus } from './types'; +import { Campuses } from './campuses.generated'; +import '@lib/prototypes/string'; + +/** + * findCampusPerSafeLink will find a campus object from its link representation. + * For example, if you want to find the campus object for the campus "Paris", + * you can use the link "paris" to find it. + * @param slug - the link of the campus in the safe url format (e.g. "paris") + * @returns the campus object or undefined if not found + */ +export const findCampusPerSafeLink = ( + slug: string | undefined, +): ICampus | undefined => { + if (!slug) return undefined; + + return Object.values(Campuses).find( + (iCampusObject) => + iCampusObject.identifier().toSafeLink() === slug.toSafeLink(), + ); +}; diff --git a/web/ui/src/lib/prototypes/string.js b/web/ui/src/lib/prototypes/string.js index 72bfe64c..024bbeb1 100644 --- a/web/ui/src/lib/prototypes/string.js +++ b/web/ui/src/lib/prototypes/string.js @@ -18,4 +18,26 @@ String.prototype.toSentenceCase = function () { return this.charAt(0).toUpperCase() + this.slice(1); }; +// toCamelCase will return the string with the first letter of each word +// capitalized and all spaces removed (except the first letter of the first word) +String.prototype.toCamelCase = function () { + return this.toLowerCase().replace(/[-_\s]+(.)?/g, (_, c) => + c ? c.toUpperCase() : '', + ); +}; + +// removeAccents will return the string with all accents removed +// Example: 'héllö wórld' -> 'hello world' +String.prototype.removeAccents = function () { + return this.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); +}; + +// toSafeLink will return the string with all accents removed, all characters +// lowercased, and all non-alphanumeric characters replaced with a dash. +String.prototype.toSafeLink = function () { + return this.removeAccents() + .toLowerCase() + .replace(/[^a-z0-9]/g, '-'); +}; + module.exports = {}; diff --git a/web/ui/src/lib/searchEngine.ts b/web/ui/src/lib/searchEngine.ts index ac49e032..adfd38aa 100644 --- a/web/ui/src/lib/searchEngine.ts +++ b/web/ui/src/lib/searchEngine.ts @@ -1,4 +1,4 @@ -import Campuses, { CampusNames } from './clustersMap'; +import { findCampusPerSafeLink } from '@lib/clustersMap'; /** * retrieve the cluster url for the given campus and identifier @@ -7,27 +7,28 @@ export const clusterURL = ( campus: string | null | undefined, identifier: string | null | undefined, ): string | null => { - const campusLower = campus?.toLowerCase(); + const campusLink = campus?.toSafeLink(); - if (!campusLower || !Object.keys(Campuses).includes(campusLower)) { + const campusData = findCampusPerSafeLink(campusLink); + + if (!campusData) { return null; } - if (campusLower && identifier) { - const { clusterWithLetter } = - Campuses[campusLower as CampusNames].extractor(identifier); + if (campusData && identifier) { + const { clusterWithLetter } = campusData.extractor(identifier); //! FIXME : this is a hack to fix the fact that the clusterWithLetter is not //! always the same as the cluster name due to paul cluster merging //! (paul-F3A and paul-F3B are now paul-F3) and the fact that the cluster //! name is used in the url to retrieve the cluster data from the API - if (clusterWithLetter.startsWith('paul') && campusLower === 'paris') { + if (clusterWithLetter.startsWith('paul') && campusLink === 'paris') { const mergedIdentifier = clusterWithLetter.slice(0, -1); - return `/clusters/${campusLower}/${mergedIdentifier}?identifier=${identifier}`; + return `/clusters/${campusLink}/${mergedIdentifier}?identifier=${identifier}`; } if (clusterWithLetter) { - return `/clusters/${campusLower}/${clusterWithLetter}?identifier=${identifier}`; + return `/clusters/${campusLink}/${clusterWithLetter}?identifier=${identifier}`; } } return null; diff --git a/web/ui/src/pages/clusters/[campusName]/[cluster].tsx b/web/ui/src/pages/clusters/[campusSlug]/[clusterSlug].tsx similarity index 65% rename from web/ui/src/pages/clusters/[campusName]/[cluster].tsx rename to web/ui/src/pages/clusters/[campusSlug]/[clusterSlug].tsx index c99f3a3c..dbe47549 100644 --- a/web/ui/src/pages/clusters/[campusName]/[cluster].tsx +++ b/web/ui/src/pages/clusters/[campusSlug]/[clusterSlug].tsx @@ -1,5 +1,4 @@ import { - ClusterContainerProps, ClusterEmpty, ClusterPillar, ClusterRow, @@ -9,7 +8,11 @@ import { extractNode, } from '@components/ClusterMap'; import { ClusterContainer } from '@components/ClusterMap/ClusterContainer'; -import { CampusNames, Campuses } from '@lib/clustersMap'; +import { + CampusIdentifier, + Campuses, + findCampusPerSafeLink, +} from '@lib/clustersMap'; import '@lib/prototypes/string'; import { GetStaticPaths, @@ -20,28 +23,34 @@ import { import Error from 'next/error'; import Head from 'next/head'; -export const IndexPage: NextPage< - ClusterContainerProps & { campusName: CampusNames } -> = ({ campusName = 'vienna', cluster }) => { - const campusData = Campuses[campusName]; - const clusterData = campusData?.cluster(cluster); +type PageProps = { + campusIdentifier: CampusIdentifier; + clusterIdentifier: string; +}; + +export const IndexPage: NextPage = ({ + campusIdentifier, + clusterIdentifier, +}) => { + const campus = findCampusPerSafeLink(campusIdentifier); + const cluster = campus?.cluster(clusterIdentifier); - if (!campusData || !clusterData) { - return ; - } + if (!campus || !cluster) return ; return ( <> - {cluster.toUpperCase()} @ {campusData.name().toTitleCase()} - Clusters - - S42 + {cluster.hasName() + ? cluster.name() + : cluster.identifier().toUpperCase()}{' '} + @ {campus.name().toTitleCase()} - Clusters - S42 - + {({ locations, showPopup }) => ( - {clusterData.map().map((row, i) => ( + {cluster.map().map((row, i) => ( {row.map((entity, j) => { const key = `cluster-workspace-${i}-${entity}-${j}`; @@ -62,7 +71,7 @@ export const IndexPage: NextPage< { showPopup({ @@ -81,7 +90,7 @@ export const IndexPage: NextPage< identifier={identifier} displayText={ entity.startsWith('W:') - ? campusData.extractor(identifier).workspace + ? campus.extractor(identifier).workspace : undefined } kind={ @@ -104,12 +113,15 @@ export const IndexPage: NextPage< export const getStaticPaths: GetStaticPaths = async () => { const paths = [] as GetStaticPathsResult['paths']; - Object.keys(Campuses).map((campusName) => { - const campus = Campuses[campusName as CampusNames]; + Object.keys(Campuses).map((key) => { + const campus = Campuses[key as CampusIdentifier]; campus.clusters().map((cluster) => { paths.push({ - params: { campusName: campusName, cluster: cluster.identifier() }, + params: { + campusSlug: campus.identifier().toSafeLink(), + clusterSlug: cluster.identifier(), + }, }); }); }); @@ -120,13 +132,22 @@ export const getStaticPaths: GetStaticPaths = async () => { }; }; -export const getStaticProps: GetStaticProps = ({ params = {} }) => { - const { campusName, cluster } = params; +export const getStaticProps: GetStaticProps = ({ params = {} }) => { + const { campusSlug, clusterSlug } = params; + + const campus = findCampusPerSafeLink(campusSlug as string); + const cluster = campus?.cluster(clusterSlug as string); + + if (!campus || !cluster) { + return { + notFound: true, + }; + } return { props: { - campusName, - cluster, + campusIdentifier: campus.identifier(), + clusterIdentifier: cluster.identifier(), }, }; }; diff --git a/web/ui/src/pages/clusters/index.tsx b/web/ui/src/pages/clusters/index.tsx index 0fb8900d..cf183577 100644 --- a/web/ui/src/pages/clusters/index.tsx +++ b/web/ui/src/pages/clusters/index.tsx @@ -1,6 +1,6 @@ import { MeWithFlagsDocument, MeWithFlagsQuery } from '@graphql.d'; import { queryAuthenticatedSSR } from '@lib/apollo'; -import Campuses, { CampusNames } from '@lib/clustersMap'; +import Campuses from '@lib/clustersMap'; import { clusterURL } from '@lib/searchEngine'; import type { GetServerSideProps, NextPage } from 'next'; @@ -32,15 +32,17 @@ export const getServerSideProps: GetServerSideProps = async ({ req, { query: MeWithFlagsDocument }, ); - const myCampusNameLowerFromAPI = me?.currentCampus?.name?.toLowerCase() || ''; + const myCampusNameFromAPI = me?.currentCampus?.name?.toSafeLink() || ''; - if (Object.keys(Campuses).includes(myCampusNameLowerFromAPI)) { - const clusterKey = Campuses[myCampusNameLowerFromAPI as CampusNames] - .clusters()[0] - .identifier(); + const myCampus = Object.values(Campuses).find( + (v) => v.name().toSafeLink() === myCampusNameFromAPI, + ); + + if (!!myCampus) { + const clusterKey = myCampus.clusters()[0].identifier(); return { redirect: { - destination: `/clusters/${myCampusNameLowerFromAPI}/${clusterKey}`, + destination: `/clusters/${myCampusNameFromAPI}/${clusterKey}`, permanent: false, }, props: {}, diff --git a/web/ui/src/types/globals.d.ts b/web/ui/src/types/globals.d.ts index fbcacaad..852efab9 100644 --- a/web/ui/src/types/globals.d.ts +++ b/web/ui/src/types/globals.d.ts @@ -33,6 +33,9 @@ declare global { equalsIgnoreCase(searchString: string): boolean; toTitleCase(): string; toSentenceCase(): string; + toCamelCase(): string; + removeAccents(): string; + toSafeLink(): string; } interface WindowEventMap { diff --git a/web/ui/tsconfig.json b/web/ui/tsconfig.json index 08bb43ea..c57aa409 100644 --- a/web/ui/tsconfig.json +++ b/web/ui/tsconfig.json @@ -39,7 +39,8 @@ "**/*.tsx", "next.config.ts", "src/lib/prototypes/string.js", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + "generators/*.ts" ], "exclude": ["node_modules"] } diff --git a/web/ui/yarn.lock b/web/ui/yarn.lock index b0eac00d..28552fbf 100644 --- a/web/ui/yarn.lock +++ b/web/ui/yarn.lock @@ -102,6 +102,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" @@ -142,6 +150,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -206,6 +224,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -221,6 +244,14 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -228,6 +259,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" @@ -310,16 +348,33 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" @@ -353,11 +408,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.8", "@babel/parser@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1087,19 +1156,28 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.7": - version "7.20.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.10.tgz#2bf98239597fcec12f842756f186a9dde6d09230" - integrity sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg== +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.7": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -1112,6 +1190,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2045,6 +2132,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -2055,7 +2147,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/sourcemap-codec@^1.4.13": +"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -2076,6 +2168,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.18": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" @@ -3765,7 +3865,7 @@ chalk@3.0.0, chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -9602,4 +9702,4 @@ zen-observable@0.8.15: zod@3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== \ No newline at end of file