diff --git a/next.config.js b/next.config.js index 6fd6d467a..87d605db8 100644 --- a/next.config.js +++ b/next.config.js @@ -7,7 +7,7 @@ const orgId = process.env.NEXT_PUBLIC_PLANNER_ORG_ID; */ const nextConfig = { output: 'standalone', - transpilePackages: ['@atb-as/mapbox-shared'], + transpilePackages: ['@atb-as/mapbox-shared', '@mrfylke/contact-form'], async headers() { return [ { diff --git a/package.json b/package.json index 67954d79e..727799d23 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@isaacs/ttlcache": "^1.4.1", "@leile/lobo-t": "^1.0.5", "@mapbox/polyline": "^1.2.1", + "@mrfylke/contact-form": "^0.1.24", "@react-aria/focus": "^3.21.0", "@react-hook/resize-observer": "^2.0.2", "@resvg/resvg-js": "^2.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16d8cf42d..6350ece66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: '@mapbox/polyline': specifier: ^1.2.1 version: 1.2.1 + '@mrfylke/contact-form': + specifier: ^0.1.24 + version: 0.1.24(@atb-as/generate-assets@18.16.0)(@atb-as/theme@15.2.0)(@atb-as/token@0.0.4(@atb-as/theme@15.2.0)(postcss@8.5.15))(@emotion/is-prop-valid@1.4.0)(@internationalized/date@3.12.2)(@types/react@19.1.10)(graphql-ws@6.0.8(graphql@16.14.2)(ws@8.21.0))(lodash@4.18.1)(next@15.4.10(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(react-aria-components@1.18.0(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(swr@2.4.1(react@19.1.4))(uuid@11.1.1)(zod@4.1.11) '@react-aria/focus': specifier: ^3.21.0 version: 3.22.1(react-dom@19.1.4(react@19.1.4))(react@19.1.4) @@ -109,7 +112,7 @@ importers: version: 12.14.0 framer-motion: specifier: ^12.23.12 - version: 12.40.0(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + version: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) graphql: specifier: ^16.11.0 version: 16.14.2 @@ -341,6 +344,12 @@ packages: peerDependencies: zod: ^4.1.11 + '@atb-as/config-specs@9.6.1': + resolution: {integrity: sha512-QFoqCNbbK3Dqz7UwOSL2e6cBWNT9tpUhndLQbmVN6CJY0zbZumxFbsVPHTZnBEsd/g/f1dGbeQ1A+8JQEC9FNA==} + hasBin: true + peerDependencies: + zod: ^4.1.11 + '@atb-as/generate-assets@18.16.0': resolution: {integrity: sha512-0+IqmDI2Ya8PYHyV2fl04BhkjQ4Bq5dUY0g6xLDnVx9rpubeYqO8Rl1Ebfl0+btgyQIujmc2GbDEJ/0lHzovmA==} hasBin: true @@ -1316,6 +1325,12 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + '@envelop/core@5.5.1': resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} engines: {node: '>=18.0.0'} @@ -2332,6 +2347,21 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@mrfylke/contact-form@0.1.24': + resolution: {integrity: sha512-2KmFbcroyfB14HtZ+rO75zg2auo4oHDSvI3q0vpPZONqLM8iLR4mMgJRn/xxnxkvlmMJSfVHbQp7lu/I722JeQ==} + peerDependencies: + '@atb-as/generate-assets': ^18.7.0 + '@atb-as/theme': ^15.0.0 + '@atb-as/token': ^0.0.4 + '@internationalized/date': ^3.11.0 + lodash: ^4.6.2 + next: '>=14.0.0' + react: ^18.0.0 + react-aria-components: ^1.15.1 + react-dom: ^18.0.0 + swr: ^2.2.5 + uuid: ^13.0.0 + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -4255,6 +4285,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -4487,6 +4518,11 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5054,6 +5090,9 @@ packages: lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} @@ -5134,6 +5173,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@8.0.7: + resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -5145,6 +5188,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -5430,6 +5477,10 @@ packages: resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} engines: {node: '>=0.10.0'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -6804,6 +6855,15 @@ snapshots: yargs: 17.7.2 zod: 4.1.11 + '@atb-as/config-specs@9.6.1(zod@4.1.11)': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + glob: 9.3.5 + js-yaml: 4.2.0 + yargs: 17.7.2 + zod: 4.1.11 + '@atb-as/generate-assets@18.16.0': dependencies: '@atb-as/theme': 14.0.1 @@ -8012,6 +8072,14 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + optional: true + + '@emotion/memoize@0.9.0': + optional: true + '@envelop/core@5.5.1': dependencies: '@envelop/instrumentation': 1.0.0 @@ -9309,6 +9377,35 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@mrfylke/contact-form@0.1.24(@atb-as/generate-assets@18.16.0)(@atb-as/theme@15.2.0)(@atb-as/token@0.0.4(@atb-as/theme@15.2.0)(postcss@8.5.15))(@emotion/is-prop-valid@1.4.0)(@internationalized/date@3.12.2)(@types/react@19.1.10)(graphql-ws@6.0.8(graphql@16.14.2)(ws@8.21.0))(lodash@4.18.1)(next@15.4.10(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(react-aria-components@1.18.0(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(swr@2.4.1(react@19.1.4))(uuid@11.1.1)(zod@4.1.11)': + dependencies: + '@apollo/client': 3.14.1(@types/react@19.1.10)(graphql-ws@6.0.8(graphql@16.14.2)(ws@8.21.0))(graphql@16.14.2)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + '@atb-as/config-specs': 9.6.1(zod@4.1.11) + '@atb-as/generate-assets': 18.16.0 + '@atb-as/theme': 15.2.0 + '@atb-as/token': 0.0.4(@atb-as/theme@15.2.0)(postcss@8.5.15) + '@internationalized/date': 3.12.2 + '@leile/lobo-t': 1.0.5(react@19.1.4) + '@xstate/react': 6.1.0(@types/react@19.1.10)(react@19.1.4)(xstate@5.32.0) + detect-nearest-browser-locale: 19.0.0 + detect-nearest-locale: 1.0.1 + framer-motion: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + graphql: 16.14.2 + lodash: 4.18.1 + next: 15.4.10(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + react: 19.1.4 + react-aria-components: 1.18.0(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + react-dom: 19.1.4(react@19.1.4) + swr: 2.4.1(react@19.1.4) + uuid: 11.1.1 + xstate: 5.32.0 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - graphql-ws + - subscriptions-transport-ws + - zod + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -11527,12 +11624,13 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.40.0(react-dom@19.1.4(react@19.1.4))(react@19.1.4): + framer-motion@12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.4(react@19.1.4))(react@19.1.4): dependencies: motion-dom: 12.40.0 motion-utils: 12.39.0 tslib: 2.8.1 optionalDependencies: + '@emotion/is-prop-valid': 1.4.0 react: 19.1.4 react-dom: 19.1.4(react@19.1.4) @@ -11629,6 +11727,13 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.7 + minipass: 4.2.8 + path-scurry: 1.11.1 + globals@14.0.0: {} globalthis@1.0.4: @@ -12187,6 +12292,8 @@ snapshots: fault: 1.0.4 highlight.js: 10.7.3 + lru-cache@10.4.3: {} + lru-cache@11.5.1: {} lru-cache@5.1.1: @@ -12257,6 +12364,10 @@ snapshots: dependencies: brace-expansion: 1.1.15 + minimatch@8.0.7: + dependencies: + brace-expansion: 2.1.1 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.1 @@ -12269,6 +12380,8 @@ snapshots: minimist@1.2.8: {} + minipass@4.2.8: {} + minipass@7.1.3: {} mkdirp@0.5.6: @@ -12587,6 +12700,11 @@ snapshots: dependencies: path-root-regex: 0.1.2 + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.5.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e35fa4ad0..5bd6c1695 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,3 +8,4 @@ allowBuilds: unrs-resolver: true minimumReleaseAgeExclude: - "@atb-as/*" + - '@mrfylke/contact-form' diff --git a/src/components/contact-form/index.ts b/src/components/contact-form/index.ts new file mode 100644 index 000000000..4c122dfa7 --- /dev/null +++ b/src/components/contact-form/index.ts @@ -0,0 +1 @@ +export { ContactFormWrapper } from './wrapper'; diff --git a/src/components/contact-form/wrapper.tsx b/src/components/contact-form/wrapper.tsx new file mode 100644 index 000000000..8ffd2cf15 --- /dev/null +++ b/src/components/contact-form/wrapper.tsx @@ -0,0 +1,34 @@ +import { + ContactFormRoot, + Language as ContactFormLanguage, +} from '@mrfylke/contact-form'; +import { contactFormConfig } from '@atb/config/contact-form-config'; +import { Language, useLanguageSettings } from '@atb/translations'; +import { useDarkMode } from '@atb/modules/theme'; +import { PropsWithChildren } from 'react'; + +export function ContactFormWrapper({ children }: PropsWithChildren) { + const { language } = useLanguageSettings(); + const [isDarkMode] = useDarkMode(); + + return ( + + {children} + + ); +} + +function toContactFormLanguage(language: Language): ContactFormLanguage { + switch (language) { + case Language.English: + return ContactFormLanguage.English; + case Language.Norwegian: + return ContactFormLanguage.Norwegian; + case Language.Nynorsk: + return ContactFormLanguage.Nynorsk; + } +} diff --git a/src/config/contact-form-config.tsx b/src/config/contact-form-config.tsx new file mode 100644 index 000000000..12b1fe22d --- /dev/null +++ b/src/config/contact-form-config.tsx @@ -0,0 +1,172 @@ +import type { ContactFormConfig, IconComponent } from '@mrfylke/contact-form'; +import type { FormSchemaName } from '@mrfylke/contact-form'; +import { useTheme, buildEnabledPageIds } from '@mrfylke/contact-form'; +import { adaptAtbTheme } from '@mrfylke/contact-form/config'; +import { theme } from '@atb/modules/theme'; +import { getOrgData, byOrg } from '@atb/modules/org-data'; +import { + PageText, + ComponentText, + CommonText, + ServerText, +} from '@atb/translations'; + +function createIconComponent(iconName: string): IconComponent { + const Icon = ({ + size = 'normal', + className, + style, + alt = '', + }: { + size?: 'small' | 'normal' | 'large'; + className?: string; + style?: React.CSSProperties; + alt?: string; + }) => { + const { isDarkMode } = useTheme(); + const sizeMap = { small: 20, normal: 24, large: 32 }; + const px = sizeMap[size]; + const mode = isDarkMode ? 'dark' : 'light'; + const assetPath = `/assets/mono/${mode}/${iconName}.svg`; + return ( + {alt} + ); + }; + Icon.displayName = `Icon(${iconName})`; + return Icon; +} + +const iconSet: ContactFormConfig['icons'] = { + 'transportation/BusFill': createIconComponent('transportation/BusFill'), + 'transportation/TramFill': createIconComponent('transportation/TramFill'), + 'transportation/TrainFill': createIconComponent('transportation/TrainFill'), + 'transportation/WalkFill': createIconComponent('transportation/WalkFill'), + 'transportation/BicycleFill': createIconComponent( + 'transportation/BicycleFill', + ), + 'transportation/PlaneFill': createIconComponent('transportation/PlaneFill'), + 'transportation/BoatFill': createIconComponent('transportation/BoatFill'), + 'transportation/FerryFill': createIconComponent('transportation/FerryFill'), + 'transportation/MetroFill': createIconComponent('transportation/MetroFill'), + 'transportation/UnknownFill': createIconComponent( + 'transportation/UnknownFill', + ), + 'actions/Close': createIconComponent('actions/Close'), + 'actions/Clear': createIconComponent('actions/Clear'), + 'actions/Add': createIconComponent('actions/Add'), + 'status/CheckmarkFill': createIconComponent('status/CheckmarkFill'), + 'status/ErrorFill': createIconComponent('status/ErrorFill'), +}; + +const atbThemes = theme; +const contactFormTheme = adaptAtbTheme(atbThemes); + +const baseUrl = typeof window !== 'undefined' ? '' : ''; +const submitEndpoint = '/api/contact/submit'; + +const onSubmit = async ( + schemaName: FormSchemaName, + state: Record, +): Promise => { + const response = await fetch(`${baseUrl}${submitEndpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...state, formType: schemaName }), + }); + return response; +}; + +function getContactFormConfig(): ContactFormConfig { + const org = getOrgData(); + const logo = + org.fylkeskommune != null + ? { + src: org.fylkeskommune.logoSrc, + srcDark: org.fylkeskommune.logoSrcDark, + alt: org.fylkeskommune.name, + } + : undefined; + + return { + theme: contactFormTheme, + icons: iconSet, + api: { + baseUrl, + endpoints: { + submitForm: submitEndpoint, + lines: '/api/contact/lines', + }, + onSubmit, + }, + org: { + name: org.fylkeskommune?.name ?? org.orgId, + supportEmail: org.supportEmail, + authorityId: org.authorityId, + logo, + }, + features: { + enableFileUploads: true, + }, + layout: { + basePath: '/contact', + backLinkDefault: { + href: '/', + label: PageText.Contact.contactPageLayout.homeLink as unknown as Record< + string, + string + >, + }, + successPath: '/contact/success', + errorPath: '/contact/error', + }, + pages: { + enabledPageIds: + byOrg({ + fram: buildEnabledPageIds([ + 'ticket-control', + 'refund', + 'means-of-transport', + 'ticketing', + 'lost-property-external', + 'group-travel', + 'journey-info', + ]), + }) ?? + buildEnabledPageIds([ + 'ticket-control', + 'refund', + 'means-of-transport', + 'ticketing', + 'lost-property', + 'group-travel', + 'journey-info', + ]), + }, + formSchemaOverrides: byOrg({ + fram: { + refund: { + enabledFormCategories: [ + 'refundOfTicket', + 'refundAndTravelGuarantee', + 'residualValueOnTravelCard', + ], + }, + }, + }), + translations: { + pages: PageText, + components: ComponentText, + common: CommonText, + server: ServerText, + }, + }; +} + +export const contactFormConfig = getContactFormConfig(); diff --git a/src/layouts/shared/footer.tsx b/src/layouts/shared/footer.tsx index 6792d4623..33a072bcb 100644 --- a/src/layouts/shared/footer.tsx +++ b/src/layouts/shared/footer.tsx @@ -8,7 +8,7 @@ import { useLanguageSettings } from '@atb/translations/language-context'; import style from './footer.module.css'; import { getButtonStyleForColor } from '@atb/components/button/utils.tsx'; import { useOrgThemeDefinitions } from '@atb/utils/org-theme-definitions.ts'; -import { shouldShowContactPage } from '@atb/page-modules/contact'; +import { shouldShowContactPage } from '@atb/server/contact/utils'; export type FooterProps = { withoutSettings?: boolean; diff --git a/src/modules/org-data/index.tsx b/src/modules/org-data/index.tsx index 96e439ac5..f67afc6bc 100644 --- a/src/modules/org-data/index.tsx +++ b/src/modules/org-data/index.tsx @@ -98,6 +98,12 @@ export function getConfigUrl(url: TranslatableUrl, lang: Language) { return url[lang] ?? url.default; } +export function byOrg( + values: Partial>, +): T | undefined { + return values[currentOrg]; +} + function getCurrentOrg(): WEBSHOP_ORGS { const orgId = process.env.NEXT_PUBLIC_PLANNER_ORG_ID; switch (orgId) { diff --git a/src/page-modules/contact/commoneEvents.ts b/src/page-modules/contact/commoneEvents.ts deleted file mode 100644 index 54f066e31..000000000 --- a/src/page-modules/contact/commoneEvents.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { TransportModeType } from './types'; -import { Line } from '.'; - -export const commonEvents = {} as - | { type: 'VALIDATE' } - | { - type: 'ON_INPUT_CHANGE'; - inputName: - | 'address' - | 'attachments' - | 'bankAccountNumber' - | 'city' - | 'date' - | 'email' - | 'firstName' - | 'feedback' - | 'IBAN' - | 'lastName' - | 'phoneNumber' - | 'postalCode' - | 'SWIFT' - | 'toStop' - | 'fromStop' - | 'plannedDepartureTime'; - value: any; - } - | { - type: 'ON_TRANSPORTMODE_CHANGE'; - value: TransportModeType; - } - | { - type: 'ON_LINE_CHANGE'; - value: Line | undefined; - }; diff --git a/src/page-modules/contact/components/form/date-selector.tsx b/src/page-modules/contact/components/form/date-selector.tsx deleted file mode 100644 index 0617a5873..000000000 --- a/src/page-modules/contact/components/form/date-selector.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { ErrorMessage } from '@atb/components/error-message'; -import style from './form.module.css'; -import { MonoIcon } from '@atb/components/icon'; -import { TranslatedString, useTranslation } from '@atb/translations'; -import { fromDate } from '@internationalized/date'; -import { - Button, - Calendar, - CalendarCell, - CalendarGrid, - DateInput, - DatePicker, - DateSegment, - Dialog, - Group, - Heading, - Popover, -} from 'react-aria-components'; -import FormComponentLabel from './form-component-label'; - -export type DateSelectorProps = { - id: string; - label: string; - value?: string; - isRequired?: boolean; - errorMessage?: TranslatedString; - onChange: (value: string) => void; -}; - -export default function DateSelector({ - id, - label, - value, - isRequired, - errorMessage, - onChange, -}: DateSelectorProps) { - const { t } = useTranslation(); - const zonedDateTime = value - ? fromDate(new Date(value || ''), 'Europe/Oslo') - : undefined; - - return ( -
- e && onChange(e.toString().slice(0, 10))} - className={style.dateSelector} - shouldForceLeadingZeros - aria-label={label} - aria-labelledby={`label-${id}`} - > - - - - - {(segment) => ( - - )} - - - - - - -
- - - -
- - {(date) => ( - - )} - -
-
-
-
- - {errorMessage && } -
- ); -} diff --git a/src/page-modules/contact/components/form/description-modal.tsx b/src/page-modules/contact/components/form/description-modal.tsx deleted file mode 100644 index 1f3593204..000000000 --- a/src/page-modules/contact/components/form/description-modal.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Typo } from '@atb/components/typography'; -import style from './form.module.css'; -import { motion } from 'framer-motion'; -import { FocusScope } from '@react-aria/focus'; -import { MonoIcon } from '@atb/components/icon'; -import { useCallback, useEffect, useRef } from 'react'; -import { Button } from '@atb/components/button'; -import { PageText, useTranslation } from '@atb/translations'; - -type DescriptionModalProps = { - title: string; - modalContent: { - description?: string; - instruction?: string; - bulletPoints?: string[]; - }; - isModalOpen: boolean; - closeModal: () => void; -}; - -const DescriptionModal = ({ - title, - modalContent, - isModalOpen, - closeModal, -}: DescriptionModalProps) => { - const { t } = useTranslation(); - const modalRef = useRef(null); - const { description, instruction, bulletPoints } = modalContent; - - const handleEscapeOrClickOutside = useCallback( - (event: KeyboardEvent | MouseEvent) => { - if (event instanceof KeyboardEvent && event.key === 'Escape') { - closeModal(); - } else if ( - event instanceof MouseEvent && - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { - closeModal(); - } - }, - [closeModal], - ); - - useEffect(() => { - document.addEventListener('keydown', handleEscapeOrClickOutside); - document.addEventListener('mousedown', handleEscapeOrClickOutside); - - return () => { - document.removeEventListener('keydown', handleEscapeOrClickOutside); - document.removeEventListener('mousedown', handleEscapeOrClickOutside); - }; - }, [handleEscapeOrClickOutside]); - - if (!isModalOpen) return null; - - return ( - - - - -
- - {title} - -
- - {description && {description}} - - {bulletPoints && ( -
    - {bulletPoints.map((desc, index) => ( -
  • - - {desc} - -
  • - ))} -
- )} - - {instruction && ( - {instruction} - )} -
-
-
-
- ); -}; - -export default DescriptionModal; diff --git a/src/page-modules/contact/components/form/field-wrapper-with-error/index.tsx b/src/page-modules/contact/components/form/field-wrapper-with-error/index.tsx deleted file mode 100644 index 0336e2ddd..000000000 --- a/src/page-modules/contact/components/form/field-wrapper-with-error/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { TranslatedString, useTranslation } from '@atb/translations'; -import { PropsWithChildren } from 'react'; -import { ErrorMessage } from '@atb/components/error-message'; - -export type FieldWrapperWithErrorProps = PropsWithChildren<{ - errorMessage?: TranslatedString; -}>; - -export const FieldWrapperWithError = ({ - errorMessage, - children, -}: FieldWrapperWithErrorProps) => { - const { t } = useTranslation(); - - return ( -
- {children} - {errorMessage && } -
- ); -}; - -export default FieldWrapperWithError; diff --git a/src/page-modules/contact/components/form/fieldset/fieldset.module.css b/src/page-modules/contact/components/form/fieldset/fieldset.module.css deleted file mode 100644 index 588d2e82f..000000000 --- a/src/page-modules/contact/components/form/fieldset/fieldset.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.fieldset { - position: relative; - display: flex; - flex-direction: column; - padding: token('spacing.large'); - border: none; - border-radius: token('border.radius.regular'); - color: token('color.background.neutral.0.foreground.primary'); - background-color: token('color.background.neutral.0.background'); - gap: token('spacing.xSmall'); -} - -.fieldset > * + * { - margin-top: token('spacing.xLarge'); -} - -.legend { - position: absolute; - top: token('spacing.medium'); -} - -.required::after { - content: '*'; - color: token('color.status.error.primary.background'); - margin-left: token('spacing.small'); -} diff --git a/src/page-modules/contact/components/form/fieldset/index.tsx b/src/page-modules/contact/components/form/fieldset/index.tsx deleted file mode 100644 index c896d916a..000000000 --- a/src/page-modules/contact/components/form/fieldset/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import style from './fieldset.module.css'; -import { PropsWithChildren } from 'react'; -import { Typo } from '@atb/components/typography'; -import { andIf } from '@atb/utils/css'; - -export type FieldsetProps = PropsWithChildren<{ - title?: string; - isRequired?: boolean; -}>; - -export const Fieldset = ({ - title, - isRequired = false, - children, -}: FieldsetProps) => { - return ( -
- {title && ( - - - {title} - - - )} - {children} -
- ); -}; - -export default Fieldset; diff --git a/src/page-modules/contact/components/form/file-input.tsx b/src/page-modules/contact/components/form/file-input.tsx deleted file mode 100644 index 4f198e753..000000000 --- a/src/page-modules/contact/components/form/file-input.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { ChangeEvent, DragEvent, useState } from 'react'; -import style from './form.module.css'; -import { Typo } from '@atb/components/typography'; -import { Button } from '@atb/components/button'; -import { MonoIcon } from '@atb/components/icon'; -import { PageText, useTranslation } from '@atb/translations'; -import { useTheme } from '@atb/modules/theme'; -import { ErrorMessage } from '@atb/components/error-message'; -import FormComponentLabel from './form-component-label'; - -export type FileInputProps = { - id: string; - iconLabel: string; - label?: string; - isRequired?: boolean; - errorMessage?: string; - onChange?: (files: File[]) => void; -} & Omit; - -const MAX_ALLOWED_FILE_SIZE = 10 * 1024 * 1024; // 10MB - -export default function FileInput({ - id, - onChange, - iconLabel, - label, - isRequired = false, - name, - errorMessage, -}: FileInputProps) { - const { t } = useTranslation(); - const { - color: { background }, - } = useTheme(); - const [files, setFiles] = useState([]); - - const handleFileChange = (event: ChangeEvent) => { - if (event.target.files) { - const newFilesArray = Array.from(event.target.files); - handleFileUpload(newFilesArray); - } - }; - - const handleRemoveFile = (indexToRemove: number) => { - const updatedFiles = files.filter((_, index) => index !== indexToRemove); - setFiles(updatedFiles); - if (onChange) { - onChange(updatedFiles); - } - }; - - const handleDragOver = (event: DragEvent) => { - event.preventDefault(); - }; - - const handleDrop = (event: DragEvent) => { - event.preventDefault(); - - if (event.dataTransfer.files) { - const droppedFiles = Array.from(event.dataTransfer.files); - handleFileUpload(droppedFiles); - } - }; - - const handleFileUpload = (uploadedFiles: File[]) => { - const filteredFiles = uploadedFiles.filter((file) => { - const isTooLarge = file.size > MAX_ALLOWED_FILE_SIZE; - if (isTooLarge) { - alert( - t( - PageText.Contact.components.fileinput.errorMessages.tooLarge( - file.name, - ), - ), - ); - } - return !isTooLarge; - }); - - setFiles((prevFiles) => [...prevFiles, ...filteredFiles]); - if (onChange) { - onChange([...files, ...filteredFiles]); - } - }; - - return ( -
- - {label && ( - - )} - - - {files.length > 0 && ( -
- {files.map((file, index) => ( -
-
- ))} -
- )} - {errorMessage && } -
- ); -} - -function FileIcon({ color }: { color: string }) { - return ( - - - - ); -} diff --git a/src/page-modules/contact/components/form/form-component-label/form-component-label.module.css b/src/page-modules/contact/components/form/form-component-label/form-component-label.module.css deleted file mode 100644 index 3334ddb12..000000000 --- a/src/page-modules/contact/components/form/form-component-label/form-component-label.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.required::after { - content: '*'; - color: token('color.status.error.primary.background'); - margin-left: token('spacing.small'); -} - -.disabled, -.disabled::after { - color: token('color.foreground.dynamic.disabled'); -} diff --git a/src/page-modules/contact/components/form/form-component-label/index.tsx b/src/page-modules/contact/components/form/form-component-label/index.tsx deleted file mode 100644 index 9fe122424..000000000 --- a/src/page-modules/contact/components/form/form-component-label/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Typo } from '@atb/components/typography'; -import style from './form-component-label.module.css'; -import { andIf } from '@atb/utils/css'; - -type FormComponentLabelProps = { - label: string; - htmlFor: string; - disabled?: boolean; - isRequired?: boolean; -}; - -export const FormComponentLabel = ({ - label, - htmlFor, - disabled = false, - isRequired = false, -}: FormComponentLabelProps) => { - return ( - - ); -}; - -export default FormComponentLabel; diff --git a/src/page-modules/contact/components/form/form.module.css b/src/page-modules/contact/components/form/form.module.css deleted file mode 100644 index 1709c571a..000000000 --- a/src/page-modules/contact/components/form/form.module.css +++ /dev/null @@ -1,582 +0,0 @@ -@value typo-body__s from "@atb/theme/typography.module.css"; -@value typo-body__m from "@atb/theme/typography.module.css"; - -.container { - composes: typo-body__s; - display: flex; - flex-direction: column; - flex-wrap: wrap; - gap: token('spacing.small'); - color: token('color.background.neutral.0.foreground.primary'); - background-color: token('color.background.neutral.0.background'); -} - -.rowDisplay { - flex-direction: row-reverse; - justify-content: start; - gap: token('spacing.small'); -} - -.input { - background: transparent; - color: token('color.foreground.dynamic.primary'); - - padding: token('spacing.small'); - - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); - border-radius: token('border.radius.small'); - width: 100%; -} - -.input:focus { - outline: 0; -} - -.input::placeholder { - color: token('color.foreground.dynamic.secondary'); - opacity: 1; -} - -.input:disabled { - cursor: not-allowed; -} - -.input:disabled::placeholder { - color: token('color.foreground.dynamic.disabled'); -} - -.input:focus-within { - outline: 0; - box-shadow: inset 0 0 0 token('border.width.slim') - token('color.interactive.2.outline.background'); -} - -.input__error { - border-color: token('color.status.error.primary.background'); -} -/* radio */ -.input__radio { - clip: rect(0 0 0 0); - clip-path: inset(100%); - height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; - width: 1px; - display: block; -} -.input__radio:focus + .label__radio .label__radioBox { - outline: 2px solid token('color.interactive.2.outline.background'); - outline-offset: 2px; -} - -.label__radioBox:hover { - background-color: token('color.interactive.2.hover.background'); - color: token('color.interactive.2.hover.foreground.primary'); -} - -.label__radioBox { - display: block; - border-radius: 50%; - height: 1.25rem; - width: 1.25rem; - --padding: 2px; - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); - padding: var(--padding); - margin-right: token('spacing.medium'); -} -.label__radioBox::after { - content: ''; - display: block; - height: calc(1rem - 2 * token('border.width.slim')); - width: calc(1rem - 2 * token('border.width.slim')); - border-radius: 50%; - background: token('color.foreground.dynamic.primary'); - - transition: transform 100ms ease-in; - transform: scale(0); -} -.input__radio:checked + .label__radio .label__radioBox::after { - transform: scale(1); -} -.label__radio { - display: flex; - cursor: pointer; - - background: token('color.background.neutral.0.background'); - color: token('color.background.neutral.0.foreground.primary'); - - border-radius: token('border.radius.regular'); - - border: token('border.width.slim') solid transparent; - - padding: token('spacing.small') 0; -} - -.label__info { - display: flex; - align-items: center; - gap: token('spacing.medium'); - margin: token('spacing.small'); - margin-left: token('spacing.large'); - padding: token('spacing.small'); - background: token('color.background.neutral.1.background'); -} - -.label_container { - display: flex; - justify-content: space-between; -} - -/* Icon button */ -.iconButton { - background: transparent; - padding: 0; - cursor: pointer; - display: flex; - align-items: center; - border: none; - border-radius: 50%; -} - -.iconButton:focus { - outline: 2px solid token('color.interactive.2.outline.background'); - outline-offset: 2px; -} - -.iconButton:hover { - background-color: token('color.interactive.2.hover.background'); - color: token('color.interactive.2.hover.foreground.primary'); -} - -/* textare */ -.textarea_container { - display: flex; - flex-direction: column; - gap: token('spacing.small'); -} - -.textarea { - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); - border-radius: token('border.radius.small'); - padding: token('spacing.small'); - width: 100%; - min-height: 6rem; - font-family: var(--font-main) !important; - font-size: 1rem !important; - line-height: var(--baseTypo-body__m-lineHeight, 1.25rem) !important; - letter-spacing: var(--baseTypo-body__m-letterSpacing, 0.03125rem) !important; -} - -/** file input */ -.file_input__container { - display: flex; - flex-direction: column; - gap: token('spacing.small'); -} - -.input__file { - opacity: 0; - display: none; - width: 100%; - height: 100%; - cursor: pointer; -} - -.label__file { - display: flex; - gap: token('spacing.small'); - align-items: center; - background-color: token('color.background.neutral.1.background'); - color: token('color.foreground.dynamic.primary'); - padding: token('spacing.small'); - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); - border-radius: token('border.radius.small'); - cursor: pointer; -} - -.label__file:focus { - outline: 0; - box-shadow: inset 0 0 0 token('border.width.slim') - token('color.interactive.2.outline.background'); -} - -.label__file:hover { - background-color: token('color.interactive.2.hover.background'); - color: token('color.interactive.2.hover.foreground.primary'); -} - -.fileList { - display: flex; - flex-direction: column; - gap: token('spacing.small'); - padding: token('spacing.small') 0; -} -.fileItem { - display: flex; - align-items: center; - gap: token('spacing.small'); -} - -/* modal */ -.modal_wrapper { - position: fixed; - z-index: 200; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.modal_content { - position: relative; - display: flex; - flex-direction: column; - max-width: 32rem; - padding: token('spacing.large'); - border: token('border.width.medium') solid - token('color.interactive.2.outline.background'); /* Use border property */ - box-shadow: inset 0 0 0 token('border.width.medium') - token('color.interactive.2.outline.background'); - color: token('color.background.neutral.0.foreground.primary'); - background-color: token('color.background.neutral.0.background'); - border-radius: token('border.radius.regular'); -} - -.modal_content > * + * { - margin-top: token('spacing.small'); -} - -.modal_header { - display: flex; - justify-content: space-between; -} - -.modal__rules_list { - padding: token('spacing.medium'); - padding-left: token('spacing.xLarge'); -} - -.modal__rules_list > li { - padding: token('spacing.xSmall') 0; -} - -/* select and searchable-select*/ -.select__select_container, -.searchable_select__comboBox { - display: flex; - flex-direction: column; - gap: token('spacing.small'); -} - -.searchable_select__input_container { - position: relative; - display: flex; - align-items: center; -} - -.searchable_select__group, -.searchable_select__disabled { - display: flex; - flex-direction: row; - align-items: center; - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); - border-radius: token('border.radius.small'); -} - -.searchable_select__group:focus-within { - outline: 0; - box-shadow: inset 0 0 0 token('border.width.slim') - token('color.interactive.2.outline.background'); -} - -.searchable_select__input { - flex-grow: 2; - border: none; - outline: none; - padding: token('spacing.small'); - background-color: transparent; - color: token('color.background.neutral.2.foreground.primary'); -} - -.searchable_select__input_disabled { - cursor: not-allowed; -} - -.searchable_select__input::placeholder { - color: token('color.foreground.dynamic.secondary'); - opacity: 1; -} - -.searchable_select__input:not(:focus):placeholder-shown { - color: token('color.foreground.dynamic.secondary'); - font-style: italic; - opacity: 0.7; -} - -.searchable_select__button { - display: flex; - border: none; - padding: token('spacing.small'); - background-color: transparent; -} - -.searchable_select__button:disabled { - cursor: not-allowed; -} - -.select__popover, -.searchable_select__popover { - overflow: auto; - max-height: 16rem; - width: var(--trigger-width); - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); - border-radius: token('border.radius.small'); - color: token('color.background.neutral.0.foreground.primary'); - background: token('color.background.neutral.0.background'); - animation: fadeIn 0.2s; - padding: token('spacing.small'); - margin-bottom: token('spacing.small'); -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.select__listBox, -.searchable_select__listBox { - max-height: 16rem; - overflow-y: auto; - outline: none; -} - -.searchable_select__truncate { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - -.select__listBoxItem, -.searchable_select__listBoxItem { - display: flex; - align-items: center; - cursor: default; - color: token('color.background.neutral.0.foreground.primary'); - border-radius: token('border.radius.small'); - border-bottom: 1px solid token('color.background.neutral.2.background'); - z-index: 1; -} - -.select__item, -.searchable_select__item { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - gap: token('spacing.small'); - padding: token('spacing.small'); -} - -.select__item:active, -.searchable_select__item:active { - background-color: token('color.interactive.3.active.background'); - color: token('color.interactive.3.active.foreground.primary'); - z-index: 2; -} - -.select__highlight, -.searchable_select__highlight { - position: relative; - border-radius: token('border.radius.small'); - background-color: token('color.interactive.2.active.background'); - color: token('color.interactive.2.active.foreground.primary'); - z-index: 2; -} - -.searchable_select__selected { - position: relative; - border-radius: token('border.radius.small'); - background-color: token('color.interactive.2.active.background'); - color: token('color.interactive.2.active.foreground.primary'); - z-index: 2; -} - -.select__button { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - padding: token('spacing.small'); - color: token('color.foreground.dynamic.primary'); - background-color: transparent; - border-radius: token('border.radius.small'); - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); -} - -.select__button:disabled { - cursor: not-allowed; -} - -.select__button:focus-within { - outline: 0; - box-shadow: inset 0 0 0 token('border.width.slim') - token('color.interactive.2.outline.background'); -} - -.select__item:focus, -.select__listBoxItem:focus { - outline: 0; - border-radius: token('border.radius.small'); -} - -.select__placholder { - font-style: italic; - opacity: 0.7; - padding-right: token('spacing.small'); -} - -/* Time and date selector */ -.dateSelectorContainer, -.timeSelectorContainer { - display: flex; - flex-direction: column; - gap: token('spacing.small'); -} - -.dateSelector, -.timeSelector { - display: contents; - --height: 2.75rem; - overflow: hidden; - border-radius: token('border.radius.small'); -} - -.dateSelector input[type='date'], -.timeSelector input[type='time'] { - height: var(--height); - padding: token('spacing.medium'); - border: none; - background-color: transparent; - color: token('color.background.neutral.0.foreground.primary'); - flex: 1; -} - -.dateSelectorInput, -.timeSelectorInput { - width: 100%; - height: var(--height); - display: flex; - align-items: center; - padding: token('spacing.medium'); - color: token('color.background.neutral.0.foreground.primary'); - border-radius: token('border.radius.small'); - border: token('border.width.slim') solid - token('color.foreground.dynamic.primary'); -} - -.dateSelectorInput:focus-within, -.timeSelectorInput:focus-within { - box-shadow: inset 0 0 0 token('border.width.slim') - token('color.interactive.2.outline.background'); -} - -.dateSelectorSegment:focus, -.timeSelectorSegment:focus { - border-radius: token('border.radius.small'); -} - -.calendarSelectorGroup { - display: flex; - position: relative; - width: 100%; -} - -.calendarButton { - position: absolute; - right: 0; - border: none; - cursor: pointer; - background: none; - margin: token('spacing.medium'); - border-radius: token('border.radius.small'); -} - -.calendarDialog { - padding: token('spacing.xSmall'); - background: token('color.background.neutral.0.background'); - box-shadow: 0.2rem 0.2rem 0.3rem rgba(0, 0, 0, 0.15); -} - -.calendarDialog__header { - display: flex; - gap: token('spacing.medium'); - justify-content: center; - align-items: center; - padding: token('spacing.small'); -} - -.calendarDialog__title { - composes: typo-heading__m from '@atb/theme/typography.module.css'; -} - -.calendarDialog__headerButtons { - background: none; - border: none; - padding: token('spacing.xSmall') token('spacing.medium'); - cursor: pointer; -} - -.calendarGrid { - width: 100%; -} - -.calendarGrid__cell { - padding: token('spacing.xSmall') token('spacing.medium'); - cursor: pointer; - text-align: center; - border-radius: token('border.radius.small'); -} - -.calendarGrid__cell[data-disabled] { - opacity: 0.3; - cursor: default; -} - -.calendarGrid__cell[data-selected] { - background-color: token('color.interactive.2.active.background'); - color: token('color.interactive.2.active.foreground.primary'); - font-weight: bold; - border-radius: token('border.radius.small'); -} - -:global(.dark) - .dateSelector - input[type='date']::-webkit-calendar-picker-indicator, -:global(.dark) - .timeSelector - input[type='time']::-webkit-calendar-picker-indicator { - filter: invert(1); -} - -.required::after { - content: '*'; - color: token('color.status.error.primary.background'); - margin-left: token('spacing.small'); -} diff --git a/src/page-modules/contact/components/form/index.ts b/src/page-modules/contact/components/form/index.ts deleted file mode 100644 index f5bb15802..000000000 --- a/src/page-modules/contact/components/form/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { default as FieldWrapperWithError } from './field-wrapper-with-error'; -export { default as FileInput } from './file-input'; -export { default as Fieldset } from './fieldset'; -export { default as Input } from './input'; -export { default as Radio } from './radio'; -export { default as Select } from './select'; -export { default as Textarea } from './textarea'; -export { default as DateSelector } from './date-selector'; -export { default as TimeSelector } from './time-selector'; -export { - SearchableSelect, - getLineOptions, - getStopOptions, -} from './searchable-select'; diff --git a/src/page-modules/contact/components/form/input.tsx b/src/page-modules/contact/components/form/input.tsx deleted file mode 100644 index f4c8eb711..000000000 --- a/src/page-modules/contact/components/form/input.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import style from './form.module.css'; -import { ChangeEvent, useState } from 'react'; -import { PageText, TranslatedString, useTranslation } from '@atb/translations'; -import { andIf } from '@atb/utils/css'; -import { MonoIcon } from '@atb/components/icon'; -import { Button } from '@atb/components/button'; -import DescriptionModal from './description-modal'; -import { ErrorMessage } from '@atb/components/error-message'; -import FormComponentLabel from './form-component-label'; - -type InputProps = { - id: string; - label: string; - modalContent?: { - description?: string; - instruction?: string; - bulletPoints?: string[]; - }; - isRequired?: boolean; - errorMessage?: TranslatedString; - onChange?: (event: ChangeEvent) => void; -} & React.JSX.IntrinsicElements['input']; - -export const Input = ({ - id, - label, - errorMessage, - type, - name, - checked, - value, - modalContent, - disabled, - onChange, - placeholder, - isRequired, -}: InputProps) => { - const { t } = useTranslation(); - const [isModalOpen, setIsModalOpen] = useState(false); - const isModalContentProvided = hasModalContent(modalContent); - const displayDescriptionModal = isModalOpen && isModalContentProvided; - const toggleModalState = () => { - setIsModalOpen(!isModalOpen); - }; - - return ( -
-
- - {isModalContentProvided && ( -
- - {errorMessage && } - - {displayDescriptionModal && ( - - )} -
- ); -}; - -export default Input; - -function hasModalContent(content?: { - description?: string; - instruction?: string; - bulletPoints?: string[]; -}): boolean { - return !!( - content && - (content.description || - content.instruction || - (content.bulletPoints && content.bulletPoints?.length > 0)) - ); -} diff --git a/src/page-modules/contact/components/form/radio.tsx b/src/page-modules/contact/components/form/radio.tsx deleted file mode 100644 index 15d5fd98f..000000000 --- a/src/page-modules/contact/components/form/radio.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ChangeEvent, useId } from 'react'; -import style from './form.module.css'; -import { Typo } from '@atb/components/typography'; - -export type RadioInputProps = { - label: string; - onChange?: (event: ChangeEvent) => void; -} & React.JSX.IntrinsicElements['input']; - -export default function Radio({ - onChange, - checked, - label, - value, - name, -}: RadioInputProps) { - const id = useId(); - return ( -
- - - -
- ); -} diff --git a/src/page-modules/contact/components/form/searchable-select/index.tsx b/src/page-modules/contact/components/form/searchable-select/index.tsx deleted file mode 100644 index 318668d9e..000000000 --- a/src/page-modules/contact/components/form/searchable-select/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default as SearchableSelect } from './searchable-select'; -export { getLineOptions, getStopOptions } from './utils'; diff --git a/src/page-modules/contact/components/form/searchable-select/searchable-select.tsx b/src/page-modules/contact/components/form/searchable-select/searchable-select.tsx deleted file mode 100644 index c522da78a..000000000 --- a/src/page-modules/contact/components/form/searchable-select/searchable-select.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { useEffect, useState } from 'react'; -import { MonoIcon } from '@atb/components/icon'; -import style from '../form.module.css'; -import { andIf } from '@atb/utils/css'; -import { - Key, - Button, - ComboBox, - Input, - ListBox, - ListBoxItem, - Popover, - Group, -} from 'react-aria-components'; -import { ErrorMessage } from '@atb/components/error-message'; -import Label from '../form-component-label'; - -export type Option = { id: string; name: string; value: T }; -export type SearchableSelectProps = { - id: string; - label: string; - value?: T; - placeholder: string; - isDisabled?: boolean; - isRequired?: boolean; - options: Option[]; - error?: string; - onChange: (value?: T) => void; -}; - -export default function SearchableSelect({ - id, - label, - value, - placeholder, - isDisabled, - isRequired, - options, - error, - onChange, -}: SearchableSelectProps) { - const showError = !!error; - const [inputValue, setInputValue] = useState(''); - const [focused, setFocused] = useState(false); - - useEffect(() => { - if (value === undefined && !focused && inputValue !== '') { - setInputValue(''); - } - }, [value, focused, inputValue]); - - const handleFocus = () => { - setFocused(true); - }; - - const handleBlur = () => { - setFocused(false); - }; - - const filteredOptions = options.filter((item) => - item.name - .toLowerCase() - .replace(/\s+/g, '') // Remove all white spaces from item name - .includes( - inputValue.toLowerCase().replace(/\s+/g, ''), // Remove all white spaces from input value - ), - ); - - const getOptionBySelector = (key: string) => - options.find((option) => option.id.toLowerCase() === key.toLowerCase()); - - const getOptionBySearch = (input: string) => { - return options.find( - (option) => option.name.toLowerCase() === input.toLowerCase(), - ); - }; - - const handleSelectionChange = (key: Key | null) => { - if (key === null) return; - const keyString = key.toString(); - const option = getOptionBySelector(keyString); - setInputValue(option?.name || keyString); - onChange(option?.value); - }; - - const handleInputChange = (inputValue: string) => { - const option = getOptionBySearch(inputValue); - setInputValue(option?.name || inputValue); - onChange(option?.value); - }; - - return ( - } - isDisabled={isDisabled} - onSelectionChange={handleSelectionChange} - className={style.searchable_select__comboBox} - aria-label={label} - aria-labelledby={`label-${id}`} - > - - ); -} diff --git a/src/page-modules/contact/components/form/searchable-select/utils.ts b/src/page-modules/contact/components/form/searchable-select/utils.ts deleted file mode 100644 index 1deda9b8d..000000000 --- a/src/page-modules/contact/components/form/searchable-select/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Line } from '@atb/page-modules/contact'; -import { Option } from './searchable-select'; - -export const getLineOptions = (lines: Line[]): Option[] => { - return lines.map((line) => ({ - id: line.id, - name: line.publicCode ? `${line.publicCode} - ${line.name}` : line.name, - value: line, - })); -}; - -export const getStopOptions = ( - stops: Line['quays'], -): Option[] => { - return stops.map((stop) => ({ - id: stop.id, - name: stop.name, - value: stop, - })); -}; diff --git a/src/page-modules/contact/components/form/select.tsx b/src/page-modules/contact/components/form/select.tsx deleted file mode 100644 index d8e117ee5..000000000 --- a/src/page-modules/contact/components/form/select.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { MonoIcon } from '@atb/components/icon'; -import style from './form.module.css'; -import { - Button, - Key, - ListBox, - ListBoxItem, - Popover, - Select, -} from 'react-aria-components'; -import { ErrorMessage } from '@atb/components/error-message'; -import FormComponentLabel from './form-component-label'; - -export type SelectProps = { - id: string; - label?: string; - onChange: (value?: T) => void; - error?: string; - value?: T; - options: T[]; - valueToId: (val: T) => string; - valueToText: (val: T) => string; - placeholder?: string; - disabled?: boolean; - isRequired?: boolean; -}; - -export default function CustomSelect({ - id, - label, - onChange, - error, - value, - options, - valueToId, - valueToText, - placeholder, - disabled, - isRequired, -}: SelectProps) { - const showError = !!error; - - const findItemFromOptions = (key: string) => - options.find( - (option) => valueToId(option).toLowerCase() === key.toLowerCase(), - ); - - const onChangeInternal = (key: Key | null) => { - if (key === null) return; - onChange(findItemFromOptions(key.toString())); - }; - - return ( - - ); -} diff --git a/src/page-modules/contact/components/form/textarea.tsx b/src/page-modules/contact/components/form/textarea.tsx deleted file mode 100644 index 467aa86eb..000000000 --- a/src/page-modules/contact/components/form/textarea.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { ErrorMessage } from '@atb/components/error-message'; -import style from './form.module.css'; -import FileInput, { FileInputProps } from './file-input'; -import { Typo } from '@atb/components/typography'; -import { andIf } from '@atb/utils/css'; - -export type CheckboxProps = { - description?: string; - value: string; - error?: string; - isRequired?: boolean; - fileInputProps?: FileInputProps; - onChange: (event: React.ChangeEvent) => void; -} & React.JSX.IntrinsicElements['textarea']; - -export default function Textarea({ - id, - description, - onChange, - error, - isRequired = false, - value, - fileInputProps, -}: CheckboxProps) { - return ( -
- {description && ( - - {description} - - )} -