From eb2ca6c950cacbd568d97f3b473988fa5117ce62 Mon Sep 17 00:00:00 2001
From: Joe Karow <58997957+JoeKarow@users.noreply.github.com>
Date: Mon, 22 Jan 2024 12:02:19 -0500
Subject: [PATCH] feat: edit org page (part 1) (#733)
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* update crowdin client
* fix lockfile
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* full width for form
* clean up comments/dead code
* move in to folder
* dep for json -> csv conversion
* api routes/mock data
* move to dir, rework hook form
* fix lockfile
* add font fallbacks
* ignore _queries dir
* add serviceAreas, isNational
* update data
* custom superjson
* clean up import
* add countryCode column
* start data migration prep
* alter api route
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* vercel geo
* fix lockfile
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* remove umami
* fix provider error
* mobile app stuff
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* fix types/add EditModeProvider
* generated icons
* fix import
* downgrade @crowdin/crowdin-api-client per crowdin/crowdin-api-client-js#355
* use array in prefixedId
* update prefetch
* fix lint error
* convert dayIndex to luxon format
* remove dupe
* fix data schema
* update mock data
* rename ContactSection file to match component name
* separate out ContactInfo in to parts
* clear errors
* add API handler for phone numbers
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* fix barrel file
* update api route/mock data
* create edit mode ContactSection
* alter drawer/phone # entry form
* update api route & mock data
* add theme variants
* change sort order
* add api route & mock data
* contact section: phone & website
* create job summary
* api routes/mock data
* basic edit drawer
* Email drawer links
* fix conditional
* fix storybook conf for msw addon
* storybook conf tweak
* add `tiny-invariant`
* generate nested freetext upsert
* update api routes/mocks
* switch hook, fix data submission
* move in to folder
* api routes / mock data
* stop unminifying json mockdata
* api routes / mock data
* api routes / mock data
* update msw public script
* social media update drawer
* fix lockfile
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* enable wdyr on app, refactor icon
* remove ModalProvider & small cleanups
* fix wdyr & add env trigger
* i18n HMR & storybook swc
* page title
* chore: lint & format
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
* fix i18n hmr plugin loading
* fix plugin loading during build
* remove file
---------
Signed-off-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
Signed-off-by: Joe Karow <58997957+JoeKarow@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: InReach [bot] <108850934+InReach-svc@users.noreply.github.com>
---
.prettierignore | 1 +
apps/app/lib/wdyr.ts | 20 ++
apps/app/next-i18next.config.mjs | 28 +-
apps/app/next.config.mjs | 22 +-
apps/app/package.json | 3 +
apps/app/public/locales/en/common.json | 9 +
apps/app/src/middleware.ts | 2 +
apps/app/src/pages/_app.tsx | 36 +-
apps/app/src/pages/_document.tsx | 1 -
apps/app/src/pages/index.tsx | 4 +-
.../pages/org/[slug]/[orgLocationId]/edit.tsx | 6 +-
.../org/[slug]/[orgLocationId]/index.tsx | 34 +-
apps/app/src/pages/org/[slug]/edit.tsx | 212 ++++++------
apps/app/src/pages/org/[slug]/index.tsx | 21 +-
apps/app/src/pages/org/[slug]/remote.tsx | 2 +-
apps/app/src/providers/index.tsx | 36 +-
packages/api/lib/prismaRaw/coveredAreas.ts | 45 ---
packages/api/lib/prismaRaw/index.ts | 1 -
packages/api/prisma/org.ts | 134 --------
packages/api/router/misc/index.ts | 35 +-
.../misc/query.forEditNavbar.handler.ts | 24 ++
.../router/misc/query.forEditNavbar.schema.ts | 9 +
.../query.getCountryTranslation.handler.ts | 1 -
.../query.getCountryTranslation.schema.ts | 2 +-
packages/api/router/misc/schemas.ts | 1 +
packages/api/router/orgEmail/index.ts | 22 ++
.../orgEmail/mutation.update.handler.ts | 55 +++-
.../router/orgEmail/mutation.update.schema.ts | 34 +-
.../orgEmail/query.forContactInfo.schema.ts | 2 +-
.../query.forContactInfoEdit.handler.ts | 52 +++
.../query.forContactInfoEdit.schema.ts | 8 +
.../orgEmail/query.forEditDrawer.handler.ts | 37 +++
.../orgEmail/query.forEditDrawer.schema.ts | 6 +
packages/api/router/orgEmail/schemas.ts | 2 +
.../orgHours/query.forHoursDisplay.handler.ts | 5 +-
.../orgHours/query.forHoursDrawer.handler.ts | 5 +-
packages/api/router/orgPhone/index.ts | 22 ++
.../orgPhone/mutation.update.handler.ts | 54 +--
.../router/orgPhone/mutation.update.schema.ts | 32 +-
.../orgPhone/query.forContactInfo.schema.ts | 2 +-
.../query.forContactInfoEdit.handler.ts | 66 ++++
.../query.forContactInfoEdit.schema.ts | 8 +
.../orgPhone/query.forEditDrawer.handler.ts | 69 ++++
.../orgPhone/query.forEditDrawer.schema.ts | 6 +
packages/api/router/orgPhone/schemas.ts | 2 +
packages/api/router/orgSocialMedia/index.ts | 33 ++
.../orgSocialMedia/mutation.update.handler.ts | 38 ++-
.../orgSocialMedia/mutation.update.schema.ts | 4 +-
.../query.forContactInfo.handler.ts | 3 +-
.../query.forContactInfo.schema.ts | 2 +-
.../query.forContactInfoEdits.handler.ts | 43 +++
.../query.forContactInfoEdits.schema.ts | 8 +
.../query.forEditDrawer.handler.ts | 30 ++
.../query.forEditDrawer.schema.ts | 6 +
.../query.getServiceTypes.handler.ts | 23 ++
.../query.getServiceTypes.schema.ts | 7 +
packages/api/router/orgSocialMedia/schemas.ts | 3 +
packages/api/router/orgWebsite/index.ts | 22 ++
.../orgWebsite/mutation.update.handler.ts | 25 +-
.../orgWebsite/query.forContactInfo.schema.ts | 2 +-
.../query.forContactInfoEdit.handler.ts | 44 +++
.../query.forContactInfoEdit.schema.ts | 8 +
.../orgWebsite/query.forEditDrawer.handler.ts | 23 ++
.../orgWebsite/query.forEditDrawer.schema.ts | 6 +
packages/api/router/orgWebsite/schemas.ts | 2 +
packages/api/router/organization/index.ts | 22 +-
packages/api/router/organization/lib.ts | 26 --
.../mutation.updateBasic.handler.ts | 66 ++++
.../mutation.updateBasic.schema.ts | 11 +
.../query.forOrgPageEdits.handler.ts | 46 +++
.../query.forOrgPageEdits.schema.ts | 4 +
.../query.searchDistance.handler.ts | 8 +-
packages/api/router/organization/schemas.ts | 2 +
.../query.forServiceInfoCard.schema.ts | 2 +-
packages/api/schemas/idPrefix.ts | 7 +-
packages/db/index.ts | 6 +-
packages/db/lib/generateFreeText.ts | 59 +++-
.../!prep.ts | 168 ++++++++++
.../index.ts} | 18 +-
packages/db/prisma/data-migrations/index.ts | 1 +
.../prisma/views/public/pg_cache_hit_rate.sql | 10 +
.../db/prisma/views/public/pg_index_usage.sql | 49 +++
.../views/public/pg_index_usage_rate.sql | 12 +
packages/env/index.ts | 3 +
packages/ui/.storybook/i18next.ts | 3 +
packages/ui/.storybook/main.ts | 23 +-
packages/ui/.swcrc | 6 +-
packages/ui/.vscode/settings.json | 5 +-
packages/ui/babel.config.json | 19 --
packages/ui/components/core/Badge.tsx | 3 +-
packages/ui/components/core/Breadcrumb.tsx | 6 +-
.../components/data-display/ContactInfo.tsx | 310 ------------------
.../data-display/ContactInfo/Emails.tsx | 153 +++++++++
.../data-display/ContactInfo/PhoneNumbers.tsx | 159 +++++++++
.../data-display/ContactInfo/SocialMedia.tsx | 108 ++++++
.../data-display/ContactInfo/Websites.tsx | 138 ++++++++
.../data-display/ContactInfo/common.styles.ts | 10 +
.../index.stories.tsx} | 2 +-
.../data-display/ContactInfo/index.tsx | 53 +++
.../data-display/ContactInfo/types.ts | 48 +++
packages/ui/components/data-display/index.ts | 4 +-
.../data-portal/EmailDrawer/index.stories.tsx | 42 +++
.../data-portal/EmailDrawer/index.tsx | 191 +++++++++++
.../data-portal/InlineTextInput.stories.tsx | 29 +-
.../data-portal/InlineTextInput.tsx | 55 +---
.../data-portal/PhoneDrawer/index.stories.tsx | 38 +++
.../data-portal/PhoneDrawer/index.tsx | 224 +++++++++++++
.../data-portal/PhoneNumberEntry.stories.tsx | 45 ---
.../PhoneNumberEntry/CountrySelectItem.tsx | 24 ++
.../PhoneNumberEntry/index.stories.tsx | 81 +++++
.../index.tsx} | 28 +-
.../data-portal/PhoneNumberEntry/lib.ts | 19 ++
.../data-portal/PhoneNumberEntry/styles.ts | 27 ++
.../PhoneNumberEntry/withHookForm.tsx | 169 ++++++++++
.../data-portal/ServiceEditDrawer.tsx | 6 +-
.../SocialMediaDrawer/index.stories.tsx | 42 +++
.../data-portal/SocialMediaDrawer/index.tsx | 215 ++++++++++++
.../data-portal/TimeRange/index.tsx | 13 +-
.../WebsiteDrawer/index.stories.tsx | 37 +++
.../data-portal/WebsiteDrawer/index.tsx | 174 ++++++++++
.../index.stories.tsx} | 3 +-
.../{Contact.tsx => ContactSection/index.tsx} | 5 +-
.../components/sections/ListingBasicInfo.tsx | 110 +++++--
packages/ui/components/sections/Navbar.tsx | 105 +++++-
packages/ui/components/sections/index.ts | 6 +-
packages/ui/hooks/index.ts | 1 +
packages/ui/hooks/useEditMode.ts | 18 +
packages/ui/icon/buildIcons.ts | 9 +-
packages/ui/icon/iconCollection.ts | 5 +-
packages/ui/icon/iconList.ts | 2 +-
.../{Icon.stories.tsx => index.stories.tsx} | 0
packages/ui/icon/index.tsx | 28 +-
.../ui/loading-states/OrgLocationPage.tsx | 25 ++
packages/ui/loading-states/OrgPage.tsx | 25 ++
.../json/orgEmail.forContactInfoEdit.json | 1 +
.../mockData/json/orgEmail.forEditDrawer.json | 1 +
.../json/orgPhone.forContactInfoEdit.json | 1 +
.../mockData/json/orgPhone.forEditDrawer.json | 1 +
.../ui/mockData/json/orgPhone.update.json | 1 +
.../json/orgSocialMedia.forContactInfo.json | 2 +-
.../orgSocialMedia.forContactInfoEdits.json | 1 +
.../json/orgSocialMedia.forEditDrawer.json | 1 +
.../json/orgSocialMedia.getServiceTypes.json | 1 +
.../json/orgWebsite.forContactInfoEdit.json | 1 +
.../json/orgWebsite.forEditDrawer.json | 1 +
packages/ui/mockData/orgEmail.ts | 24 ++
packages/ui/mockData/orgHours.ts | 13 +-
packages/ui/mockData/orgPhone.ts | 26 +-
packages/ui/mockData/orgSocialMedia.ts | 32 ++
packages/ui/mockData/orgWebsite.ts | 29 ++
packages/ui/modals/Service.tsx | 3 +-
packages/ui/package.json | 8 +
packages/ui/providers/EditMode.tsx | 32 ++
packages/ui/public/mockServiceWorker.js | 4 +-
packages/ui/store/index.ts | 19 ++
packages/ui/theme/variants/Text.ts | 37 +++
packages/ui/theme/variants/index.ts | 6 +
packages/util/luxon/weekday.ts | 6 +
pnpm-lock.yaml | 149 +++++++++
159 files changed, 4189 insertions(+), 1092 deletions(-)
create mode 100644 apps/app/lib/wdyr.ts
delete mode 100644 packages/api/lib/prismaRaw/coveredAreas.ts
delete mode 100644 packages/api/prisma/org.ts
create mode 100644 packages/api/router/misc/query.forEditNavbar.handler.ts
create mode 100644 packages/api/router/misc/query.forEditNavbar.schema.ts
create mode 100644 packages/api/router/orgEmail/query.forContactInfoEdit.handler.ts
create mode 100644 packages/api/router/orgEmail/query.forContactInfoEdit.schema.ts
create mode 100644 packages/api/router/orgEmail/query.forEditDrawer.handler.ts
create mode 100644 packages/api/router/orgEmail/query.forEditDrawer.schema.ts
create mode 100644 packages/api/router/orgPhone/query.forContactInfoEdit.handler.ts
create mode 100644 packages/api/router/orgPhone/query.forContactInfoEdit.schema.ts
create mode 100644 packages/api/router/orgPhone/query.forEditDrawer.handler.ts
create mode 100644 packages/api/router/orgPhone/query.forEditDrawer.schema.ts
create mode 100644 packages/api/router/orgSocialMedia/query.forContactInfoEdits.handler.ts
create mode 100644 packages/api/router/orgSocialMedia/query.forContactInfoEdits.schema.ts
create mode 100644 packages/api/router/orgSocialMedia/query.forEditDrawer.handler.ts
create mode 100644 packages/api/router/orgSocialMedia/query.forEditDrawer.schema.ts
create mode 100644 packages/api/router/orgSocialMedia/query.getServiceTypes.handler.ts
create mode 100644 packages/api/router/orgSocialMedia/query.getServiceTypes.schema.ts
create mode 100644 packages/api/router/orgWebsite/query.forContactInfoEdit.handler.ts
create mode 100644 packages/api/router/orgWebsite/query.forContactInfoEdit.schema.ts
create mode 100644 packages/api/router/orgWebsite/query.forEditDrawer.handler.ts
create mode 100644 packages/api/router/orgWebsite/query.forEditDrawer.schema.ts
delete mode 100644 packages/api/router/organization/lib.ts
create mode 100644 packages/api/router/organization/mutation.updateBasic.handler.ts
create mode 100644 packages/api/router/organization/mutation.updateBasic.schema.ts
create mode 100644 packages/api/router/organization/query.forOrgPageEdits.handler.ts
create mode 100644 packages/api/router/organization/query.forOrgPageEdits.schema.ts
create mode 100644 packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/!prep.ts
rename packages/db/prisma/data-migrations/{!YYYY-MM-DD_job-template.ts => 2023-09-28_phone-number-normalization/index.ts} (76%)
create mode 100644 packages/db/prisma/views/public/pg_cache_hit_rate.sql
create mode 100644 packages/db/prisma/views/public/pg_index_usage.sql
create mode 100644 packages/db/prisma/views/public/pg_index_usage_rate.sql
delete mode 100644 packages/ui/babel.config.json
delete mode 100644 packages/ui/components/data-display/ContactInfo.tsx
create mode 100644 packages/ui/components/data-display/ContactInfo/Emails.tsx
create mode 100644 packages/ui/components/data-display/ContactInfo/PhoneNumbers.tsx
create mode 100644 packages/ui/components/data-display/ContactInfo/SocialMedia.tsx
create mode 100644 packages/ui/components/data-display/ContactInfo/Websites.tsx
create mode 100644 packages/ui/components/data-display/ContactInfo/common.styles.ts
rename packages/ui/components/data-display/{ContactInfo.stories.tsx => ContactInfo/index.stories.tsx} (95%)
create mode 100644 packages/ui/components/data-display/ContactInfo/index.tsx
create mode 100644 packages/ui/components/data-display/ContactInfo/types.ts
create mode 100644 packages/ui/components/data-portal/EmailDrawer/index.stories.tsx
create mode 100644 packages/ui/components/data-portal/EmailDrawer/index.tsx
create mode 100644 packages/ui/components/data-portal/PhoneDrawer/index.stories.tsx
create mode 100644 packages/ui/components/data-portal/PhoneDrawer/index.tsx
delete mode 100644 packages/ui/components/data-portal/PhoneNumberEntry.stories.tsx
create mode 100644 packages/ui/components/data-portal/PhoneNumberEntry/CountrySelectItem.tsx
create mode 100644 packages/ui/components/data-portal/PhoneNumberEntry/index.stories.tsx
rename packages/ui/components/data-portal/{PhoneNumberEntry.tsx => PhoneNumberEntry/index.tsx} (99%)
create mode 100644 packages/ui/components/data-portal/PhoneNumberEntry/lib.ts
create mode 100644 packages/ui/components/data-portal/PhoneNumberEntry/styles.ts
create mode 100644 packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx
create mode 100644 packages/ui/components/data-portal/SocialMediaDrawer/index.stories.tsx
create mode 100644 packages/ui/components/data-portal/SocialMediaDrawer/index.tsx
create mode 100644 packages/ui/components/data-portal/WebsiteDrawer/index.stories.tsx
create mode 100644 packages/ui/components/data-portal/WebsiteDrawer/index.tsx
rename packages/ui/components/sections/{Contact.stories.tsx => ContactSection/index.stories.tsx} (92%)
rename packages/ui/components/sections/{Contact.tsx => ContactSection/index.tsx} (82%)
create mode 100644 packages/ui/hooks/useEditMode.ts
rename packages/ui/icon/{Icon.stories.tsx => index.stories.tsx} (100%)
create mode 100644 packages/ui/loading-states/OrgLocationPage.tsx
create mode 100644 packages/ui/loading-states/OrgPage.tsx
create mode 100644 packages/ui/mockData/json/orgEmail.forContactInfoEdit.json
create mode 100644 packages/ui/mockData/json/orgEmail.forEditDrawer.json
create mode 100644 packages/ui/mockData/json/orgPhone.forContactInfoEdit.json
create mode 100644 packages/ui/mockData/json/orgPhone.forEditDrawer.json
create mode 100644 packages/ui/mockData/json/orgPhone.update.json
create mode 100644 packages/ui/mockData/json/orgSocialMedia.forContactInfoEdits.json
create mode 100644 packages/ui/mockData/json/orgSocialMedia.forEditDrawer.json
create mode 100644 packages/ui/mockData/json/orgSocialMedia.getServiceTypes.json
create mode 100644 packages/ui/mockData/json/orgWebsite.forContactInfoEdit.json
create mode 100644 packages/ui/mockData/json/orgWebsite.forEditDrawer.json
create mode 100644 packages/ui/providers/EditMode.tsx
create mode 100644 packages/ui/store/index.ts
create mode 100644 packages/util/luxon/weekday.ts
diff --git a/.prettierignore b/.prettierignore
index 0b5e4595c6..4c6848663d 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -14,3 +14,4 @@ pnpm-lock.yaml
.eslintignore
*.hbs
.trace/*.json
+packages/ui/mockData/json/*.json
diff --git a/apps/app/lib/wdyr.ts b/apps/app/lib/wdyr.ts
new file mode 100644
index 0000000000..8ad61c8c29
--- /dev/null
+++ b/apps/app/lib/wdyr.ts
@@ -0,0 +1,20 @@
+///
+import React from 'react'
+
+// eslint-disable-next-line node/no-process-env
+if (process.env.NODE_ENV === 'development' && !!process.env.WDYR) {
+ if (typeof window !== 'undefined') {
+ const loadWdyr = async () => {
+ const { default: whyDidYouRender } = await import('@welldone-software/why-did-you-render')
+ whyDidYouRender(React, {
+ trackAllPureComponents: true,
+ include: [/.*/],
+ exclude: [/.*mantine.*/i],
+ logOnDifferentValues: false,
+ logOwnerReasons: true,
+ collapseGroups: true,
+ })
+ }
+ loadWdyr()
+ }
+}
diff --git a/apps/app/next-i18next.config.mjs b/apps/app/next-i18next.config.mjs
index 077a3fa03e..34ee72bb18 100644
--- a/apps/app/next-i18next.config.mjs
+++ b/apps/app/next-i18next.config.mjs
@@ -6,6 +6,7 @@ import HttpBackend from 'i18next-http-backend'
import intervalPlural from 'i18next-intervalplural-postprocessor'
// import LocalStorageBackend from 'i18next-localstorage-backend'
import MultiBackend from 'i18next-multiload-backend-adapter'
+import compact from 'just-compact'
import path from 'path'
@@ -13,7 +14,7 @@ import path from 'path'
import { localeList } from '@weareinreach/db/generated/locales.mjs'
const isBrowser = typeof window !== 'undefined'
-const isDev = process.env.NODE_ENV !== 'production'
+const isDev = process.env.NODE_ENV !== 'production' && !process.env.CI
const isVerbose = !!process.env.NEXT_VERBOSE
// const Keys = z.record(z.string())
@@ -42,6 +43,29 @@ const multi = new MultiBackend(null, {
},
})
+const plugins = () => {
+ /** @type {any[]} */
+ const pluginsToUse = [intervalPlural, LanguageDetector]
+ if (isBrowser) {
+ pluginsToUse.push(ChainedBackend)
+ }
+ if (process.env.NODE_ENV === 'development') {
+ if (isBrowser) {
+ // @ts-expect-error - yelling about declaration file
+ import('i18next-hmr/plugin').then(({ HMRPlugin }) =>
+ pluginsToUse.push(new HMRPlugin({ webpack: { client: true } }))
+ )
+ } else {
+ // @ts-expect-error - yelling about declaration file
+ import('i18next-hmr/plugin').then(({ HMRPlugin }) =>
+ pluginsToUse.push(new HMRPlugin({ webpack: { server: true } }))
+ )
+ }
+ }
+
+ return compact(pluginsToUse)
+}
+
/** @type {import('next-i18next').UserConfig} */
const config = {
i18n: {
@@ -76,7 +100,7 @@ const config = {
backends: isBrowser ? [multi] : [],
},
serializeConfig: false,
- use: isBrowser ? [ChainedBackend, intervalPlural, LanguageDetector] : [intervalPlural, LanguageDetector],
+ use: plugins(),
maxParallelReads: 20,
joinArrays: '',
interpolation: {
diff --git a/apps/app/next.config.mjs b/apps/app/next.config.mjs
index ee813f1283..db4e444868 100644
--- a/apps/app/next.config.mjs
+++ b/apps/app/next.config.mjs
@@ -78,10 +78,30 @@ const nextConfig = {
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: isVercelProd || isVercelActiveDev,
},
- webpack: (config, { isServer, webpack }) => {
+ webpack: (config, { dev, isServer, webpack }) => {
if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()]
}
+ if (dev && !isServer) {
+ /** WDYR */
+ const origEntry = config.entry
+ config.entry = async () => {
+ const wdyrPath = path.resolve(__dirname, './lib/wdyr.ts')
+ const entries = await origEntry()
+ if (entries['main.js'] && !entries['main.js'].includes(wdyrPath)) {
+ entries['main.js'].push(wdyrPath)
+ }
+ return entries
+ }
+ /** I18 HMR */
+ import('i18next-hmr/webpack').then(({ I18NextHMRPlugin }) =>
+ config.plugins.push(
+ new I18NextHMRPlugin({
+ localesDir: path.resolve(__dirname, 'public/static/locales'),
+ })
+ )
+ )
+ }
config.devtool = 'eval-source-map'
diff --git a/apps/app/package.json b/apps/app/package.json
index ab0539ccc3..7afe57ddc7 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -36,6 +36,7 @@
"@mantine/nprogress": "6.0.21",
"@mantine/utils": "6.0.21",
"@next/bundle-analyzer": "14.1.0",
+ "@next/third-parties": "14.1.0",
"@opentelemetry/api": "1.7.0",
"@opentelemetry/core": "1.20.0",
"@opentelemetry/exporter-trace-otlp-http": "0.47.0",
@@ -119,10 +120,12 @@
"@typescript-eslint/parser": "6.19.0",
"@weareinreach/config": "workspace:*",
"@weareinreach/eslint-config": "0.100.0",
+ "@welldone-software/why-did-you-render": "8.0.1",
"commander": "11.1.0",
"dotenv": "16.3.1",
"eslint": "8.56.0",
"eslint-plugin-i18next": "6.0.3",
+ "i18next-hmr": "3.0.4",
"listr2": "8.0.1",
"prettier": "3.2.4",
"trpc-client-devtools-link": "0.2.1-next",
diff --git a/apps/app/public/locales/en/common.json b/apps/app/public/locales/en/common.json
index c0c29dcbf9..26df5b794a 100644
--- a/apps/app/public/locales/en/common.json
+++ b/apps/app/public/locales/en/common.json
@@ -110,6 +110,9 @@
"try-again-text": "Something went wrong! Please try again."
},
"exclude": "Exclude",
+ "exit": {
+ "edit-mode": "Exit edit mode"
+ },
"filter-by-service": "Filter by services",
"find-resources": "Find resources",
"find-x": "Find {{value}}",
@@ -189,6 +192,7 @@
"other-specify": "Other (please specify)",
"page-title": {
"base": "{{- title}} - InReach",
+ "edit-mode": "{{- title}} [EDIT MODE] - InReach",
"search-results": "Search Results"
},
"password": "Password",
@@ -423,9 +427,13 @@
"please-wait": "Please wait...",
"prev": "Prev",
"print": "Print",
+ "publish": "Publish",
+ "restore": "Restore",
+ "reverify": "Reverify",
"review": "Review",
"reviews": "Reviews",
"save": "Save",
+ "save-changes": "Save changes",
"saved": "Saved",
"search": "Search",
"service-hours": "Service hours",
@@ -434,6 +442,7 @@
"sign-up": "Sign up",
"skip": "Skip",
"support": "Support",
+ "unpublish": "Unpublish",
"website": "Website",
"yes": "Yes"
}
diff --git a/apps/app/src/middleware.ts b/apps/app/src/middleware.ts
index 0a86929357..9d6a94e1ee 100644
--- a/apps/app/src/middleware.ts
+++ b/apps/app/src/middleware.ts
@@ -1,8 +1,10 @@
+import { track } from '@vercel/analytics/server'
import { get } from '@vercel/edge-config'
import { type NextMiddleware, type NextRequest, NextResponse } from 'next/server'
export const middleware: NextMiddleware = async (req: NextRequest) => {
const res = NextResponse.next()
+ track('geo', { ...req.geo })
if (!req.cookies.has('inreach-session')) {
res.cookies.set({
diff --git a/apps/app/src/pages/_app.tsx b/apps/app/src/pages/_app.tsx
index b6619613f4..29c81b8397 100644
--- a/apps/app/src/pages/_app.tsx
+++ b/apps/app/src/pages/_app.tsx
@@ -1,3 +1,5 @@
+import '../../lib/wdyr'
+
import { Space } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
import { Analytics } from '@vercel/analytics/react'
@@ -37,31 +39,11 @@ const defaultSEO = {
titleTemplate: '%s | InReach',
defaultTitle: 'InReach',
additionalLinkTags: [
- {
- rel: 'icon',
- href: '/favicon-16x16.png',
- sizes: '16x16',
- },
- {
- rel: 'icon',
- href: '/favicon-32x32.png',
- sizes: '32x32',
- },
- {
- rel: 'icon',
- href: '/favicon-96x96.png',
- sizes: '96x96',
- },
- {
- rel: 'apple-touch-icon',
- href: '/apple-icon-120x120.png',
- sizes: '120x120',
- },
- {
- rel: 'apple-touch-icon',
- href: '/apple-icon-180x180.png',
- sizes: '180x180',
- },
+ { rel: 'icon', href: '/favicon-16x16.png', sizes: '16x16' },
+ { rel: 'icon', href: '/favicon-32x32.png', sizes: '32x32' },
+ { rel: 'icon', href: '/favicon-96x96.png', sizes: '96x96' },
+ { rel: 'apple-touch-icon', href: '/apple-icon-120x120.png', sizes: '120x120' },
+ { rel: 'apple-touch-icon', href: '/apple-icon-180x180.png', sizes: '180x180' },
],
} satisfies DefaultSeoProps
@@ -117,8 +99,8 @@ const MyApp = (appProps: AppPropsWithGridSwitch) => {
export default api.withTRPC(appWithTranslation(MyApp, nextI18nConfig))
-export type NextPageWithoutGrid
= NextPage
& {
+export type NextPageWithOptions = NextPage & {
omitGrid?: boolean
autoResetState?: boolean
}
-type AppPropsWithGridSwitch = AppProps<{ session: Session }> & { Component: NextPageWithoutGrid }
+type AppPropsWithGridSwitch = AppProps<{ session: Session }> & { Component: NextPageWithOptions }
diff --git a/apps/app/src/pages/_document.tsx b/apps/app/src/pages/_document.tsx
index c96680a2de..4599ad5527 100644
--- a/apps/app/src/pages/_document.tsx
+++ b/apps/app/src/pages/_document.tsx
@@ -10,7 +10,6 @@ const stylesServer = createStylesServer(appCache)
export default class _Document extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
-
return {
...initialProps,
styles: [
diff --git a/apps/app/src/pages/index.tsx b/apps/app/src/pages/index.tsx
index ba9246503e..13a0fe2a39 100644
--- a/apps/app/src/pages/index.tsx
+++ b/apps/app/src/pages/index.tsx
@@ -35,7 +35,7 @@ import { ResetPasswordModal } from '@weareinreach/ui/modals/ResetPassword'
import { api } from '~app/utils/api'
import { getServerSideTranslations } from '~app/utils/i18n'
-import { type NextPageWithoutGrid } from './_app'
+import { type NextPageWithOptions } from './_app'
const useStyles = createStyles((theme) => ({
callout1text: {
@@ -159,7 +159,7 @@ const CardTranslation = ({ i18nKey, t }: { i18nKey: string; t: TFunction }) => {
)
}
-const Home: NextPageWithoutGrid = () => {
+const Home: NextPageWithOptions = () => {
const router = useRouter()
const { t } = useTranslation('landingPage')
const theme = useMantineTheme()
diff --git a/apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx b/apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx
index f591dc6d11..b888e3b9ea 100644
--- a/apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx
+++ b/apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx
@@ -9,13 +9,14 @@ import { z } from 'zod'
import { trpcServerClient } from '@weareinreach/api/trpc'
import { checkServerPermissions } from '@weareinreach/auth'
import { Toolbar } from '@weareinreach/ui/components/core/Toolbar'
-import { ContactSection } from '@weareinreach/ui/components/sections/Contact'
+import { ContactSection } from '@weareinreach/ui/components/sections/ContactSection'
import { ListingBasicInfo } from '@weareinreach/ui/components/sections/ListingBasicInfo'
// import {LocationCard } from '@weareinreach/ui/components/sections/LocationCard'
import { PhotosSection } from '@weareinreach/ui/components/sections/Photos'
import { ReviewSection } from '@weareinreach/ui/components/sections/Reviews'
import { ServicesInfoCard } from '@weareinreach/ui/components/sections/ServicesInfo'
import { VisitCard } from '@weareinreach/ui/components/sections/VisitCard'
+import { OrgLocationPageLoading } from '@weareinreach/ui/loading-states/OrgLocationPage'
import { api } from '~app/utils/api'
import { getServerSideTranslations } from '~app/utils/i18n'
@@ -36,7 +37,7 @@ const OrgLocationPage: NextPage = () => {
useEffect(() => {
if (data && status === 'success' && orgData && orgDataStatus === 'success') setLoading(false)
}, [data, status, orgData, orgDataStatus])
- if (loading || !data || !orgData) return <>Loading>
+ if (loading || !data || !orgData) return
const {
// emails,
@@ -69,7 +70,6 @@ const OrgLocationPage: NextPage = () => {
/>
(
- <>
-
- {/* Toolbar */}
-
-
- {/* Listing Basic */}
-
- {/* Body */}
-
- {/* Tab panels */}
-
-
-
-
- {/* Contact Card */}
-
- {/* Visit Card */}
-
-
-
- >
-)
-
const useStyles = createStyles((theme) => ({
tabsList: {
position: 'sticky',
@@ -89,7 +65,7 @@ const OrgLocationPage: NextPage = () => {
useEffect(() => {
if (data && status === 'success' && orgData && orgDataStatus === 'success') setLoading(false)
}, [data, status, orgData, orgDataStatus])
- if (loading || !data || !orgData || router.isFallback) return
+ if (loading || !data || !orgData || router.isFallback) return
const { attributes, description, reviews } = data
@@ -125,7 +101,6 @@ const OrgLocationPage: NextPage = () => {
/>
))}
{
- const { t } = useTranslation()
+const formSchema = z
+ .object({
+ name: z.string(),
+ description: z.string(),
+ })
+ .partial()
+type FormSchema = z.infer
+
+const OrganizationPage: NextPageWithOptions> = () => {
const router = useRouter<'/org/[slug]'>()
- const { query } = router.isReady ? router : { query: { slug: '' } }
- const [activeTab, setActiveTab] = useState('services')
+ const {
+ query: { slug: pageSlug },
+ } = router.isReady ? router : { query: { slug: '' } }
+ const { data, status } = api.organization.forOrgPageEdits.useQuery(
+ { slug: pageSlug },
+ { enabled: router.isReady }
+ )
+
+ const formMethods = useForm({
+ values: {
+ name: data?.name,
+ description: data?.description?.tsKey?.text,
+ },
+ })
const [loading, setLoading] = useState(true)
- const { data, status } = api.organization.getBySlug.useQuery(query, { enabled: router.isReady })
+ const { data: hasRemote } = api.service.forServiceInfoCard.useQuery(
+ { parentId: data?.id ?? '', remoteOnly: true },
+ {
+ enabled: !!data?.id && data?.locations.length > 1,
+ select: (data) => data.length !== 0,
+ }
+ )
const { ref, width } = useElementSize()
useEffect(() => {
if (data && status === 'success') setLoading(false)
}, [data, status])
- if (loading || !data) return <>Loading>
-
- const {
- // emails,
- // phones,
- // socialMedia,
- // websites,
- userLists,
- attributes,
- description,
- slug,
- // photos,
- reviews,
- locations,
- isClaimed,
- id: organizationId,
- } = data
+ if (loading || !data) return
- const body =
- locations?.length === 1 ? (
-
-
- {t('services')}
- {t('photo', { count: 2 })}
- {t('review', { count: 2 })}
-
-
-
-
-
-
-
-
-
-
-
- ) : (
- <>
- {locations.map((location) => (
-
- ))}
- >
- )
-
- const sidebar =
- locations?.length === 1 ? (
- <>{locations[0] && }>
- ) : (
- locations.length && (
-
- {width && (
- id)}
- width={width}
- height={Math.floor(width * 1.185)}
- />
- )}
-
- )
- )
+ const { attributes, description, slug, locations, isClaimed } = data
return (
<>
- EDIT MODE
-
- router.back() }}
- saved={Boolean(userLists?.length)}
- organizationId={organizationId}
- />
-
-
- {body}
-
-
-
-
-
- {/* */}
- {/* */}
- {sidebar}
-
-
+
+ {t('page-title.edit-mode', { ns: 'common', title: data.name })}
+
+
+
+
+
+
+ {locations.map((location) => (
+
+ ))}
+ {hasRemote && }
+
+
+
+
+
+
+
+ {!!width && (
+ id)}
+ width={width}
+ height={Math.floor(width * 1.185)}
+ />
+ )}
+
+
+
+
+
>
)
}
-export const getServerSideProps: GetServerSideProps<
- Record,
- RoutedQuery<'/org/[slug]'>
-> = async ({ locale, params, req, res }) => {
+export const getServerSideProps = async ({
+ locale,
+ params,
+ req,
+ res,
+}: GetServerSidePropsContext>) => {
if (!params) return { notFound: true }
const { slug } = params
@@ -154,17 +136,21 @@ export const getServerSideProps: GetServerSideProps<
}
const ssg = await trpcServerClient({ session })
+ const orgId = await ssg.organization.getIdFromSlug.fetch({ slug })
- await ssg.organization.getBySlug.prefetch({ slug })
+ const [i18n] = await Promise.allSettled([
+ getServerSideTranslations(locale, compact(['common', 'services', 'attribute', 'phone-type', orgId?.id])),
+ ssg.organization.forOrgPageEdits.prefetch({ slug }),
+ ssg.fieldOpt.countries.prefetch({ activeForOrgs: true }),
+ ])
const props = {
session,
trpcState: ssg.dehydrate(),
- ...(await getServerSideTranslations(locale, ['common', 'services', 'attribute', 'phone-type', slug])),
+ ...(i18n.status === 'fulfilled' ? i18n.value : {}),
}
return {
props,
}
}
-
export default OrganizationPage
diff --git a/apps/app/src/pages/org/[slug]/index.tsx b/apps/app/src/pages/org/[slug]/index.tsx
index c3e35d82de..c25590c734 100644
--- a/apps/app/src/pages/org/[slug]/index.tsx
+++ b/apps/app/src/pages/org/[slug]/index.tsx
@@ -9,12 +9,10 @@ import { type RoutedQuery } from 'nextjs-routes'
import { useEffect, useRef, useState } from 'react'
import { trpcServerClient } from '@weareinreach/api/trpc'
-// import { getEnv } from '@weareinreach/env'
-// import { prisma } from '@weareinreach/db/client'
import { AlertMessage } from '@weareinreach/ui/components/core/AlertMessage'
// import { GoogleMap } from '@weareinreach/ui/components/core/GoogleMap'
import { Toolbar } from '@weareinreach/ui/components/core/Toolbar'
-import { ContactSection } from '@weareinreach/ui/components/sections/Contact'
+import { ContactSection } from '@weareinreach/ui/components/sections/ContactSection'
import { ListingBasicInfo } from '@weareinreach/ui/components/sections/ListingBasicInfo'
import { LocationCard } from '@weareinreach/ui/components/sections/LocationCard'
import { PhotosSection } from '@weareinreach/ui/components/sections/Photos'
@@ -22,6 +20,7 @@ import { ReviewSection } from '@weareinreach/ui/components/sections/Reviews'
import { ServicesInfoCard } from '@weareinreach/ui/components/sections/ServicesInfo'
import { VisitCard } from '@weareinreach/ui/components/sections/VisitCard'
import { useSearchState } from '@weareinreach/ui/hooks/useSearchState'
+import { OrgPageLoading } from '@weareinreach/ui/loading-states/OrgPage'
import { api } from '~app/utils/api'
import { getServerSideTranslations } from '~app/utils/i18n'
@@ -61,7 +60,10 @@ const useStyles = createStyles((theme) => ({
},
}))
-const OrganizationPage = ({ slug }: InferGetStaticPropsType) => {
+const OrganizationPage = ({
+ slug,
+ organizationId: orgId,
+}: InferGetStaticPropsType) => {
const router = useRouter<'/org/[slug]'>()
const { data, status } = api.organization.forOrgPage.useQuery({ slug }, { enabled: !!slug })
// const { query } = router
@@ -69,7 +71,7 @@ const OrganizationPage = ({ slug }: InferGetStaticPropsType('services')
const [loading, setLoading] = useState(true)
const { data: hasRemote } = api.service.forServiceInfoCard.useQuery(
@@ -104,7 +106,7 @@ const OrganizationPage = ({ slug }: InferGetStaticPropsType
+ if (loading || !data || router.isFallback) return
const { userLists, attributes, description, reviews, locations, isClaimed, id: organizationId } = data
@@ -218,7 +220,6 @@ const OrganizationPage = ({ slug }: InferGetStaticPropsType
))}
{
)
return (
- //
-
-
-
+
+ {/* */}
+
+
{children}
{/* {t('cookie-consent.intro')} */}
-
-
-
- //
+
+
+ {/* */}
+
)
}
diff --git a/packages/api/lib/prismaRaw/coveredAreas.ts b/packages/api/lib/prismaRaw/coveredAreas.ts
deleted file mode 100644
index c1153ff2fb..0000000000
--- a/packages/api/lib/prismaRaw/coveredAreas.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// import { type Context } from '../context'
-
-// export const getCoveredDists = async (coords: Coords, ctx: Context) => {
-// const { lat, lon } = coords
-
-// const result = await ctx.prisma.$queryRaw<{ id: string }[]>`
-// SELECT id FROM "GovDist"
-// WHERE ST_CoveredBy(ST_SetSRID(ST_MakePoint(${lon}, ${lat}),4326), geo)
-// `
-// return result
-// }
-// export const getCoveredCountry = async (coords: Coords, ctx: Context) => {
-// const { lat, lon } = coords
-
-// const result = await ctx.prisma.$queryRaw<{ id: string }>`
-// SELECT id FROM "Country"
-// WHERE ST_CoveredBy(ST_SetSRID(ST_MakePoint(${lon}, ${lat}),4326), geo)
-// `
-// return result
-// }
-// export const getCoveredAreas = async (coords: Coords, ctx: Context) => {
-// const { lat, lon } = coords
-
-// const result = await ctx.prisma.$transaction(async (tx) => {
-// const govDist = await tx.$queryRaw<{ id: string }[]>`
-// SELECT id FROM "GovDist"
-// WHERE ST_CoveredBy(ST_SetSRID(ST_MakePoint(${lon}, ${lat}),4326), geo)
-// `
-// const country = await tx.$queryRaw<{ id: string }[]>`
-// SELECT id FROM "Country"
-// WHERE ST_CoveredBy(ST_SetSRID(ST_MakePoint(${lon}, ${lat}),4326), geo)
-// `
-// return {
-// country,
-// govDist,
-// }
-// })
-// return result
-// }
-
-// type Coords = {
-// lat: number
-// lon: number
-// }
-export {}
diff --git a/packages/api/lib/prismaRaw/index.ts b/packages/api/lib/prismaRaw/index.ts
index f295d4eb50..93a111c83c 100644
--- a/packages/api/lib/prismaRaw/index.ts
+++ b/packages/api/lib/prismaRaw/index.ts
@@ -1,5 +1,4 @@
// codegen:start {preset: barrel, include: ./*.ts}
-export * from './coveredAreas'
export * from './orgSearch'
export * from './updateGeo'
// codegen:end
diff --git a/packages/api/prisma/org.ts b/packages/api/prisma/org.ts
deleted file mode 100644
index ff6aa8632e..0000000000
--- a/packages/api/prisma/org.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-// import { getDistance } from 'geolib'
-
-// import { type Prisma } from '@weareinreach/db'
-// import { type Context } from '~api/lib'
-// import { type DistSearch } from '~api/schemas/org/search'
-// import { isPublic } from '~api/schemas/selects/common'
-// import { orgSearchSelect } from '~api/schemas/selects/org'
-
-// export const prismaDistSearchDetails = async ({ ctx, input }: PrismaSearchDistance) => {
-// const { resultIds, lat: latitude, lon: longitude } = input
-// const results = await ctx.prisma.organization.findMany({
-// where: {
-// id: { in: resultIds },
-// // ...attributeFilter(attributes),
-// // ...serviceFilter(services),
-// ...isPublic,
-// },
-// select: orgSearchSelect,
-// })
-
-// const transformed = results.map(({ attributes, description, locations, services, ...rest }) => {
-// const servIds = new Set()
-// const attribIds = new Set()
-// const cities: City[] = []
-// const serviceTagMap = new Map()
-// const serviceCategoryMap = new Map()
-// const attributeMap = new Map()
-
-// services.forEach(({ services }) =>
-// services.forEach(({ tag, service }) => {
-// const { id, tsKey, tsNs, primaryCategory } = tag
-// servIds.add(id)
-// serviceCategoryMap.set(primaryCategory.id, primaryCategory)
-// serviceTagMap.set(id, { id, tsKey, tsNs })
-// service.attributes.forEach(({ attribute }) => {
-// const { categories, ...rest } = attribute
-// attribIds.add(rest.id)
-// categories.forEach(({ category }) =>
-// attributeMap.set(`${rest.id}${category.tag}`, { category, ...rest })
-// )
-// })
-// })
-// )
-
-// locations.forEach(({ services, city, ...coords }) => {
-// cities.push({
-// city,
-// dist: getDistance(
-// { latitude, longitude },
-// { latitude: coords.latitude ?? 0, longitude: coords.longitude ?? 0 },
-// 1000
-// ),
-// })
-// services.forEach(({ service }) =>
-// service.services.forEach(({ tag, service }) => {
-// const { id, tsKey, tsNs, primaryCategory } = tag
-// servIds.add(id)
-// serviceCategoryMap.set(primaryCategory.id, primaryCategory)
-// serviceTagMap.set(id, { id, tsKey, tsNs })
-// service.attributes.forEach(({ attribute }) => {
-// const { categories, ...rest } = attribute
-// attribIds.add(rest.id)
-// categories.forEach(({ category }) =>
-// attributeMap.set(`${rest.id}${category.tag}`, { category, ...rest })
-// )
-// })
-// })
-// )
-// })
-// attributes.forEach(({ attribute }) => {
-// const { categories, ...rest } = attribute
-// attribIds.add(rest.id)
-// categories.forEach(({ category }) =>
-// attributeMap.set(`${rest.id}${category.tag}`, { category, ...rest })
-// )
-// })
-
-// const desc = description
-// ? { key: description.key, ns: description.ns, text: description.tsKey.text }
-// : null
-
-// // const serviceTags = Array.from(serviceTagMap.values())
-// const serviceCategories = Array.from(serviceCategoryMap.values())
-// const allAttributes = Array.from(attributeMap.values())
-
-// const orgLeader = allAttributes.filter(({ category }) => category.tag === 'organization-leadership')
-// const orgFocus = allAttributes.filter(
-// ({ category, _count: count }) => category.tag === 'service-focus' && count.parents === 0
-// )
-// const sortedCities = [
-// ...new Set(cities.sort(({ dist: distA }, { dist: distB }) => distA - distB).map(({ city }) => city)),
-// ]
-
-// return {
-// ...rest,
-// description: desc,
-// // attributes: allAttributes,
-// // services: serviceTags,
-// serviceCategories,
-// orgLeader,
-// orgFocus,
-// locations: sortedCities,
-// }
-// })
-
-// return transformed
-// }
-
-// export type PrismaDistSearchDetailsResult = Prisma.PromiseReturnType
-// interface PrismaSearchDistance {
-// ctx: Context
-// input: DistSearch & { resultIds: string[] }
-// }
-// type IdKeyNs = {
-// id: string
-// tsKey: string
-// tsNs: string
-// }
-// type Attribute = {
-// id: string
-// tsKey: string
-// icon: string | null
-// iconBg: string | null
-// category: {
-// tag: string
-// }
-// _count: {
-// parents: number
-// }
-// }
-// type City = {
-// city: string
-// dist: number
-// }
diff --git a/packages/api/router/misc/index.ts b/packages/api/router/misc/index.ts
index 0f90f7194a..6be4c1dcf5 100644
--- a/packages/api/router/misc/index.ts
+++ b/packages/api/router/misc/index.ts
@@ -1,32 +1,45 @@
-import { defineRouter, publicProcedure } from '~api/lib/trpc'
+import { defineRouter, permissionedProcedure, publicProcedure } from '~api/lib/trpc'
import * as schema from './schemas'
-export const HandlerCache: Partial = {}
+const HandlerCache: Partial = {}
+
+type MiscHandlerCache = {
+ hasContactInfo: typeof import('./query.hasContactInfo.handler').hasContactInfo
+ getCountryTranslation: typeof import('./query.getCountryTranslation.handler').getCountryTranslation
+ forEditNavbar: typeof import('./query.forEditNavbar.handler').forEditNavbar
+}
+
export const miscRouter = defineRouter({
hasContactInfo: publicProcedure.input(schema.ZHasContactInfoSchema).query(async ({ ctx, input }) => {
- if (!HandlerCache.hasContactInfo) {
+ if (!HandlerCache.hasContactInfo)
HandlerCache.hasContactInfo = await import('./query.hasContactInfo.handler').then(
(mod) => mod.hasContactInfo
)
- }
+
if (!HandlerCache.hasContactInfo) throw new Error('Failed to load handler')
return HandlerCache.hasContactInfo({ ctx, input })
}),
getCountryTranslation: publicProcedure
.input(schema.ZGetCountryTranslationSchema)
.query(async ({ ctx, input }) => {
- if (!HandlerCache.getCountryTranslation) {
+ if (!HandlerCache.getCountryTranslation)
HandlerCache.getCountryTranslation = await import('./query.getCountryTranslation.handler').then(
(mod) => mod.getCountryTranslation
)
- }
+
if (!HandlerCache.getCountryTranslation) throw new Error('Failed to load handler')
return HandlerCache.getCountryTranslation({ ctx, input })
}),
-})
+ forEditNavbar: permissionedProcedure('updateLocation')
+ .input(schema.ZForEditNavbarSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forEditNavbar)
+ HandlerCache.forEditNavbar = await import('./query.forEditNavbar.handler').then(
+ (mod) => mod.forEditNavbar
+ )
-type MiscHandlerCache = {
- hasContactInfo: typeof import('./query.hasContactInfo.handler').hasContactInfo
- getCountryTranslation: typeof import('./query.getCountryTranslation.handler').getCountryTranslation
-}
+ if (!HandlerCache.forEditNavbar) throw new Error('Failed to load handler')
+ return HandlerCache.forEditNavbar({ ctx, input })
+ }),
+})
diff --git a/packages/api/router/misc/query.forEditNavbar.handler.ts b/packages/api/router/misc/query.forEditNavbar.handler.ts
new file mode 100644
index 0000000000..a52bae3b14
--- /dev/null
+++ b/packages/api/router/misc/query.forEditNavbar.handler.ts
@@ -0,0 +1,24 @@
+import { isIdFor, prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForEditNavbarSchema } from './query.forEditNavbar.schema'
+
+export const forEditNavbar = async ({ input }: TRPCHandlerParams) => {
+ try {
+ if (input.slug) {
+ const result = await prisma.organization.findUniqueOrThrow({
+ where: { slug: input.slug },
+ select: { published: true, deleted: true, lastVerified: true },
+ })
+ return result
+ }
+ const result = await prisma.orgLocation.findUniqueOrThrow({
+ where: { id: input.orgLocationId },
+ select: { published: true, deleted: true },
+ })
+ return { ...result, lastVerified: null }
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/misc/query.forEditNavbar.schema.ts b/packages/api/router/misc/query.forEditNavbar.schema.ts
new file mode 100644
index 0000000000..1464036d9d
--- /dev/null
+++ b/packages/api/router/misc/query.forEditNavbar.schema.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForEditNavbarSchema = z.union([
+ z.object({ slug: z.string(), orgLocationId: z.never().optional() }),
+ z.object({ orgLocationId: prefixedId('orgLocation'), slug: z.never().optional() }),
+])
+export type TForEditNavbarSchema = z.infer
diff --git a/packages/api/router/misc/query.getCountryTranslation.handler.ts b/packages/api/router/misc/query.getCountryTranslation.handler.ts
index 511e5e1059..b6d28f9820 100644
--- a/packages/api/router/misc/query.getCountryTranslation.handler.ts
+++ b/packages/api/router/misc/query.getCountryTranslation.handler.ts
@@ -10,7 +10,6 @@ export const getCountryTranslation = async ({ input }: TRPCHandlerParams val.toLocaleUpperCase()),
+ .transform((str) => str.toUpperCase()),
})
export type TGetCountryTranslationSchema = z.infer
diff --git a/packages/api/router/misc/schemas.ts b/packages/api/router/misc/schemas.ts
index d23f4da46b..7b1c664352 100644
--- a/packages/api/router/misc/schemas.ts
+++ b/packages/api/router/misc/schemas.ts
@@ -1,4 +1,5 @@
// codegen:start {preset: barrel, include: ./*.schema.ts}
+export * from './query.forEditNavbar.schema'
export * from './query.getCountryTranslation.schema'
export * from './query.hasContactInfo.schema'
// codegen:end
diff --git a/packages/api/router/orgEmail/index.ts b/packages/api/router/orgEmail/index.ts
index efb0114c8f..997a025bc9 100644
--- a/packages/api/router/orgEmail/index.ts
+++ b/packages/api/router/orgEmail/index.ts
@@ -9,6 +9,8 @@ type OrgEmailHandlerCache = {
upsertMany: typeof import('./mutation.upsertMany.handler').upsertMany
get: typeof import('./query.get.handler').get
forContactInfo: typeof import('./query.forContactInfo.handler').forContactInfo
+ forContactInfoEdit: typeof import('./query.forContactInfoEdit.handler').forContactInfoEdit
+ forEditDrawer: typeof import('./query.forEditDrawer.handler').forEditDrawer
}
export const orgEmailRouter = defineRouter({
create: permissionedProcedure('createNewEmail')
@@ -50,4 +52,24 @@ export const orgEmailRouter = defineRouter({
if (!HandlerCache.forContactInfo) throw new Error('Failed to load handler')
return HandlerCache.forContactInfo({ ctx, input })
}),
+ forContactInfoEdit: permissionedProcedure('updateEmail')
+ .input(schema.ZForContactInfoEditSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forContactInfoEdit)
+ HandlerCache.forContactInfoEdit = await import('./query.forContactInfoEdit.handler').then(
+ (mod) => mod.forContactInfoEdit
+ )
+ if (!HandlerCache.forContactInfoEdit) throw new Error('Failed to load handler')
+ return HandlerCache.forContactInfoEdit({ ctx, input })
+ }),
+ forEditDrawer: permissionedProcedure('updateEmail')
+ .input(schema.ZForEditDrawerSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forEditDrawer)
+ HandlerCache.forEditDrawer = await import('./query.forEditDrawer.handler').then(
+ (mod) => mod.forEditDrawer
+ )
+ if (!HandlerCache.forEditDrawer) throw new Error('Failed to load handler')
+ return HandlerCache.forEditDrawer({ ctx, input })
+ }),
})
diff --git a/packages/api/router/orgEmail/mutation.update.handler.ts b/packages/api/router/orgEmail/mutation.update.handler.ts
index 456dbbda11..b97915a65a 100644
--- a/packages/api/router/orgEmail/mutation.update.handler.ts
+++ b/packages/api/router/orgEmail/mutation.update.handler.ts
@@ -1,27 +1,54 @@
-import { prisma } from '@weareinreach/db'
-import { CreateAuditLog } from '~api/schemas/create/auditLog'
+import { generateNestedFreeTextUpsert, prisma, setAuditId } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'
import { type TUpdateSchema } from './mutation.update.schema'
export const update = async ({ ctx, input }: TRPCHandlerParams) => {
- const { where, data } = input
+ const { id, orgId, description, descriptionId, titleId, ...record } = input
+
+ const updatedDescription = description
+ ? generateNestedFreeTextUpsert({
+ orgId,
+ type: 'emailDesc',
+ itemId: id,
+ freeTextId: descriptionId,
+ text: description,
+ })
+ : undefined
+
const updatedRecord = await prisma.$transaction(async (tx) => {
- const current = await tx.orgEmail.findUniqueOrThrow({ where })
- const auditLogs = CreateAuditLog({
- actorId: ctx.session.user.id,
- operation: 'UPDATE',
- from: current,
- to: data,
- })
+ await setAuditId(ctx.actorId, tx)
+
const updated = await tx.orgEmail.update({
- where,
+ where: { id },
data: {
- ...data,
- auditLogs,
+ ...record,
+ description: updatedDescription,
+ title: titleId ? { connect: { id: titleId } } : undefined,
+ },
+ select: {
+ id: true,
+ deleted: true,
+ description: { select: { tsKey: { select: { text: true, key: true, ns: true } } } },
+ descriptionId: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ locationOnly: true,
+ primary: true,
+ published: true,
+ serviceOnly: true,
+ titleId: true,
},
})
- return updated
+ const { description, ...rest } = updated
+
+ const reformatted = {
+ ...rest,
+ description: description ? description.tsKey.text : null,
+ }
+
+ return reformatted
})
return updatedRecord
}
diff --git a/packages/api/router/orgEmail/mutation.update.schema.ts b/packages/api/router/orgEmail/mutation.update.schema.ts
index d466057c86..e5b5500255 100644
--- a/packages/api/router/orgEmail/mutation.update.schema.ts
+++ b/packages/api/router/orgEmail/mutation.update.schema.ts
@@ -1,24 +1,20 @@
import { z } from 'zod'
-import { Prisma } from '@weareinreach/db'
import { prefixedId } from '~api/schemas/idPrefix'
-export const ZUpdateSchema = z
- .object({
- id: prefixedId('orgEmail'),
- data: z
- .object({
- firstName: z.string(),
- lastName: z.string(),
- primary: z.boolean(),
- email: z.string(),
- published: z.boolean(),
- deleted: z.boolean(),
- titleId: prefixedId('userTitle'),
- locationOnly: z.boolean(),
- serviceOnly: z.boolean(),
- })
- .partial(),
- })
- .transform(({ data, id }) => Prisma.validator()({ where: { id }, data }))
+export const ZUpdateSchema = z.object({
+ id: prefixedId('orgEmail'),
+ orgId: prefixedId('organization'),
+ firstName: z.string().nullish(),
+ lastName: z.string().nullish(),
+ primary: z.boolean().optional(),
+ email: z.string().optional(),
+ published: z.boolean().optional(),
+ deleted: z.boolean().optional(),
+ titleId: prefixedId('userTitle').nullish(),
+ locationOnly: z.boolean().optional(),
+ serviceOnly: z.boolean().optional(),
+ description: z.string().nullish(),
+ descriptionId: z.string().nullish(),
+})
export type TUpdateSchema = z.infer
diff --git a/packages/api/router/orgEmail/query.forContactInfo.schema.ts b/packages/api/router/orgEmail/query.forContactInfo.schema.ts
index 37333e8559..f4fa0dc4db 100644
--- a/packages/api/router/orgEmail/query.forContactInfo.schema.ts
+++ b/packages/api/router/orgEmail/query.forContactInfo.schema.ts
@@ -3,7 +3,7 @@ import { z } from 'zod'
import { prefixedId } from '~api/schemas/idPrefix'
export const ZForContactInfoSchema = z.object({
- parentId: z.union([prefixedId('organization'), prefixedId('orgLocation'), prefixedId('orgService')]),
+ parentId: prefixedId(['organization', 'orgLocation', 'orgService']),
locationOnly: z.boolean().optional(),
serviceOnly: z.boolean().optional(),
})
diff --git a/packages/api/router/orgEmail/query.forContactInfoEdit.handler.ts b/packages/api/router/orgEmail/query.forContactInfoEdit.handler.ts
new file mode 100644
index 0000000000..ee7b8c89ea
--- /dev/null
+++ b/packages/api/router/orgEmail/query.forContactInfoEdit.handler.ts
@@ -0,0 +1,52 @@
+import { isIdFor, type Prisma, prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForContactInfoEditSchema } from './query.forContactInfoEdit.schema'
+
+const whereId = (input: TForContactInfoEditSchema): Prisma.OrgEmailWhereInput => {
+ switch (true) {
+ case isIdFor('organization', input.parentId): {
+ return { organization: { some: { organization: { id: input.parentId } } } }
+ }
+ case isIdFor('orgLocation', input.parentId): {
+ return { locations: { some: { location: { id: input.parentId } } } }
+ }
+ case isIdFor('orgService', input.parentId): {
+ return { services: { some: { service: { id: input.parentId } } } }
+ }
+ default: {
+ return {}
+ }
+ }
+}
+
+export const forContactInfoEdit = async ({ ctx, input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgEmail.findMany({
+ where: {
+ ...whereId(input),
+ },
+ select: {
+ id: true,
+ email: true,
+ primary: true,
+ title: { select: { key: { select: { key: true } } } },
+ description: { select: { tsKey: { select: { text: true, key: true } } } },
+ locationOnly: true,
+ serviceOnly: true,
+ published: true,
+ deleted: true,
+ },
+ orderBy: [{ published: 'desc' }, { deleted: 'asc' }],
+ })
+ const transformed = result.map(({ description, title, ...record }) => ({
+ ...record,
+ title: title ? { key: title?.key.key } : null,
+ description: description ? { key: description?.tsKey.key, defaultText: description?.tsKey.text } : null,
+ }))
+ return transformed
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgEmail/query.forContactInfoEdit.schema.ts b/packages/api/router/orgEmail/query.forContactInfoEdit.schema.ts
new file mode 100644
index 0000000000..f45523ff6d
--- /dev/null
+++ b/packages/api/router/orgEmail/query.forContactInfoEdit.schema.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForContactInfoEditSchema = z.object({
+ parentId: prefixedId(['organization', 'orgLocation', 'orgService']),
+})
+export type TForContactInfoEditSchema = z.infer
diff --git a/packages/api/router/orgEmail/query.forEditDrawer.handler.ts b/packages/api/router/orgEmail/query.forEditDrawer.handler.ts
new file mode 100644
index 0000000000..7eb8d2aaef
--- /dev/null
+++ b/packages/api/router/orgEmail/query.forEditDrawer.handler.ts
@@ -0,0 +1,37 @@
+import { prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForEditDrawerSchema } from './query.forEditDrawer.schema'
+
+export const forEditDrawer = async ({ ctx, input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgEmail.findUniqueOrThrow({
+ where: input,
+ select: {
+ id: true,
+ deleted: true,
+ description: { select: { tsKey: { select: { text: true, key: true, ns: true } } } },
+ descriptionId: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ locationOnly: true,
+ primary: true,
+ published: true,
+ serviceOnly: true,
+ titleId: true,
+ },
+ })
+ const { description, ...rest } = result
+
+ const reformatted = {
+ ...rest,
+ description: description ? description.tsKey.text : null,
+ }
+
+ return reformatted
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgEmail/query.forEditDrawer.schema.ts b/packages/api/router/orgEmail/query.forEditDrawer.schema.ts
new file mode 100644
index 0000000000..65dd6df82a
--- /dev/null
+++ b/packages/api/router/orgEmail/query.forEditDrawer.schema.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForEditDrawerSchema = z.object({ id: prefixedId('orgEmail') })
+export type TForEditDrawerSchema = z.infer
diff --git a/packages/api/router/orgEmail/schemas.ts b/packages/api/router/orgEmail/schemas.ts
index 39890d6f7b..25e30dd020 100644
--- a/packages/api/router/orgEmail/schemas.ts
+++ b/packages/api/router/orgEmail/schemas.ts
@@ -3,5 +3,7 @@ export * from './mutation.create.schema'
export * from './mutation.update.schema'
export * from './mutation.upsertMany.schema'
export * from './query.forContactInfo.schema'
+export * from './query.forContactInfoEdit.schema'
+export * from './query.forEditDrawer.schema'
export * from './query.get.schema'
// codegen:end
diff --git a/packages/api/router/orgHours/query.forHoursDisplay.handler.ts b/packages/api/router/orgHours/query.forHoursDisplay.handler.ts
index 3b1edd6421..69e44152c6 100644
--- a/packages/api/router/orgHours/query.forHoursDisplay.handler.ts
+++ b/packages/api/router/orgHours/query.forHoursDisplay.handler.ts
@@ -2,6 +2,7 @@ import groupBy from 'just-group-by'
import { DateTime, Interval } from 'luxon'
import { isIdFor, prisma, type Prisma } from '@weareinreach/db'
+import { convertToLuxonWeekday } from '@weareinreach/util/luxon/weekday'
import { globalWhere } from '~api/selects/global'
import { type TRPCHandlerParams } from '~api/types/handler'
@@ -38,12 +39,12 @@ export const forHoursDisplay = async ({ input }: TRPCHandlerParams {
const interval = Interval.fromDateTimes(
DateTime.fromJSDate(start, { zone: tz ?? 'America/New_York' }).set({
- weekday: dayIndex,
+ weekday: convertToLuxonWeekday(dayIndex),
weekYear,
weekNumber,
}),
DateTime.fromJSDate(end, { zone: tz ?? 'America/New_York' }).set({
- weekday: start > end ? dayIndex + 1 : dayIndex,
+ weekday: convertToLuxonWeekday(start > end ? dayIndex + 1 : dayIndex),
weekYear,
weekNumber,
})
diff --git a/packages/api/router/orgHours/query.forHoursDrawer.handler.ts b/packages/api/router/orgHours/query.forHoursDrawer.handler.ts
index cece533075..542f9a069e 100644
--- a/packages/api/router/orgHours/query.forHoursDrawer.handler.ts
+++ b/packages/api/router/orgHours/query.forHoursDrawer.handler.ts
@@ -1,6 +1,7 @@
import { DateTime, Interval } from 'luxon'
import { isIdFor, prisma, type Prisma } from '@weareinreach/db'
+import { convertToLuxonWeekday } from '@weareinreach/util/luxon/weekday'
import { type TRPCHandlerParams } from '~api/types/handler'
import { type TForHoursDrawerSchema } from './query.forHoursDrawer.schema'
@@ -45,12 +46,12 @@ export const forHoursDrawer = async ({ input }: TRPCHandlerParams {
+ if (!HandlerCache.forEditDrawer)
+ HandlerCache.forEditDrawer = await import('./query.forEditDrawer.handler').then(
+ (mod) => mod.forEditDrawer
+ )
+ if (!HandlerCache.forEditDrawer) throw new Error('Failed to load handler')
+ return HandlerCache.forEditDrawer({ ctx, input })
+ }),
+ forContactInfoEdit: permissionedProcedure('updatePhone')
+ .input(schema.ZForContactInfoEditSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forContactInfoEdit)
+ HandlerCache.forContactInfoEdit = await import('./query.forContactInfoEdit.handler').then(
+ (mod) => mod.forContactInfoEdit
+ )
+ if (!HandlerCache.forContactInfoEdit) throw new Error('Failed to load handler')
+ return HandlerCache.forContactInfoEdit({ ctx, input })
+ }),
})
diff --git a/packages/api/router/orgPhone/mutation.update.handler.ts b/packages/api/router/orgPhone/mutation.update.handler.ts
index ec6e18ef0e..c8797d7a40 100644
--- a/packages/api/router/orgPhone/mutation.update.handler.ts
+++ b/packages/api/router/orgPhone/mutation.update.handler.ts
@@ -1,27 +1,43 @@
-import { prisma } from '@weareinreach/db'
-import { CreateAuditLog } from '~api/schemas/create/auditLog'
+import { generateFreeText, getAuditedClient } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'
import { type TUpdateSchema } from './mutation.update.schema'
export const update = async ({ ctx, input }: TRPCHandlerParams) => {
- const { where, data } = input
- const updatedRecord = await prisma.$transaction(async (tx) => {
- const current = await tx.orgPhone.findUniqueOrThrow({ where })
- const auditLogs = CreateAuditLog({
- actorId: ctx.session.user.id,
- operation: 'UPDATE',
- from: current,
- to: data,
- })
- const updated = await tx.orgPhone.update({
- where,
- data: {
- ...data,
- auditLogs,
- },
- })
- return updated
+ const prisma = getAuditedClient(ctx.actorId)
+ const { id, orgId, description, countryId, phoneTypeId, ...rest } = input
+
+ const textData = description
+ ? generateFreeText({ orgId, type: 'phoneDesc', text: description, itemId: id })
+ : undefined
+
+ const updatedRecord = await prisma.orgPhone.update({
+ where: { id },
+ data: {
+ ...rest,
+ ...(textData
+ ? {
+ description: {
+ upsert: {
+ create: {
+ id: textData.freeText.id,
+ tsKey: {
+ connectOrCreate: {
+ where: { ns_key: { ns: textData.translationKey.ns, key: textData.translationKey.key } },
+ create: textData.translationKey,
+ },
+ },
+ },
+ update: { tsKey: { update: { text: textData.translationKey.text } } },
+ },
+ },
+ }
+ : description === null
+ ? { description: { delete: true } }
+ : {}),
+ ...(countryId ? { country: { connect: { id: countryId } } } : {}),
+ ...(phoneTypeId ? { phoneType: { connect: { id: phoneTypeId } } } : {}),
+ },
})
return updatedRecord
}
diff --git a/packages/api/router/orgPhone/mutation.update.schema.ts b/packages/api/router/orgPhone/mutation.update.schema.ts
index 42090693c9..6a29fa0ca8 100644
--- a/packages/api/router/orgPhone/mutation.update.schema.ts
+++ b/packages/api/router/orgPhone/mutation.update.schema.ts
@@ -1,23 +1,19 @@
import { z } from 'zod'
-import { Prisma } from '@weareinreach/db'
import { prefixedId } from '~api/schemas/idPrefix'
-export const ZUpdateSchema = z
- .object({
- id: prefixedId('orgPhone'),
- data: z
- .object({
- number: z.string(),
- ext: z.string(),
- primary: z.boolean(),
- published: z.boolean(),
- deleted: z.boolean(),
- countryId: prefixedId('country'),
- phoneTypeId: prefixedId('phoneType'),
- locationOnly: z.boolean(),
- })
- .partial(),
- })
- .transform(({ data, id }) => Prisma.validator()({ where: { id }, data }))
+export const ZUpdateSchema = z.object({
+ id: prefixedId('orgPhone'),
+ orgId: prefixedId('organization'),
+ number: z.string().optional(),
+ ext: z.string().nullish(),
+ primary: z.boolean().optional(),
+ published: z.boolean().optional(),
+ deleted: z.boolean().optional(),
+ countryId: prefixedId('country').optional(),
+ phoneTypeId: prefixedId('phoneType').nullish(),
+ locationOnly: z.boolean().optional(),
+ serviceOnly: z.boolean().optional(),
+ description: z.string().nullish(),
+})
export type TUpdateSchema = z.infer
diff --git a/packages/api/router/orgPhone/query.forContactInfo.schema.ts b/packages/api/router/orgPhone/query.forContactInfo.schema.ts
index 78411bcf3c..84b0607218 100644
--- a/packages/api/router/orgPhone/query.forContactInfo.schema.ts
+++ b/packages/api/router/orgPhone/query.forContactInfo.schema.ts
@@ -3,7 +3,7 @@ import { z } from 'zod'
import { prefixedId } from '~api/schemas/idPrefix'
export const ZForContactInfoSchema = z.object({
- parentId: z.union([prefixedId('organization'), prefixedId('orgLocation'), prefixedId('orgService')]),
+ parentId: prefixedId(['organization', 'orgLocation', 'orgService']),
locationOnly: z.boolean().optional(),
})
export type TForContactInfoSchema = z.infer
diff --git a/packages/api/router/orgPhone/query.forContactInfoEdit.handler.ts b/packages/api/router/orgPhone/query.forContactInfoEdit.handler.ts
new file mode 100644
index 0000000000..5ff2ebb1be
--- /dev/null
+++ b/packages/api/router/orgPhone/query.forContactInfoEdit.handler.ts
@@ -0,0 +1,66 @@
+import { isIdFor, type Prisma, prisma } from '@weareinreach/db'
+import { type TForContactInfoSchema } from '~api/router/orgEmail/query.forContactInfo.schema'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForContactInfoEditSchema } from './query.forContactInfoEdit.schema'
+
+const getWhereId = (input: TForContactInfoEditSchema, isSingleLoc?: boolean): Prisma.OrgPhoneWhereInput => {
+ switch (true) {
+ case isIdFor('organization', input.parentId): {
+ return isSingleLoc
+ ? {
+ OR: [
+ { organization: { organization: { id: input.parentId } } },
+ { locations: { some: { location: { organization: { id: input.parentId } } } } },
+ ],
+ }
+ : { organization: { organization: { id: input.parentId } } }
+ }
+ case isIdFor('orgLocation', input.parentId): {
+ return { locations: { some: { location: { id: input.parentId } } } }
+ }
+ case isIdFor('orgService', input.parentId): {
+ return { services: { some: { service: { id: input.parentId } } } }
+ }
+ default: {
+ return {}
+ }
+ }
+}
+
+export const forContactInfoEdit = async ({ input }: TRPCHandlerParams) => {
+ const locCount = isIdFor('organization', input.parentId)
+ ? await prisma.orgLocation.count({
+ where: { organization: { id: input.parentId } },
+ })
+ : 0
+ const isSingleLoc = locCount === 1
+
+ const whereId = getWhereId(input, isSingleLoc)
+
+ const result = await prisma.orgPhone.findMany({
+ where: {
+ ...whereId,
+ },
+ select: {
+ id: true,
+ number: true,
+ ext: true,
+ country: { select: { cca2: true } },
+ primary: true,
+ description: { select: { tsKey: { select: { text: true, key: true } } } },
+ phoneType: { select: { key: { select: { text: true, key: true } } } },
+ locationOnly: true,
+ published: true,
+ deleted: true,
+ },
+ orderBy: [{ published: 'desc' }, { deleted: 'asc' }],
+ })
+ const transformed = result.map(({ description, phoneType, country, ...record }) => ({
+ ...record,
+ country: country?.cca2,
+ phoneType: phoneType ? { key: phoneType?.key.key, defaultText: phoneType?.key.text } : null,
+ description: description ? { key: description?.tsKey.key, defaultText: description?.tsKey.text } : null,
+ }))
+ return transformed
+}
diff --git a/packages/api/router/orgPhone/query.forContactInfoEdit.schema.ts b/packages/api/router/orgPhone/query.forContactInfoEdit.schema.ts
new file mode 100644
index 0000000000..f45523ff6d
--- /dev/null
+++ b/packages/api/router/orgPhone/query.forContactInfoEdit.schema.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForContactInfoEditSchema = z.object({
+ parentId: prefixedId(['organization', 'orgLocation', 'orgService']),
+})
+export type TForContactInfoEditSchema = z.infer
diff --git a/packages/api/router/orgPhone/query.forEditDrawer.handler.ts b/packages/api/router/orgPhone/query.forEditDrawer.handler.ts
new file mode 100644
index 0000000000..883d98123c
--- /dev/null
+++ b/packages/api/router/orgPhone/query.forEditDrawer.handler.ts
@@ -0,0 +1,69 @@
+import { prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForEditDrawerSchema } from './query.forEditDrawer.schema'
+
+const getOrgId = async (phoneId: string) => {
+ const result = await prisma.orgPhone.findUniqueOrThrow({
+ where: { id: phoneId },
+ select: {
+ organization: { select: { organizationId: true } },
+ locations: { select: { location: { select: { orgId: true } } } },
+ services: { select: { service: { select: { organizationId: true } } } },
+ },
+ })
+
+ switch (true) {
+ case !!result.organization?.organizationId: {
+ return result.organization.organizationId
+ }
+ case result.locations.length !== 0 &&
+ result.locations.filter((loc) => !!loc.location.orgId).length !== 0: {
+ const filtered = result.locations.filter((loc) => !!loc.location.orgId)
+ return filtered[0]!.location.orgId
+ }
+ case result.services.length !== 0 &&
+ result.services.filter((serv) => !!serv.service.organizationId).length !== 0: {
+ const filtered = result.services.filter((serv) => !!serv.service.organizationId)
+ return filtered[0]!.service.organizationId!
+ }
+ default: {
+ throw new Error('Unable to get organizationId')
+ }
+ }
+}
+
+export const forEditDrawer = async ({ input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgPhone.findUniqueOrThrow({
+ where: input,
+ select: {
+ id: true,
+ number: true,
+ ext: true,
+ primary: true,
+ published: true,
+ deleted: true,
+ countryId: true,
+ phoneTypeId: true,
+ description: { select: { id: true, key: true, tsKey: { select: { text: true } } } },
+ locationOnly: true,
+ serviceOnly: true,
+ country: { select: { cca2: true } },
+ },
+ })
+ const orgId = await getOrgId(input.id)
+ const { country, description, ...rest } = result
+
+ const reformatted = {
+ ...rest,
+ description: description ? description?.tsKey?.text : null,
+ orgId,
+ country: country?.cca2,
+ }
+ return reformatted
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgPhone/query.forEditDrawer.schema.ts b/packages/api/router/orgPhone/query.forEditDrawer.schema.ts
new file mode 100644
index 0000000000..778f0f3668
--- /dev/null
+++ b/packages/api/router/orgPhone/query.forEditDrawer.schema.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForEditDrawerSchema = z.object({ id: prefixedId('orgPhone') })
+export type TForEditDrawerSchema = z.infer
diff --git a/packages/api/router/orgPhone/schemas.ts b/packages/api/router/orgPhone/schemas.ts
index 39890d6f7b..25e30dd020 100644
--- a/packages/api/router/orgPhone/schemas.ts
+++ b/packages/api/router/orgPhone/schemas.ts
@@ -3,5 +3,7 @@ export * from './mutation.create.schema'
export * from './mutation.update.schema'
export * from './mutation.upsertMany.schema'
export * from './query.forContactInfo.schema'
+export * from './query.forContactInfoEdit.schema'
+export * from './query.forEditDrawer.schema'
export * from './query.get.schema'
// codegen:end
diff --git a/packages/api/router/orgSocialMedia/index.ts b/packages/api/router/orgSocialMedia/index.ts
index 687dfb3cdf..d68e82a9d3 100644
--- a/packages/api/router/orgSocialMedia/index.ts
+++ b/packages/api/router/orgSocialMedia/index.ts
@@ -7,6 +7,9 @@ type OrgSocialMediaHandlerCache = {
create: typeof import('./mutation.create.handler').create
update: typeof import('./mutation.update.handler').update
forContactInfo: typeof import('./query.forContactInfo.handler').forContactInfo
+ forContactInfoEdits: typeof import('./query.forContactInfoEdits.handler').forContactInfoEdits
+ getServiceTypes: typeof import('./query.getServiceTypes.handler').getServiceTypes
+ forEditDrawer: typeof import('./query.forEditDrawer.handler').forEditDrawer
}
export const orgSocialMediaRouter = defineRouter({
create: permissionedProcedure('createNewSocial')
@@ -33,4 +36,34 @@ export const orgSocialMediaRouter = defineRouter({
if (!HandlerCache.forContactInfo) throw new Error('Failed to load handler')
return HandlerCache.forContactInfo({ ctx, input })
}),
+ forContactInfoEdits: permissionedProcedure('updateSocialMedia')
+ .input(schema.ZForContactInfoEditsSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forContactInfoEdits)
+ HandlerCache.forContactInfoEdits = await import('./query.forContactInfoEdits.handler').then(
+ (mod) => mod.forContactInfoEdits
+ )
+ if (!HandlerCache.forContactInfoEdits) throw new Error('Failed to load handler')
+ return HandlerCache.forContactInfoEdits({ ctx, input })
+ }),
+ getServiceTypes: permissionedProcedure('updateSocialMedia')
+ .input(schema.ZGetServiceTypesSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.getServiceTypes)
+ HandlerCache.getServiceTypes = await import('./query.getServiceTypes.handler').then(
+ (mod) => mod.getServiceTypes
+ )
+ if (!HandlerCache.getServiceTypes) throw new Error('Failed to load handler')
+ return HandlerCache.getServiceTypes({ ctx, input })
+ }),
+ forEditDrawer: permissionedProcedure('updateSocialMedia')
+ .input(schema.ZForEditDrawerSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forEditDrawer)
+ HandlerCache.forEditDrawer = await import('./query.forEditDrawer.handler').then(
+ (mod) => mod.forEditDrawer
+ )
+ if (!HandlerCache.forEditDrawer) throw new Error('Failed to load handler')
+ return HandlerCache.forEditDrawer({ ctx, input })
+ }),
})
diff --git a/packages/api/router/orgSocialMedia/mutation.update.handler.ts b/packages/api/router/orgSocialMedia/mutation.update.handler.ts
index 1b2c52017a..137a13ff18 100644
--- a/packages/api/router/orgSocialMedia/mutation.update.handler.ts
+++ b/packages/api/router/orgSocialMedia/mutation.update.handler.ts
@@ -1,27 +1,29 @@
-import { prisma } from '@weareinreach/db'
-import { CreateAuditLog } from '~api/schemas/create/auditLog'
+import { getAuditedClient } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'
import { type TUpdateSchema } from './mutation.update.schema'
export const update = async ({ ctx, input }: TRPCHandlerParams) => {
const { where, data } = input
- const updatedRecord = await prisma.$transaction(async (tx) => {
- const current = await tx.orgSocialMedia.findUniqueOrThrow({ where })
- const auditLogs = CreateAuditLog({
- actorId: ctx.session.user.id,
- operation: 'UPDATE',
- from: current,
- to: data,
- })
- const updated = await tx.orgSocialMedia.update({
- where,
- data: {
- ...data,
- auditLogs,
+ const prisma = getAuditedClient(ctx.actorId)
+
+ const updated = await prisma.orgSocialMedia.update({
+ where,
+ data,
+ select: {
+ id: true,
+ username: true,
+ url: true,
+ deleted: true,
+ published: true,
+ serviceId: true,
+ organizationId: true,
+ orgLocationId: true,
+ orgLocationOnly: true,
+ service: {
+ select: { id: true, name: true, logoIcon: true },
},
- })
- return updated
+ },
})
- return updatedRecord
+ return updated
}
diff --git a/packages/api/router/orgSocialMedia/mutation.update.schema.ts b/packages/api/router/orgSocialMedia/mutation.update.schema.ts
index c87f08e25f..5dc8d374d9 100644
--- a/packages/api/router/orgSocialMedia/mutation.update.schema.ts
+++ b/packages/api/router/orgSocialMedia/mutation.update.schema.ts
@@ -13,8 +13,8 @@ export const ZUpdateSchema = z
published: z.boolean(),
deleted: z.boolean(),
serviceId: z.string(),
- organizationId: prefixedId('organization'),
- orgLocationId: prefixedId('orgLocation'),
+ organizationId: prefixedId('organization').nullable(),
+ orgLocationId: prefixedId('orgLocation').nullable(),
orgLocationOnly: z.boolean(),
})
.partial(),
diff --git a/packages/api/router/orgSocialMedia/query.forContactInfo.handler.ts b/packages/api/router/orgSocialMedia/query.forContactInfo.handler.ts
index 11eb9e70e3..3567cdbaf8 100644
--- a/packages/api/router/orgSocialMedia/query.forContactInfo.handler.ts
+++ b/packages/api/router/orgSocialMedia/query.forContactInfo.handler.ts
@@ -44,13 +44,14 @@ export const forContactInfo = async ({ input }: TRPCHandlerParams ({
...record,
service: service?.name,
+ serviceIcon: service?.logoIcon,
}))
return transformed
}
diff --git a/packages/api/router/orgSocialMedia/query.forContactInfo.schema.ts b/packages/api/router/orgSocialMedia/query.forContactInfo.schema.ts
index 36af3dfd1d..228ddc783f 100644
--- a/packages/api/router/orgSocialMedia/query.forContactInfo.schema.ts
+++ b/packages/api/router/orgSocialMedia/query.forContactInfo.schema.ts
@@ -3,7 +3,7 @@ import { z } from 'zod'
import { prefixedId } from '~api/schemas/idPrefix'
export const ZForContactInfoSchema = z.object({
- parentId: z.union([prefixedId('organization'), prefixedId('orgLocation')]),
+ parentId: prefixedId(['organization', 'orgLocation']),
locationOnly: z.boolean().optional(),
})
export type TForContactInfoSchema = z.infer
diff --git a/packages/api/router/orgSocialMedia/query.forContactInfoEdits.handler.ts b/packages/api/router/orgSocialMedia/query.forContactInfoEdits.handler.ts
new file mode 100644
index 0000000000..93f5331b9e
--- /dev/null
+++ b/packages/api/router/orgSocialMedia/query.forContactInfoEdits.handler.ts
@@ -0,0 +1,43 @@
+import { isIdFor, prisma, type Prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForContactInfoEditsSchema } from './query.forContactInfoEdits.schema'
+
+const whereId = (input: TForContactInfoEditsSchema): Prisma.OrgSocialMediaWhereInput => {
+ switch (true) {
+ case isIdFor('organization', input.parentId): {
+ return { organization: { id: input.parentId } }
+ }
+ case isIdFor('orgLocation', input.parentId): {
+ return { orgLocation: { id: input.parentId } }
+ }
+ default: {
+ return {}
+ }
+ }
+}
+export const forContactInfoEdits = async ({ input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgSocialMedia.findMany({
+ where: whereId(input),
+ select: {
+ id: true,
+ url: true,
+ username: true,
+ service: { select: { name: true, logoIcon: true } },
+ orgLocationOnly: true,
+ published: true,
+ deleted: true,
+ },
+ })
+ const transformed = result.map(({ service, ...record }) => ({
+ ...record,
+ service: service?.name,
+ serviceIcon: service?.logoIcon,
+ }))
+ return transformed
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgSocialMedia/query.forContactInfoEdits.schema.ts b/packages/api/router/orgSocialMedia/query.forContactInfoEdits.schema.ts
new file mode 100644
index 0000000000..9fe6756581
--- /dev/null
+++ b/packages/api/router/orgSocialMedia/query.forContactInfoEdits.schema.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForContactInfoEditsSchema = z.object({
+ parentId: prefixedId(['organization', 'orgLocation']),
+})
+export type TForContactInfoEditsSchema = z.infer
diff --git a/packages/api/router/orgSocialMedia/query.forEditDrawer.handler.ts b/packages/api/router/orgSocialMedia/query.forEditDrawer.handler.ts
new file mode 100644
index 0000000000..3d2fcfc35c
--- /dev/null
+++ b/packages/api/router/orgSocialMedia/query.forEditDrawer.handler.ts
@@ -0,0 +1,30 @@
+import { prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForEditDrawerSchema } from './query.forEditDrawer.schema'
+
+export const forEditDrawer = async ({ ctx, input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgSocialMedia.findUniqueOrThrow({
+ where: input,
+ select: {
+ id: true,
+ username: true,
+ url: true,
+ deleted: true,
+ published: true,
+ serviceId: true,
+ organizationId: true,
+ orgLocationId: true,
+ orgLocationOnly: true,
+ service: {
+ select: { id: true, name: true, logoIcon: true },
+ },
+ },
+ })
+ return result
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgSocialMedia/query.forEditDrawer.schema.ts b/packages/api/router/orgSocialMedia/query.forEditDrawer.schema.ts
new file mode 100644
index 0000000000..36bf638f7a
--- /dev/null
+++ b/packages/api/router/orgSocialMedia/query.forEditDrawer.schema.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForEditDrawerSchema = z.object({ id: prefixedId('orgSocialMedia') })
+export type TForEditDrawerSchema = z.infer
diff --git a/packages/api/router/orgSocialMedia/query.getServiceTypes.handler.ts b/packages/api/router/orgSocialMedia/query.getServiceTypes.handler.ts
new file mode 100644
index 0000000000..2c4e38e193
--- /dev/null
+++ b/packages/api/router/orgSocialMedia/query.getServiceTypes.handler.ts
@@ -0,0 +1,23 @@
+import { prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TGetServiceTypesSchema } from './query.getServiceTypes.schema'
+
+export const getServiceTypes = async ({ ctx, input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.socialMediaService.findMany({
+ where: input,
+ select: {
+ id: true,
+ active: true,
+ logoIcon: true,
+ name: true,
+ urlBase: true,
+ },
+ })
+ return result
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgSocialMedia/query.getServiceTypes.schema.ts b/packages/api/router/orgSocialMedia/query.getServiceTypes.schema.ts
new file mode 100644
index 0000000000..6b61901b93
--- /dev/null
+++ b/packages/api/router/orgSocialMedia/query.getServiceTypes.schema.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod'
+
+export const ZGetServiceTypesSchema = z
+ .object({ active: z.boolean(), internal: z.boolean() })
+ .partial()
+ .default({ active: true, internal: false })
+export type TGetServiceTypesSchema = z.infer
diff --git a/packages/api/router/orgSocialMedia/schemas.ts b/packages/api/router/orgSocialMedia/schemas.ts
index 57e4fa8f25..97058e6434 100644
--- a/packages/api/router/orgSocialMedia/schemas.ts
+++ b/packages/api/router/orgSocialMedia/schemas.ts
@@ -2,4 +2,7 @@
export * from './mutation.create.schema'
export * from './mutation.update.schema'
export * from './query.forContactInfo.schema'
+export * from './query.forContactInfoEdits.schema'
+export * from './query.forEditDrawer.schema'
+export * from './query.getServiceTypes.schema'
// codegen:end
diff --git a/packages/api/router/orgWebsite/index.ts b/packages/api/router/orgWebsite/index.ts
index c180c06f1a..9912a25e38 100644
--- a/packages/api/router/orgWebsite/index.ts
+++ b/packages/api/router/orgWebsite/index.ts
@@ -7,6 +7,8 @@ type OrgWebsiteHandlerCache = {
create: typeof import('./mutation.create.handler').create
update: typeof import('./mutation.update.handler').update
forContactInfo: typeof import('./query.forContactInfo.handler').forContactInfo
+ forEditDrawer: typeof import('./query.forEditDrawer.handler').forEditDrawer
+ forContactInfoEdit: typeof import('./query.forContactInfoEdit.handler').forContactInfoEdit
}
export const orgWebsiteRouter = defineRouter({
create: permissionedProcedure('createOrgWebsite')
@@ -33,4 +35,24 @@ export const orgWebsiteRouter = defineRouter({
if (!HandlerCache.forContactInfo) throw new Error('Failed to load handler')
return HandlerCache.forContactInfo({ ctx, input })
}),
+ forEditDrawer: permissionedProcedure('updateOrgWebsite')
+ .input(schema.ZForEditDrawerSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forEditDrawer)
+ HandlerCache.forEditDrawer = await import('./query.forEditDrawer.handler').then(
+ (mod) => mod.forEditDrawer
+ )
+ if (!HandlerCache.forEditDrawer) throw new Error('Failed to load handler')
+ return HandlerCache.forEditDrawer({ ctx, input })
+ }),
+ forContactInfoEdit: permissionedProcedure('updateOrgWebsite')
+ .input(schema.ZForContactInfoEditSchema)
+ .query(async ({ ctx, input }) => {
+ if (!HandlerCache.forContactInfoEdit)
+ HandlerCache.forContactInfoEdit = await import('./query.forContactInfoEdit.handler').then(
+ (mod) => mod.forContactInfoEdit
+ )
+ if (!HandlerCache.forContactInfoEdit) throw new Error('Failed to load handler')
+ return HandlerCache.forContactInfoEdit({ ctx, input })
+ }),
})
diff --git a/packages/api/router/orgWebsite/mutation.update.handler.ts b/packages/api/router/orgWebsite/mutation.update.handler.ts
index bfa43e3f12..a12f54f8a4 100644
--- a/packages/api/router/orgWebsite/mutation.update.handler.ts
+++ b/packages/api/router/orgWebsite/mutation.update.handler.ts
@@ -1,27 +1,14 @@
-import { prisma } from '@weareinreach/db'
-import { CreateAuditLog } from '~api/schemas/create/auditLog'
+import { getAuditedClient } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'
import { type TUpdateSchema } from './mutation.update.schema'
export const update = async ({ ctx, input }: TRPCHandlerParams) => {
const { where, data } = input
- const updatedRecord = await prisma.$transaction(async (tx) => {
- const current = await tx.orgWebsite.findUniqueOrThrow({ where })
- const auditLogs = CreateAuditLog({
- actorId: ctx.session.user.id,
- operation: 'UPDATE',
- from: current,
- to: data,
- })
- const updated = await tx.orgWebsite.update({
- where,
- data: {
- ...data,
- auditLogs,
- },
- })
- return updated
+ const prisma = getAuditedClient(ctx.actorId)
+ const updated = await prisma.orgWebsite.update({
+ where,
+ data,
})
- return updatedRecord
+ return updated
}
diff --git a/packages/api/router/orgWebsite/query.forContactInfo.schema.ts b/packages/api/router/orgWebsite/query.forContactInfo.schema.ts
index 36af3dfd1d..228ddc783f 100644
--- a/packages/api/router/orgWebsite/query.forContactInfo.schema.ts
+++ b/packages/api/router/orgWebsite/query.forContactInfo.schema.ts
@@ -3,7 +3,7 @@ import { z } from 'zod'
import { prefixedId } from '~api/schemas/idPrefix'
export const ZForContactInfoSchema = z.object({
- parentId: z.union([prefixedId('organization'), prefixedId('orgLocation')]),
+ parentId: prefixedId(['organization', 'orgLocation']),
locationOnly: z.boolean().optional(),
})
export type TForContactInfoSchema = z.infer
diff --git a/packages/api/router/orgWebsite/query.forContactInfoEdit.handler.ts b/packages/api/router/orgWebsite/query.forContactInfoEdit.handler.ts
new file mode 100644
index 0000000000..f2358ecff4
--- /dev/null
+++ b/packages/api/router/orgWebsite/query.forContactInfoEdit.handler.ts
@@ -0,0 +1,44 @@
+import { isIdFor, prisma, type Prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForContactInfoEditSchema } from './query.forContactInfoEdit.schema'
+
+const whereId = (input: TForContactInfoEditSchema): Prisma.OrgWebsiteWhereInput => {
+ switch (true) {
+ case isIdFor('organization', input.parentId): {
+ return { organization: { id: input.parentId } }
+ }
+ case isIdFor('orgLocation', input.parentId): {
+ return { orgLocation: { id: input.parentId } }
+ }
+
+ default: {
+ return {}
+ }
+ }
+}
+export const forContactInfoEdit = async ({ ctx, input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgWebsite.findMany({
+ where: {
+ ...whereId(input),
+ },
+ select: {
+ id: true,
+ url: true,
+ description: { select: { tsKey: { select: { text: true, key: true } } } },
+ published: true,
+ deleted: true,
+ },
+ orderBy: [{ published: 'desc' }, { deleted: 'asc' }],
+ })
+ const transformed = result.map(({ description, ...record }) => ({
+ ...record,
+ description: description ? { key: description?.tsKey.key, defaultText: description?.tsKey.text } : null,
+ }))
+ return transformed
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgWebsite/query.forContactInfoEdit.schema.ts b/packages/api/router/orgWebsite/query.forContactInfoEdit.schema.ts
new file mode 100644
index 0000000000..89f8de59b6
--- /dev/null
+++ b/packages/api/router/orgWebsite/query.forContactInfoEdit.schema.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForContactInfoEditSchema = z.object({
+ parentId: prefixedId(['organization', 'orgLocation']),
+})
+export type TForContactInfoEditSchema = z.infer
diff --git a/packages/api/router/orgWebsite/query.forEditDrawer.handler.ts b/packages/api/router/orgWebsite/query.forEditDrawer.handler.ts
new file mode 100644
index 0000000000..d1f868338d
--- /dev/null
+++ b/packages/api/router/orgWebsite/query.forEditDrawer.handler.ts
@@ -0,0 +1,23 @@
+import { prisma } from '@weareinreach/db'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForEditDrawerSchema } from './query.forEditDrawer.schema'
+
+export const forEditDrawer = async ({ input }: TRPCHandlerParams) => {
+ try {
+ const result = await prisma.orgWebsite.findUniqueOrThrow({
+ where: input,
+ include: {
+ description: { include: { tsKey: true } },
+ },
+ })
+ const reformatted = {
+ ...result,
+ description: result.description?.tsKey?.text,
+ }
+ return reformatted
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/orgWebsite/query.forEditDrawer.schema.ts b/packages/api/router/orgWebsite/query.forEditDrawer.schema.ts
new file mode 100644
index 0000000000..f62a286958
--- /dev/null
+++ b/packages/api/router/orgWebsite/query.forEditDrawer.schema.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZForEditDrawerSchema = z.object({ id: prefixedId('orgWebsite') })
+export type TForEditDrawerSchema = z.infer
diff --git a/packages/api/router/orgWebsite/schemas.ts b/packages/api/router/orgWebsite/schemas.ts
index 57e4fa8f25..c0f965271c 100644
--- a/packages/api/router/orgWebsite/schemas.ts
+++ b/packages/api/router/orgWebsite/schemas.ts
@@ -2,4 +2,6 @@
export * from './mutation.create.schema'
export * from './mutation.update.schema'
export * from './query.forContactInfo.schema'
+export * from './query.forContactInfoEdit.schema'
+export * from './query.forEditDrawer.schema'
// codegen:end
diff --git a/packages/api/router/organization/index.ts b/packages/api/router/organization/index.ts
index 3c27ba3648..69e0035adb 100644
--- a/packages/api/router/organization/index.ts
+++ b/packages/api/router/organization/index.ts
@@ -25,6 +25,7 @@ type OrganizationHandlerCache = {
getIntlCrisis: typeof import('./query.getIntlCrisis.handler').getIntlCrisis
getNatlCrisis: typeof import('./query.getNatlCrisis.handler').getNatlCrisis
forOrganizationTable: typeof import('./query.forOrganizationTable.handler').forOrganizationTable
+ forOrgPageEdits: typeof import('./query.forOrgPageEdits.handler').forOrgPageEdits
getAttributes: typeof import('./query.getAttributes.handler').getAttributes
getAlerts: typeof import('./query.getAlerts.handler').getAlerts
// #endregion
@@ -36,6 +37,7 @@ type OrganizationHandlerCache = {
createNewQuick: typeof import('./mutation.createNewQuick.handler').createNewQuick
createNewSuggestion: typeof import('./mutation.createNewSuggestion.handler').createNewSuggestion
attachAttribute: typeof import('./mutation.attachAttribute.handler').attachAttribute
+ updateBasic: typeof import('./mutation.updateBasic.handler').updateBasic
// #endregion
}
@@ -176,6 +178,15 @@ export const orgRouter = defineRouter({
if (!HandlerCache.forOrganizationTable) throw new Error('Failed to load handler')
return HandlerCache.forOrganizationTable({ ctx, input })
}),
+ forOrgPageEdits: publicProcedure.input(schema.ZForOrgPageEditsSchema).query(async ({ ctx, input }) => {
+ if (!HandlerCache.forOrgPageEdits)
+ HandlerCache.forOrgPageEdits = await import('./query.forOrgPageEdits.handler').then(
+ (mod) => mod.forOrgPageEdits
+ )
+
+ if (!HandlerCache.forOrgPageEdits) throw new Error('Failed to load handler')
+ return HandlerCache.forOrgPageEdits({ ctx, input })
+ }),
getAttributes: publicProcedure.input(schema.ZGetAttributesSchema).query(async ({ ctx, input }) => {
if (!HandlerCache.getAttributes)
HandlerCache.getAttributes = await import('./query.getAttributes.handler').then(
@@ -228,6 +239,15 @@ export const orgRouter = defineRouter({
if (!HandlerCache.attachAttribute) throw new Error('Failed to load handler')
return HandlerCache.attachAttribute({ ctx, input })
}),
-
+ updateBasic: permissionedProcedure('createNewOrgQuick')
+ .input(schema.ZUpdateBasicSchema)
+ .mutation(async ({ ctx, input }) => {
+ if (!HandlerCache.updateBasic)
+ HandlerCache.updateBasic = await import('./mutation.updateBasic.handler').then(
+ (mod) => mod.updateBasic
+ )
+ if (!HandlerCache.updateBasic) throw new Error('Failed to load handler')
+ return HandlerCache.updateBasic({ ctx, input })
+ }),
// #endregion
})
diff --git a/packages/api/router/organization/lib.ts b/packages/api/router/organization/lib.ts
deleted file mode 100644
index 1ac24328e2..0000000000
--- a/packages/api/router/organization/lib.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-// import slugify from 'slugify'
-
-// import { type Context } from '~api/lib/context'
-
-// export const uniqueSlug = async (ctx: Context, name: string, inc?: number): Promise => {
-// try {
-// const check = async (slug: string) => {
-// const existing = await ctx.prisma.organization.findUnique({
-// where: {
-// slug,
-// },
-// select: {
-// slug: true,
-// },
-// })
-// return !existing?.slug
-// }
-// const generatedSlug = slugify(inc ? `${name} ${inc}` : name, { lower: true, strict: true, trim: true })
-// const isUnique = await check(generatedSlug)
-// if (isUnique) return generatedSlug
-// else return await uniqueSlug(ctx, name, (inc ?? 0) + 1)
-// } catch (error) {
-// console.error(error)
-// throw error
-// }
-// }
diff --git a/packages/api/router/organization/mutation.updateBasic.handler.ts b/packages/api/router/organization/mutation.updateBasic.handler.ts
new file mode 100644
index 0000000000..f1809df7f8
--- /dev/null
+++ b/packages/api/router/organization/mutation.updateBasic.handler.ts
@@ -0,0 +1,66 @@
+import { crowdinApi, getStringIdByKey, projectId } from '@weareinreach/crowdin/api'
+import {
+ generateFreeText,
+ generateId,
+ generateUniqueSlug,
+ getAuditedClient,
+ type Prisma,
+} from '@weareinreach/db'
+import { isVercelProd } from '@weareinreach/env'
+import { createLoggerInstance } from '@weareinreach/util/logger'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TUpdateBasicSchema } from './mutation.updateBasic.schema'
+
+const logger = createLoggerInstance('api - organization.updateBasic')
+export const updateBasic = async ({ ctx, input }: TRPCHandlerParams) => {
+ try {
+ const prisma = getAuditedClient(ctx.actorId)
+ const data: Prisma.OrganizationUpdateInput = {}
+ const existing = await prisma.organization.findUniqueOrThrow({
+ where: { id: input.id },
+ select: { slug: true, description: { select: { tsKey: { select: { crowdinId: true, key: true } } } } },
+ })
+
+ if (input.name) {
+ const newSlug = await generateUniqueSlug({ name: input.name, id: input.id })
+ data.name = input.name
+ data.slug = newSlug
+ data.oldSlugs = { create: { from: existing.slug, to: newSlug, id: generateId('slugRedirect') } }
+ }
+ if (input.description) {
+ // TODO: [IN-920] Handle new string creation in Crowdin
+ const newText = generateFreeText({ orgId: input.id, type: 'orgDesc', text: input.description })
+ data.description = {
+ upsert: {
+ update: { tsKey: { update: { text: input.description } } },
+ create: { id: newText.freeText.id, tsKey: { create: newText.translationKey } },
+ },
+ }
+ }
+ const update = await prisma.organization.update({
+ where: { id: input.id },
+ data,
+ })
+ if (update && input.description && existing.description) {
+ const stringId =
+ existing.description.tsKey.crowdinId ||
+ (await getStringIdByKey(existing.description?.tsKey.key, true))
+ if (stringId) {
+ if (isVercelProd) {
+ await crowdinApi.sourceStringsApi.editString(projectId, stringId, [
+ { op: 'replace', path: '/text', value: input.description },
+ ])
+ } else {
+ logger.info(
+ `\n==========\nSkipping Crowdin Update - Not production environment.\nCrowdin String ID: ${stringId}. Updated Description: ${input.description}\n==========`
+ )
+ }
+ }
+ }
+ return update
+ } catch (error) {
+ handleError(error)
+ }
+}
diff --git a/packages/api/router/organization/mutation.updateBasic.schema.ts b/packages/api/router/organization/mutation.updateBasic.schema.ts
new file mode 100644
index 0000000000..9e06891231
--- /dev/null
+++ b/packages/api/router/organization/mutation.updateBasic.schema.ts
@@ -0,0 +1,11 @@
+import { z } from 'zod'
+
+import { prefixedId } from '~api/schemas/idPrefix'
+
+export const ZUpdateBasicSchema = z.object({
+ id: prefixedId('organization'),
+ name: z.string().optional(),
+ description: z.string().optional(),
+})
+
+export type TUpdateBasicSchema = z.infer
diff --git a/packages/api/router/organization/query.forOrgPageEdits.handler.ts b/packages/api/router/organization/query.forOrgPageEdits.handler.ts
new file mode 100644
index 0000000000..7d68ca3709
--- /dev/null
+++ b/packages/api/router/organization/query.forOrgPageEdits.handler.ts
@@ -0,0 +1,46 @@
+import { prisma } from '@weareinreach/db'
+import { attributes, freeText } from '~api/schemas/selects/common'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TForOrgPageEditsSchema } from './query.forOrgPageEdits.schema'
+
+export const forOrgPageEdits = async ({ input }: TRPCHandlerParams) => {
+ const { slug } = input
+ const org = await prisma.organization.findUniqueOrThrow({
+ where: {
+ slug,
+ },
+ select: {
+ id: true,
+ name: true,
+ slug: true,
+ published: true,
+ deleted: true,
+ lastVerified: true,
+ allowedEditors: { where: { authorized: true }, select: { userId: true } },
+ description: freeText,
+ attributes,
+ reviews: {
+ select: { id: true },
+ },
+ locations: {
+ select: {
+ id: true,
+ street1: true,
+ street2: true,
+ city: true,
+ postCode: true,
+ country: { select: { cca2: true } },
+ govDist: { select: { abbrev: true, tsKey: true, tsNs: true } },
+ },
+ },
+ },
+ })
+ const { allowedEditors, ...orgData } = org
+ const reformatted = {
+ ...orgData,
+ isClaimed: Boolean(allowedEditors.length),
+ }
+
+ return reformatted
+}
diff --git a/packages/api/router/organization/query.forOrgPageEdits.schema.ts b/packages/api/router/organization/query.forOrgPageEdits.schema.ts
new file mode 100644
index 0000000000..e508a676cc
--- /dev/null
+++ b/packages/api/router/organization/query.forOrgPageEdits.schema.ts
@@ -0,0 +1,4 @@
+import { z } from 'zod'
+
+export const ZForOrgPageEditsSchema = z.object({ slug: z.string() })
+export type TForOrgPageEditsSchema = z.infer
diff --git a/packages/api/router/organization/query.searchDistance.handler.ts b/packages/api/router/organization/query.searchDistance.handler.ts
index b3a6d8ec28..87eb74ee1e 100644
--- a/packages/api/router/organization/query.searchDistance.handler.ts
+++ b/packages/api/router/organization/query.searchDistance.handler.ts
@@ -78,6 +78,7 @@ service_area as (
SELECT
country."geoDataId" as "countryGeoId",
district."geoDataId" as "districtGeoId",
+ district.slug AS "districtSlug",
country.cca3 as "cca3",
sa."organizationId",
sa."orgLocationId"
@@ -112,7 +113,9 @@ service_area as (
ST_Distance(ST_Transform(loc.geo, 3857), (SELECT meters FROM points))::int
)
) AS distance,
- ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3), NULL) AS "national"
+ ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3), NULL) AS "national",
+ ARRAY_LENGTH(ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3), NULL),1) is not NULL AS "isNational",
+ ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3) || ARRAY_AGG( DISTINCT sa."districtSlug"), NULL) AS "serviceAreas"
FROM "OrgLocation" loc
INNER JOIN "Organization" org ON org.id = loc. "orgId"
LEFT JOIN service_area sa ON sa. "organizationId" = loc. "orgId"
@@ -165,9 +168,10 @@ type SearchResult = {
matchedAttributes?: string[]
distance: string
national: string[]
+ isNational: boolean
+ serviceAreas: string[]
total: string
}
-
const prismaDistSearchDetails = async (input: TSearchDistanceSchema & { resultIds: string[] }) => {
const { resultIds, lat: latitude, lon: longitude } = input
const results = await prisma.organization.findMany({
diff --git a/packages/api/router/organization/schemas.ts b/packages/api/router/organization/schemas.ts
index f0cccf2c7b..b4882ce871 100644
--- a/packages/api/router/organization/schemas.ts
+++ b/packages/api/router/organization/schemas.ts
@@ -2,10 +2,12 @@
export * from './mutation.attachAttribute.schema'
export * from './mutation.createNewQuick.schema'
export * from './mutation.createNewSuggestion.schema'
+export * from './mutation.updateBasic.schema'
export * from './query.checkForExisting.schema'
export * from './query.forLocationPage.schema'
export * from './query.forOrganizationTable.schema'
export * from './query.forOrgPage.schema'
+export * from './query.forOrgPageEdits.schema'
export * from './query.generateSlug.schema'
export * from './query.getAlerts.schema'
export * from './query.getAttributes.schema'
diff --git a/packages/api/router/service/query.forServiceInfoCard.schema.ts b/packages/api/router/service/query.forServiceInfoCard.schema.ts
index 09f03d7ffb..38db5f94cb 100644
--- a/packages/api/router/service/query.forServiceInfoCard.schema.ts
+++ b/packages/api/router/service/query.forServiceInfoCard.schema.ts
@@ -3,7 +3,7 @@ import { z } from 'zod'
import { prefixedId } from '~api/schemas/idPrefix'
export const ZForServiceInfoCardSchema = z.object({
- parentId: prefixedId('organization').or(prefixedId('orgLocation')),
+ parentId: prefixedId(['organization', 'orgLocation']),
remoteOnly: z.boolean().optional(),
})
export type TForServiceInfoCardSchema = z.infer
diff --git a/packages/api/schemas/idPrefix.ts b/packages/api/schemas/idPrefix.ts
index da27f13186..82b4f0b41c 100644
--- a/packages/api/schemas/idPrefix.ts
+++ b/packages/api/schemas/idPrefix.ts
@@ -2,4 +2,9 @@ import { z } from 'zod'
import { type IdPrefix, idPrefix } from '@weareinreach/db/lib/idGen'
-export const prefixedId = (model: IdPrefix) => z.string().regex(new RegExp(`^${idPrefix[model]}_\\w+$`))
+export const prefixedId = (model: IdPrefix | IdPrefix[]) => {
+ const regEx = Array.isArray(model)
+ ? new RegExp(`^(?:${model.map((m) => idPrefix[m]).join('|')})_\\w+$`)
+ : new RegExp(`^${idPrefix[model]}_\\w+$`)
+ return z.string().regex(regEx)
+}
diff --git a/packages/db/index.ts b/packages/db/index.ts
index d8f376a950..4651f4c117 100644
--- a/packages/db/index.ts
+++ b/packages/db/index.ts
@@ -5,6 +5,10 @@ export * from './zod_util'
export { setAuditId } from './lib/audit'
export { slug, generateUniqueSlug } from './lib/slugGen'
export { createPoint, createPointOrNull, createGeoFields } from './lib/createPoint'
-export { generateFreeText, generateNestedFreeText } from './lib/generateFreeText'
+export {
+ generateFreeText,
+ generateNestedFreeText,
+ generateNestedFreeTextUpsert,
+} from './lib/generateFreeText'
export { generateId, getIdPrefixRegex, isIdFor } from './lib/idGen'
export { PrismaInstrumentation } from '@prisma/instrumentation'
diff --git a/packages/db/lib/generateFreeText.ts b/packages/db/lib/generateFreeText.ts
index 4390412970..65aabeb1ab 100644
--- a/packages/db/lib/generateFreeText.ts
+++ b/packages/db/lib/generateFreeText.ts
@@ -39,7 +39,7 @@ export const generateFreeText = ({
}
})()
const ns = namespaces.orgData
- if (!key) throw new Error('Error creating key')
+ invariant(key, 'Error creating key')
return {
translationKey: Prisma.validator()({ key, text, ns }),
freeText: Prisma.validator()({
@@ -78,7 +78,7 @@ export const generateNestedFreeText = ({
}
}
})()
- invariant(key)
+ invariant(key, 'Error creating key')
const ns = namespaces.orgData
return {
create: {
@@ -88,11 +88,64 @@ export const generateNestedFreeText = ({
}
}
+export const generateNestedFreeTextUpsert = ({
+ orgId: orgSlug,
+ itemId,
+ text,
+ type,
+ freeTextId,
+}: GenerateFreeTextParams) => {
+ const key = (() => {
+ switch (type) {
+ case 'orgDesc': {
+ return createKey([orgSlug, 'description'])
+ }
+ case 'attSupp': {
+ invariant(itemId)
+ return createKey([orgSlug, 'attribute', itemId])
+ }
+ case 'svcName': {
+ invariant(itemId)
+ return createKey([orgSlug, itemId, 'name'])
+ }
+ case 'websiteDesc':
+ case 'phoneDesc':
+ case 'emailDesc':
+ case 'svcDesc': {
+ invariant(itemId)
+ return createKey([orgSlug, itemId, 'description'])
+ }
+ }
+ })()
+ invariant(key, 'Error creating key')
+ const ns = namespaces.orgData
+
+ const id = freeTextId ?? generateId('freeText')
+ return {
+ upsert: {
+ where: { id },
+ create: {
+ id,
+ tsKey: { create: { key, text, namespace: { connect: { name: ns } } } },
+ },
+ update: {
+ tsKey: {
+ upsert: {
+ where: { key },
+ create: { key, text, namespace: { connect: { name: ns } } },
+ update: { text },
+ },
+ },
+ },
+ },
+ }
+}
+
type GenerateFreeTextParams = GenerateFreeTextWithItem
interface GenerateFreeTextBase {
text: string
orgId: string
- freeTextId?: string
+ freeTextId?: string | null
}
type GenerateFreeTextType =
diff --git a/packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/!prep.ts b/packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/!prep.ts
new file mode 100644
index 0000000000..1cd8777a00
--- /dev/null
+++ b/packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/!prep.ts
@@ -0,0 +1,168 @@
+import {
+ findPhoneNumbersInText,
+ isSupportedCountry,
+ parsePhoneNumber,
+ parsePhoneNumberFromString,
+ searchPhoneNumbersInText,
+} from 'libphonenumber-js'
+
+import fs from 'fs'
+import path from 'path'
+
+import { prisma } from '~db/client'
+
+function lettersToPhoneNumber(input: string) {
+ // convert lowercase string to uppercase since we compare against uppercase;
+ // then split string into an array of letters;
+ const searchStr = input.toUpperCase().split('')
+
+ // create virtual keypad, associating alpha characters with a single digit;
+ const keyPads = [
+ {
+ alpha: ['A', 'B', 'C'],
+ digit: 2,
+ },
+ {
+ alpha: ['D', 'E', 'F'],
+ digit: 3,
+ },
+ {
+ alpha: ['G', 'H', 'I'],
+ digit: 4,
+ },
+ {
+ alpha: ['J', 'K', 'L'],
+ digit: 5,
+ },
+ {
+ alpha: ['M', 'N', 'O'],
+ digit: 6,
+ },
+ {
+ alpha: ['P', 'Q', 'R', 'S'],
+ digit: 7,
+ },
+ {
+ alpha: ['T', 'U', 'V'],
+ digit: 8,
+ },
+ {
+ alpha: ['W', 'X', 'Y', 'Z'],
+ digit: 9,
+ },
+ // {
+ // alpha: [' '],
+ // digit: 0,
+ // },
+ ]
+
+ // declare our results as an array;
+ const result: (number | undefined)[] = []
+
+ // iterate over each letter of the string
+ searchStr.forEach((letter) => {
+ // filters the virtual keypads for the current letter;
+ // since we're creating a single digit array,
+ // we immediately select [0].digit to return the digit corresponding with the letter;
+ const testDigit = !isNaN(parseInt(letter))
+ if (testDigit) {
+ result.push(parseInt(letter))
+ return
+ } else if (['-', '(', ')'].includes(letter)) {
+ return
+ }
+ const digit = keyPads.filter((keyPad) => keyPad.alpha.indexOf(letter) > -1)[0]?.digit
+
+ // using the spread operator, we concatenate a new array;
+ result.push(digit)
+ })
+
+ // we return the numbers as a number using the Array.prototype.join method nested within parseInt;
+ return parseInt(result.join(''))
+}
+
+const run = async () => {
+ const output: Output[] = []
+ const rejected: Exception[] = []
+ const exceptions: Exception[] = []
+ const data = await prisma.orgPhone.findMany({
+ include: {
+ country: { select: { cca2: true } },
+ organization: { select: { organization: { select: { name: true, id: true } } } },
+ locations: { select: { location: { select: { country: { select: { cca2: true } } } } } },
+ },
+ })
+
+ for (const item of data) {
+ const country = isSupportedCountry(item.country.cca2) ? item.country.cca2 : undefined
+ const letters2digits = lettersToPhoneNumber(item.number)
+ try {
+ const parsed =
+ // parsePhoneNumber(item.number, country) ||
+ findPhoneNumbersInText(letters2digits.toString(), { defaultCountry: country })
+ if (parsed.length) {
+ for (const phone of parsed) {
+ if (phone.number.isValid()) {
+ output.push({
+ id: item.id,
+ number: phone.number.nationalNumber,
+ countryCode: phone.number.countryCallingCode,
+ })
+ } else {
+ rejected.push({ id: item.id, number: phone.number.number, country: item.country.cca2 })
+ }
+ }
+ } else {
+ const parsed = parsePhoneNumber(item.number, country)
+ const isValid = parsed.isValid() //isValidNumberForRegion(parsed.nationalNumber, parsed.country)
+ if (parsed && isValid) {
+ output.push({ id: item.id, number: parsed.nationalNumber, countryCode: parsed.countryCallingCode })
+ } else {
+ rejected.push({
+ id: item.id,
+ number: item.number,
+ country: item.country.cca2,
+ orgName: item.organization?.organization.name,
+ orgId: item.organization?.organization.id,
+ locations: item.locations.map((l) => l.location.country.cca2),
+ })
+ }
+ }
+ } catch (e) {
+ console.error(e)
+ const x = findPhoneNumbersInText(item.number, { defaultCountry: country })
+ const letters2digits = lettersToPhoneNumber(item.number)
+ rejected.push({
+ id: item.id,
+ number: item.number,
+ country: item.country.cca2,
+ orgName: item.organization?.organization.name,
+ orgId: item.organization?.organization.id,
+ locations: item.locations.map((l) => l.location.country.cca2),
+ })
+ }
+ }
+ for (const item of rejected) {
+ console.log(item)
+ }
+
+ fs.writeFileSync(path.resolve(__dirname, './output.json'), JSON.stringify(output))
+ fs.writeFileSync(path.resolve(__dirname, './exceptions.json'), JSON.stringify(rejected))
+}
+
+run()
+
+type Output = {
+ id: string
+ number: string
+ countryCode: string
+}
+
+type Exception = {
+ id: string
+ number: string
+ country: string
+ orgName?: string
+ orgId?: string
+ locations?: string[]
+}
diff --git a/packages/db/prisma/data-migrations/!YYYY-MM-DD_job-template.ts b/packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/index.ts
similarity index 76%
rename from packages/db/prisma/data-migrations/!YYYY-MM-DD_job-template.ts
rename to packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/index.ts
index fd0c4b91a3..60f5fa94bb 100644
--- a/packages/db/prisma/data-migrations/!YYYY-MM-DD_job-template.ts
+++ b/packages/db/prisma/data-migrations/2023-09-28_phone-number-normalization/index.ts
@@ -5,22 +5,14 @@ import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun'
/** Define the job metadata here. */
const jobDef: JobDef = {
- jobId: 'yyyy-mm-dd-shortDescription',
- title: 'Descriptive Title',
- createdBy: 'Your Name',
+ jobId: '2023-09-28-phone-number-normalization',
+ title: 'phone number normalization',
+ createdBy: 'Joe Karow',
/** Optional: Longer description for the job */
description: undefined,
}
-/**
- * Job export - this variable MUST be UNIQUE
- *
- * Use the format `jobYYYYMMDD` and append a letter afterwards if there is already a job with this name.
- *
- * @example `job20230404`
- *
- * @example `job20230404b`
- */
-export const jobYYYYmmDD = {
+/** Job export - this variable MUST be UNIQUE */
+export const job20230928_phone_number_normalization = {
title: `[${jobDef.jobId}] ${jobDef.title}`,
task: async (_ctx, task) => {
/** Create logging instance */
diff --git a/packages/db/prisma/data-migrations/index.ts b/packages/db/prisma/data-migrations/index.ts
index 2aab52e39c..f1a5da56d3 100644
--- a/packages/db/prisma/data-migrations/index.ts
+++ b/packages/db/prisma/data-migrations/index.ts
@@ -53,6 +53,7 @@ export * from './2023-09-20_fix-access-instructions/index'
export * from './2023-09-20_update-services'
export * from './2023-09-22_service-areas'
export * from './2023-09-22_update-alerts/index'
+export * from './2023-09-28_phone-number-normalization/index'
export * from './2023-10-23_add-suggested-orgs/index'
export * from './2023-10-25_service-tag-updates'
export * from './2023-10-27_tlc-service-updates'
diff --git a/packages/db/prisma/views/public/pg_cache_hit_rate.sql b/packages/db/prisma/views/public/pg_cache_hit_rate.sql
new file mode 100644
index 0000000000..9eb8e150ee
--- /dev/null
+++ b/packages/db/prisma/views/public/pg_cache_hit_rate.sql
@@ -0,0 +1,10 @@
+SELECT
+ sum(pg_statio_user_tables.heap_blks_read) AS heap_read,
+ sum(pg_statio_user_tables.heap_blks_hit) AS heap_hit,
+ (
+ (
+ sum(pg_statio_user_tables.heap_blks_hit) - sum(pg_statio_user_tables.heap_blks_read)
+ ) / sum(pg_statio_user_tables.heap_blks_hit)
+ ) AS ratio
+FROM
+ pg_statio_user_tables;
\ No newline at end of file
diff --git a/packages/db/prisma/views/public/pg_index_usage.sql b/packages/db/prisma/views/public/pg_index_usage.sql
new file mode 100644
index 0000000000..309375a916
--- /dev/null
+++ b/packages/db/prisma/views/public/pg_index_usage.sql
@@ -0,0 +1,49 @@
+SELECT
+ t.tablename AS relation,
+ foo.indexname,
+ c.reltuples AS num_rows,
+ pg_size_pretty(
+ pg_relation_size((quote_ident((t.tablename) :: text)) :: regclass)
+ ) AS table_size,
+ pg_size_pretty(
+ pg_relation_size(
+ (quote_ident((foo.indexrelname) :: text)) :: regclass
+ )
+ ) AS index_size,
+ foo.idx_scan AS number_of_scans,
+ foo.idx_tup_read AS tuples_read,
+ foo.idx_tup_fetch AS tuples_fetched
+FROM
+ (
+ (
+ pg_tables t
+ LEFT JOIN pg_class c ON ((t.tablename = c.relname))
+ )
+ LEFT JOIN (
+ SELECT
+ c_1.relname AS ctablename,
+ ipg.relname AS indexname,
+ x.indnatts AS number_of_columns,
+ psai.idx_scan,
+ psai.idx_tup_read,
+ psai.idx_tup_fetch,
+ psai.indexrelname,
+ x.indisunique
+ FROM
+ (
+ (
+ (
+ pg_index x
+ JOIN pg_class c_1 ON ((c_1.oid = x.indrelid))
+ )
+ JOIN pg_class ipg ON ((ipg.oid = x.indexrelid))
+ )
+ JOIN pg_stat_all_indexes psai ON ((x.indexrelid = psai.indexrelid))
+ )
+ ) foo ON ((t.tablename = foo.ctablename))
+ )
+WHERE
+ (t.schemaname = 'public' :: name)
+ORDER BY
+ t.tablename,
+ foo.indexname;
\ No newline at end of file
diff --git a/packages/db/prisma/views/public/pg_index_usage_rate.sql b/packages/db/prisma/views/public/pg_index_usage_rate.sql
new file mode 100644
index 0000000000..2dd4f1aecc
--- /dev/null
+++ b/packages/db/prisma/views/public/pg_index_usage_rate.sql
@@ -0,0 +1,12 @@
+SELECT
+ pg_stat_user_tables.relname,
+ (
+ (100 * pg_stat_user_tables.idx_scan) / (
+ pg_stat_user_tables.seq_scan + pg_stat_user_tables.idx_scan
+ )
+ ) AS percent_of_times_index_used,
+ pg_stat_user_tables.n_live_tup AS rows_in_table
+FROM
+ pg_stat_user_tables
+ORDER BY
+ pg_stat_user_tables.n_live_tup DESC;
\ No newline at end of file
diff --git a/packages/env/index.ts b/packages/env/index.ts
index 0dd6092a7b..d27a30ebf7 100644
--- a/packages/env/index.ts
+++ b/packages/env/index.ts
@@ -89,3 +89,6 @@ export const env = createEnv({
})
export const getEnv = (envVar: T): (typeof env)[T] => env[envVar]
+
+export const isDev = process.env.NODE_ENV === 'development'
+export const isVercelProd = process.env.VERCEL_ENV === 'production'
diff --git a/packages/ui/.storybook/i18next.ts b/packages/ui/.storybook/i18next.ts
index 2e6c32a836..6d4d03215d 100644
--- a/packages/ui/.storybook/i18next.ts
+++ b/packages/ui/.storybook/i18next.ts
@@ -1,5 +1,7 @@
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
+// @ts-expect-error It is a valid package..
+import { HMRPlugin } from 'i18next-hmr/plugin'
import HttpApi, { type HttpBackendOptions } from 'i18next-http-backend'
import intervalPlural from 'i18next-intervalplural-postprocessor'
import { initReactI18next } from 'react-i18next'
@@ -15,6 +17,7 @@ i18n
.use(LanguageDetector)
.use(HttpApi)
.use(initReactI18next)
+ .use(new HMRPlugin({ webpack: { client: true } }))
.init({
...config,
debug: false,
diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts
index 9212ba4197..e069661de1 100644
--- a/packages/ui/.storybook/main.ts
+++ b/packages/ui/.storybook/main.ts
@@ -2,6 +2,8 @@
import { type StorybookConfig } from '@storybook/nextjs'
import isChromatic from 'chromatic/isChromatic'
import dotenv from 'dotenv'
+// @ts-expect-error It is a valid package..
+import { I18NextHMRPlugin } from 'i18next-hmr/webpack'
import { mergeAndConcat } from 'merge-anything'
import { type PropItem } from 'react-docgen-typescript'
@@ -38,6 +40,7 @@ const config: StorybookConfig = {
getAbsolutePath('@storybook/addon-designs'),
getAbsolutePath('storybook-addon-pseudo-states'),
getAbsolutePath('@storybook/addon-interactions'),
+ '@storybook/addon-webpack5-compiler-swc',
],
framework: {
name: '@storybook/nextjs',
@@ -89,16 +92,25 @@ const config: StorybookConfig = {
'mockAuthStates.ts'
),
'next-i18next': 'react-i18next',
+ 'msw/native': path.resolve(__dirname, '../node_modules/msw/lib/native/index.mjs'),
},
- roots: Array.isArray(config.resolve?.roots)
- ? [...config.resolve.roots, publicStatic]
- : [publicStatic],
+ roots: [publicStatic],
},
stats: {
colors: true,
},
devtool: options.configType === 'DEVELOPMENT' ? 'eval-source-map' : undefined,
}
+
+ /** I18 HMR */
+ if (options.configType === 'DEVELOPMENT') {
+ const plugin = new I18NextHMRPlugin({
+ localesDir: path.resolve(__dirname, '../../../apps/app/public/locales'),
+ })
+
+ Array.isArray(config.plugins) ? config.plugins.push(plugin) : (config.plugins = [plugin])
+ }
+
const mergedConfig = mergeAndConcat(config, configAdditions)
return mergedConfig
},
@@ -109,6 +121,9 @@ const config: StorybookConfig = {
? {
SKIP_ENV_VALIDATION: 'true',
}
- : { NEXT_PUBLIC_GOOGLE_MAPS_API: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API as string },
+ : {
+ NEXT_PUBLIC_GOOGLE_MAPS_API: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API as string,
+ STORYBOOK_PROJECT_ROOT: path.resolve(__dirname, '../'),
+ },
}
export default config
diff --git a/packages/ui/.swcrc b/packages/ui/.swcrc
index bfa87d4f85..fa29756c88 100644
--- a/packages/ui/.swcrc
+++ b/packages/ui/.swcrc
@@ -12,10 +12,10 @@
"refresh": true
}
},
- "target": "es2020",
"loose": false,
- "externalHelpers": false,
+ "externalHelpers": true,
"keepClassNames": false
},
- "minify": false
+ "minify": false,
+ "sourceMaps": true
}
diff --git a/packages/ui/.vscode/settings.json b/packages/ui/.vscode/settings.json
index 53e471d98d..a2c3de751c 100644
--- a/packages/ui/.vscode/settings.json
+++ b/packages/ui/.vscode/settings.json
@@ -1,3 +1,6 @@
{
- "i18n-ally.localesPaths": "../../apps/app/public/locales"
+ "i18n-ally.localesPaths": "../../apps/app/public/locales",
+ "[json]": {
+ "editor.codeActionsOnSave": { "source.fixAll": "never" }
+ }
}
diff --git a/packages/ui/babel.config.json b/packages/ui/babel.config.json
deleted file mode 100644
index 962d9e03e8..0000000000
--- a/packages/ui/babel.config.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "sourceType": "unambiguous",
- "presets": [
- "@babel/preset-env",
- "@babel/preset-typescript",
- [
- "@babel/preset-react",
- {
- "runtime": "automatic",
- "useSpread": true
- }
- ]
- ],
- "plugins": [],
- "targets": {
- "chrome": 100,
- "esmodules": true
- }
-}
diff --git a/packages/ui/components/core/Badge.tsx b/packages/ui/components/core/Badge.tsx
index b9d193f6ac..9578c8996e 100644
--- a/packages/ui/components/core/Badge.tsx
+++ b/packages/ui/components/core/Badge.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/display-name */
import {
type BadgeProps,
type BadgeStylesNames,
@@ -476,7 +475,7 @@ export const Badge = forwardRef {
const variants = useCustomVariant()
const theme = useMantineTheme()
diff --git a/packages/ui/components/core/Breadcrumb.tsx b/packages/ui/components/core/Breadcrumb.tsx
index 6f3005bbca..0962b3d1d5 100644
--- a/packages/ui/components/core/Breadcrumb.tsx
+++ b/packages/ui/components/core/Breadcrumb.tsx
@@ -35,7 +35,7 @@ const useStyles = createStyles((theme) => ({
const isString = (...args: unknown[]) => args.every((val) => typeof val === 'string')
export const Breadcrumb = (props: BreadcrumbProps) => {
- const { option, backTo, backToText, onClick } = props
+ const { option, backTo, backToText, onClick, children } = props
const { classes } = useStyles()
const theme = useMantineTheme()
const { t } = useTranslation('common')
@@ -125,7 +125,7 @@ export const Breadcrumb = (props: BreadcrumbProps) => {
className={classes.icon}
/>
- {childrenRender}
+ {children || childrenRender}
@@ -150,7 +150,7 @@ type PossibleBreadcrumbProps = {
export type ModalTitleBreadcrumb = Omit & {
onClick?: MouseEventHandler
}
-export type BreadcrumbProps = Close | Back | BackToDynamic
+export type BreadcrumbProps = (Close | Back | BackToDynamic) & { children?: React.ReactNode }
interface Close {
option: 'close'
onClick: MouseEventHandler
diff --git a/packages/ui/components/data-display/ContactInfo.tsx b/packages/ui/components/data-display/ContactInfo.tsx
deleted file mode 100644
index cd30b4432c..0000000000
--- a/packages/ui/components/data-display/ContactInfo.tsx
+++ /dev/null
@@ -1,310 +0,0 @@
-import { Stack, Text, Title } from '@mantine/core'
-import { useTranslation } from 'next-i18next'
-import { type JSX } from 'react'
-
-import { type ApiOutput } from '@weareinreach/api'
-import { isExternal, Link } from '~ui/components/core/Link'
-import { isSocialIcon, SocialLink, type SocialLinkProps } from '~ui/components/core/SocialLink'
-import { parsePhoneNumber, useCustomVariant, useSlug } from '~ui/hooks'
-import { trpc as api } from '~ui/lib/trpcClient'
-
-const PhoneNumbers = ({ parentId = '', passedData, direct, locationOnly }: PhoneNumbersProps) => {
- const output: JSX.Element[] = []
- const slug = useSlug()
- const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
- const { t } = useTranslation(orgId?.id ? ['common', 'phone-type', orgId.id] : ['common', 'phone-type'])
- const variants = useCustomVariant()
- const { data } = api.orgPhone.forContactInfo.useQuery({ parentId, locationOnly }, { enabled: !passedData })
- let k = 0
-
- const componentData = passedData ? passedData : data
-
- if (!componentData?.length) return null
-
- for (const phone of componentData) {
- const { country, ext, locationOnly: showLocationOnly, number, phoneType, primary, description } = phone
- const parsedPhone = parsePhoneNumber(number, country)
- if (!parsedPhone) continue
- if (ext) parsedPhone.setExt(ext)
- const dialURL = parsedPhone.getURI()
- const phoneNumber = parsedPhone.formatNational()
- if (direct) {
- return (
-
- {t('direct.phone')}
- {isExternal(dialURL) ? (
-
- {phoneNumber}
-
- ) : (
- {phoneNumber}
- )}
-
- )
- }
- if (locationOnly && !showLocationOnly) continue
- const desc = description
- ? t(description.key, { ns: orgId?.id, defaultValue: description.defaultText })
- : phoneType?.key
- ? t(phoneType.key, { ns: 'phone-type' })
- : undefined
-
- const item = (
-
- {isExternal(dialURL) ? (
-
- {phoneNumber}
-
- ) : (
- {phoneNumber}
- )}
- {desc && {desc}}
-
- )
- primary ? output.unshift(item) : output.push(item)
- k++
- }
- return (
-
- {t('words.phone')}
- {output}
-
- )
-}
-
-const Emails = ({ parentId = '', passedData, direct, locationOnly, serviceOnly }: EmailsProps) => {
- const output: JSX.Element[] = []
- const slug = useSlug()
- const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
- const { t } = useTranslation(orgId?.id ? ['common', orgId.id, 'user-title'] : ['common', 'user-title'])
- const variants = useCustomVariant()
- const { data } = api.orgEmail.forContactInfo.useQuery(
- { parentId, locationOnly, serviceOnly },
- { enabled: !passedData }
- )
- let k = 0
-
- const componentData = passedData ? passedData : data
-
- if (!componentData?.length) return null
-
- for (const email of componentData) {
- const {
- primary,
- title,
- description,
- email: address,
- locationOnly: showLocOnly,
- serviceOnly: showServOnly,
- } = email
- if ((locationOnly && !showLocOnly) || (serviceOnly && !showServOnly)) continue
-
- const href = `mailto:${address}`
- if (!isExternal(href)) continue
- if (direct) {
- return (
-
- {t('direct.email')}
-
- {address}
-
-
- )
- }
- const desc = title
- ? t(title.key, { ns: 'user-title' })
- : description?.key
- ? t(description.key, { defaultValue: description.defaultText, ns: orgId?.id })
- : undefined
-
- const item = (
-
-
- {address}
-
- {desc && {desc}}
-
- )
- primary ? output.unshift(item) : output.push(item)
- k++
- }
- return (
-
- {t('words.email')}
- {output}
-
- )
-}
-
-const Websites = ({ parentId = '', passedData, direct, locationOnly, websiteDesc }: WebsitesProps) => {
- const output: JSX.Element[] = []
- const slug = useSlug()
- const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
- const { t } = useTranslation(orgId?.id ? ['common', orgId.id] : ['common'])
- const variants = useCustomVariant()
- const { data } = api.orgWebsite.forContactInfo.useQuery(
- { parentId, locationOnly },
- { enabled: !passedData }
- )
- // eslint-disable-next-line no-useless-escape
- const domainExtract = /https?:\/\/([^:\/\n?]+)/
-
- const componentData = passedData ? passedData : data
-
- if (!componentData?.length) return null
-
- for (const website of componentData) {
- const { id, url, orgLocationOnly, description, isPrimary } = website
- const urlMatch = url.match(domainExtract)
- const urlBase = urlMatch?.length ? urlMatch[1] : undefined
- if (!isExternal(url)) continue
- if (!urlBase) continue
- if (locationOnly && !orgLocationOnly) continue
-
- if (direct) {
- return (
-
- {t('direct.website')}
-
- {urlBase}
-
-
- )
- }
-
- const desc = websiteDesc
- ? description?.key
- ? t(description.key, { ns: orgId?.id, defaultText: description.defaultText })
- : urlBase
- : urlBase
- const item = (
-
- {desc}
-
- )
- isPrimary ? output.unshift(item) : output.push(item)
- }
-
- if (!output.length) return null
-
- return (
-
- {t('website', { count: output.length })}
- {output}
-
- )
-}
-
-const SocialMedia = ({ parentId = '', passedData, locationOnly }: SocialMediaProps) => {
- const { data } = api.orgSocialMedia.forContactInfo.useQuery(
- { parentId, locationOnly },
- { enabled: !passedData }
- )
-
- const componentData = passedData ? passedData : data
-
- if (!componentData?.length) return null
- const items: SocialLinkProps[] = []
-
- for (const item of componentData) {
- const icon = item.service.toLowerCase()
- if (!isSocialIcon(icon)) continue
- items.push({
- icon,
- href: item.url,
- title: item.username,
- })
- }
- if (!items.length) return null
- return
-}
-
-export const ContactInfo = ({
- passedData,
- parentId,
- order = ['website', 'phone', 'email', 'socialMedia'],
- gap = 24,
- ...commonProps
-}: ContactInfoProps) => {
- const sections: ContactMap = {
- website: (
-
- ),
- phone: (
-
- ),
- email: (
-
- ),
- socialMedia: (
-
- ),
- }
- const items = order.map((item) => sections[item])
- return {items}
-}
-
-export const hasContactInfo = (data: PassedDataObject) => {
- const { websites, phones, emails, socialMedia } = data
- return Boolean(websites.length || phones.length || emails.length || socialMedia.length)
-}
-
-type ContactSections = 'phone' | 'email' | 'website' | 'socialMedia'
-
-type ContactMap = {
- [K in ContactSections]: JSX.Element
-}
-
-export type ContactInfoProps = CommonProps & {
- order?: ContactSections[]
- gap?: number
-} & (ApiData | PassedDataProps)
-
-interface CommonProps {
- direct?: boolean
- locationOnly?: boolean
- serviceOnly?: boolean
- websiteDesc?: boolean
-}
-
-type PhoneNumbersProps = CommonProps & (ApiData | PassedData<'orgPhone', 'forContactInfo'>)
-type WebsitesProps = CommonProps & (ApiData | PassedData<'orgWebsite', 'forContactInfo'>)
-
-type EmailsProps = CommonProps & (ApiData | PassedData<'orgEmail', 'forContactInfo'>)
-type SocialMediaProps = CommonProps & (ApiData | PassedData<'orgSocialMedia', 'forContactInfo'>)
-
-type PassedData = {
- passedData: ApiOutput[K1][K2]
- parentId?: never
-}
-
-export interface PassedDataObject {
- phones: ApiOutput['orgPhone']['forContactInfo']
- emails: ApiOutput['orgEmail']['forContactInfo']
- websites: ApiOutput['orgWebsite']['forContactInfo']
- socialMedia: ApiOutput['orgSocialMedia']['forContactInfo']
-}
-type ApiData = {
- parentId: string
- passedData?: never
-}
-type PassedDataProps = {
- passedData: PassedDataObject
- parentId?: never
-}
diff --git a/packages/ui/components/data-display/ContactInfo/Emails.tsx b/packages/ui/components/data-display/ContactInfo/Emails.tsx
new file mode 100644
index 0000000000..64dda8b119
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/Emails.tsx
@@ -0,0 +1,153 @@
+import { Group, Stack, Text, Title, useMantineTheme } from '@mantine/core'
+import { useTranslation } from 'next-i18next'
+import { type ReactElement } from 'react'
+
+import { isExternal, Link } from '~ui/components/core/Link'
+import { EmailDrawer } from '~ui/components/data-portal/EmailDrawer'
+import { useCustomVariant } from '~ui/hooks/useCustomVariant'
+import { useOrgInfo } from '~ui/hooks/useOrgInfo'
+import { useSlug } from '~ui/hooks/useSlug'
+import { Icon } from '~ui/icon'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+import { useCommonStyles } from './common.styles'
+import { type EmailsProps } from './types'
+
+export const Emails = ({ edit, ...props }: EmailsProps) =>
+ edit ? :
+
+const EmailsDisplay = ({ parentId = '', passedData, direct, locationOnly, serviceOnly }: EmailsProps) => {
+ const output: ReactElement[] = []
+ const { id: orgId } = useOrgInfo()
+ const { t } = useTranslation(orgId ? ['common', orgId, 'user-title'] : ['common', 'user-title'])
+ const variants = useCustomVariant()
+ const { data } = api.orgEmail.forContactInfo.useQuery(
+ { parentId, locationOnly, serviceOnly },
+ { enabled: !passedData }
+ )
+ let k = 0
+
+ const componentData = passedData ? passedData : data
+
+ if (!componentData?.length) return null
+
+ for (const email of componentData) {
+ const {
+ primary,
+ title,
+ description,
+ email: address,
+ locationOnly: showLocOnly,
+ serviceOnly: showServOnly,
+ } = email
+ if ((locationOnly && !showLocOnly) || (serviceOnly && !showServOnly)) continue
+
+ const href = `mailto:${address}`
+ if (!isExternal(href)) continue
+ if (direct) {
+ return (
+
+ {t('direct.email')}
+
+ {address}
+
+
+ )
+ }
+ const desc = title
+ ? t(title.key, { ns: 'user-title' })
+ : description?.key
+ ? t(description.key, { defaultValue: description.defaultText, ns: orgId })
+ : undefined
+
+ const item = (
+
+
+ {address}
+
+ {desc && {desc}}
+
+ )
+ primary ? output.unshift(item) : output.push(item)
+ k++
+ }
+ return (
+
+ {t('words.email')}
+ {output}
+
+ )
+}
+
+const EmailsEdit = ({ parentId = '' }: EmailsProps) => {
+ const output: ReactElement[] = []
+ const slug = useSlug()
+ const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
+ const { t } = useTranslation(orgId?.id ? ['common', orgId.id, 'user-title'] : ['common', 'user-title'])
+ const variants = useCustomVariant()
+ const theme = useMantineTheme()
+ const { classes } = useCommonStyles()
+ const { data } = api.orgEmail.forContactInfoEdit.useQuery({ parentId })
+
+ if (!data?.length) return null
+
+ for (const email of data) {
+ const { primary, title, description, email: address, published, deleted, id } = email
+
+ const desc = title
+ ? t(title.key, { ns: 'user-title' })
+ : description?.key
+ ? t(description.key, { defaultValue: description.defaultText, ns: orgId?.id })
+ : undefined
+
+ const renderItem = () => {
+ switch (true) {
+ case deleted: {
+ return {
+ email: (
+
+ {address}
+
+ ),
+ desc: desc ? {desc} : null,
+ }
+ }
+ case !published: {
+ return {
+ email: (
+
+
+ {address}
+
+ ),
+ desc: desc ? {desc} : null,
+ }
+ }
+ default: {
+ return {
+ email: {address},
+ desc: desc ? {desc} : null,
+ }
+ }
+ }
+ }
+
+ const item = (
+
+
+ {renderItem().email}
+
+ {renderItem().desc}
+
+ )
+ output.push(item)
+ }
+ return (
+
+ {t('words.email')}
+
+ {output}
+
+
+ )
+}
diff --git a/packages/ui/components/data-display/ContactInfo/PhoneNumbers.tsx b/packages/ui/components/data-display/ContactInfo/PhoneNumbers.tsx
new file mode 100644
index 0000000000..6a92531697
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/PhoneNumbers.tsx
@@ -0,0 +1,159 @@
+import { Group, Stack, Text, Title, useMantineTheme } from '@mantine/core'
+import { useTranslation } from 'next-i18next'
+import { type ReactElement } from 'react'
+
+import { isExternal, Link } from '~ui/components/core/Link'
+import { PhoneDrawer } from '~ui/components/data-portal/PhoneDrawer'
+import { useCustomVariant } from '~ui/hooks/useCustomVariant'
+import { parsePhoneNumber } from '~ui/hooks/usePhoneNumber'
+import { useSlug } from '~ui/hooks/useSlug'
+import { Icon } from '~ui/icon'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+import { useCommonStyles } from './common.styles'
+import { type PhoneNumbersProps } from './types'
+
+export const PhoneNumbers = ({ edit, ...props }: PhoneNumbersProps) =>
+ edit ? :
+
+const PhoneNumbersDisplay = ({ parentId = '', passedData, direct, locationOnly }: PhoneNumbersProps) => {
+ const output: ReactElement[] = []
+ const slug = useSlug()
+ const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
+ const { t } = useTranslation(orgId?.id ? ['common', 'phone-type', orgId.id] : ['common', 'phone-type'])
+ const variants = useCustomVariant()
+ const { data } = api.orgPhone.forContactInfo.useQuery({ parentId, locationOnly }, { enabled: !passedData })
+
+ const componentData = passedData ? passedData : data
+
+ if (!componentData?.length) return null
+
+ for (const phone of componentData) {
+ const { country, ext, locationOnly: showLocationOnly, number, phoneType, primary, description } = phone
+ const parsedPhone = parsePhoneNumber(number, country)
+ if (!parsedPhone) continue
+ if (ext) parsedPhone.setExt(ext)
+ const dialURL = parsedPhone.getURI()
+ const phoneNumber = parsedPhone.formatNational()
+ if (direct) {
+ return (
+
+ {t('direct.phone')}
+ {isExternal(dialURL) ? (
+
+ {phoneNumber}
+
+ ) : (
+ {phoneNumber}
+ )}
+
+ )
+ }
+ if (locationOnly && !showLocationOnly) continue
+ const desc = description
+ ? t(description.key, { ns: orgId?.id, defaultValue: description.defaultText })
+ : phoneType?.key
+ ? t(phoneType.key, { ns: 'phone-type' })
+ : undefined
+
+ const item = (
+
+ {isExternal(dialURL) ? (
+
+ {phoneNumber}
+
+ ) : (
+ {phoneNumber}
+ )}
+ {desc && {desc}}
+
+ )
+ primary ? output.unshift(item) : output.push(item)
+ }
+ return (
+
+ {t('words.phone')}
+ {output}
+
+ )
+}
+
+const PhoneNumbersEdit = ({ parentId = '' }: PhoneNumbersProps) => {
+ const output: ReactElement[] = []
+ const slug = useSlug()
+ const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
+ const { t } = useTranslation(orgId?.id ? ['common', 'phone-type', orgId.id] : ['common', 'phone-type'])
+ const variants = useCustomVariant()
+ const { data } = api.orgPhone.forContactInfoEdit.useQuery({ parentId })
+ const theme = useMantineTheme()
+ const { classes } = useCommonStyles()
+
+ if (!data?.length) return null
+
+ for (const phone of data) {
+ const { country, ext, number, phoneType, primary, description } = phone
+ const parsedPhone = parsePhoneNumber(number, country)
+ if (!parsedPhone) continue
+ if (ext) parsedPhone.setExt(ext)
+
+ const phoneNumber = parsedPhone.formatNational()
+
+ const desc = description
+ ? t(description.key, { ns: orgId?.id, defaultValue: description.defaultText })
+ : phoneType?.key
+ ? t(phoneType.key, { ns: 'phone-type' })
+ : undefined
+
+ const renderItem = () => {
+ switch (true) {
+ case phone.deleted: {
+ return {
+ number: (
+
+ {phoneNumber}
+
+ ),
+ desc: desc ? {desc} : null,
+ }
+ }
+ case !phone.published: {
+ return {
+ number: (
+
+
+ {phoneNumber}
+
+ ),
+ desc: desc ? {desc} : null,
+ }
+ }
+ default: {
+ return {
+ number: {phoneNumber},
+ desc: desc ? {desc} : null,
+ }
+ }
+ }
+ }
+
+ const itemDisplay = renderItem()
+
+ const item = (
+
+
+ {itemDisplay.number}
+
+ {itemDisplay.desc}
+
+ )
+ primary ? output.unshift(item) : output.push(item)
+ }
+ return (
+
+ {t('words.phone')}
+
+ {output}
+
+
+ )
+}
diff --git a/packages/ui/components/data-display/ContactInfo/SocialMedia.tsx b/packages/ui/components/data-display/ContactInfo/SocialMedia.tsx
new file mode 100644
index 0000000000..220381e02b
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/SocialMedia.tsx
@@ -0,0 +1,108 @@
+import { Group, List, Stack, Text, Title, useMantineTheme } from '@mantine/core'
+import { useTranslation } from 'next-i18next'
+
+import { Link } from '~ui/components/core/Link'
+import { isSocialIcon, SocialLink, type SocialLinkProps } from '~ui/components/core/SocialLink'
+import { SocialMediaDrawer } from '~ui/components/data-portal/SocialMediaDrawer'
+import { useCustomVariant } from '~ui/hooks/useCustomVariant'
+import { Icon } from '~ui/icon'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+import { useCommonStyles } from './common.styles'
+import { type SocialMediaProps } from './types'
+
+export const SocialMedia = ({ edit, ...props }: SocialMediaProps) =>
+ edit ? :
+
+const SocialMediaDisplay = ({ parentId = '', passedData, locationOnly }: SocialMediaProps) => {
+ const { data } = api.orgSocialMedia.forContactInfo.useQuery(
+ { parentId, locationOnly },
+ { enabled: !passedData }
+ )
+
+ const componentData = passedData ? passedData : data
+
+ if (!componentData?.length) return null
+ const items: SocialLinkProps[] = []
+
+ for (const item of componentData) {
+ const icon = item.service.toLowerCase()
+ if (!isSocialIcon(icon)) continue
+ items.push({
+ icon,
+ href: item.url,
+ title: item.username,
+ })
+ }
+ if (!items.length) return null
+ return
+}
+
+const SocialMediaEdit = ({ parentId = '' }: SocialMediaProps) => {
+ const { data } = api.orgSocialMedia.forContactInfoEdits.useQuery({ parentId })
+ const { t } = useTranslation(['common'])
+ const { classes } = useCommonStyles()
+ const variants = useCustomVariant()
+ const theme = useMantineTheme()
+ if (!data?.length) return null
+
+ return (
+
+ {t('social.group-header')}
+
+
+ {data.map((link) => {
+ const renderItem = () => {
+ switch (true) {
+ case link.deleted: {
+ return (
+
+
+ {link.service}
+
+ )
+ }
+ case !link.published: {
+ return (
+
+
+
+
+ {link.service}
+
+
+ )
+ }
+ default: {
+ return (
+
+
+ {link.service}
+
+ )
+ }
+ }
+ }
+
+ return (
+
+
+ {renderItem()}
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/packages/ui/components/data-display/ContactInfo/Websites.tsx b/packages/ui/components/data-display/ContactInfo/Websites.tsx
new file mode 100644
index 0000000000..813e0551fe
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/Websites.tsx
@@ -0,0 +1,138 @@
+import { Group, Stack, Text, Title, useMantineTheme } from '@mantine/core'
+import { useTranslation } from 'next-i18next'
+import { type ReactElement } from 'react'
+
+import { isExternal, Link } from '~ui/components/core/Link'
+import { WebsiteDrawer } from '~ui/components/data-portal/WebsiteDrawer'
+import { useCustomVariant } from '~ui/hooks/useCustomVariant'
+import { useSlug } from '~ui/hooks/useSlug'
+import { Icon } from '~ui/icon'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+import { useCommonStyles } from './common.styles'
+import { type WebsitesProps } from './types'
+
+export const Websites = ({ edit, ...props }: WebsitesProps) =>
+ edit ? :
+
+const WebsitesDisplay = ({ parentId = '', passedData, direct, locationOnly, websiteDesc }: WebsitesProps) => {
+ const output: ReactElement[] = []
+ const slug = useSlug()
+ const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
+ const { t } = useTranslation(orgId?.id ? ['common', orgId.id] : ['common'])
+ const variants = useCustomVariant()
+ const { data } = api.orgWebsite.forContactInfo.useQuery(
+ { parentId, locationOnly },
+ { enabled: !passedData }
+ )
+ // eslint-disable-next-line no-useless-escape
+ const domainExtract = /https?:\/\/([^:\/\n?]+)/
+
+ const componentData = passedData ? passedData : data
+
+ if (!componentData?.length) return null
+
+ for (const website of componentData) {
+ const { id, url, orgLocationOnly, description, isPrimary } = website
+ const urlMatch = url.match(domainExtract)
+ const urlBase = urlMatch?.length ? urlMatch[1] : undefined
+ if (!isExternal(url)) continue
+ if (!urlBase) continue
+ if (locationOnly && !orgLocationOnly) continue
+
+ if (direct) {
+ return (
+
+ {t('direct.website')}
+
+ {urlBase}
+
+
+ )
+ }
+
+ const desc = websiteDesc
+ ? description?.key
+ ? t(description.key, { ns: orgId?.id, defaultText: description.defaultText })
+ : urlBase
+ : urlBase
+ const item = (
+
+ {desc}
+
+ )
+ isPrimary ? output.unshift(item) : output.push(item)
+ }
+
+ if (!output.length) return null
+
+ return (
+
+ {t('website', { count: output.length })}
+ {output}
+
+ )
+}
+
+const WebsitesEdit = ({ parentId = '' }: WebsitesProps) => {
+ const output: ReactElement[] = []
+ const slug = useSlug()
+ const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug })
+ const { t } = useTranslation(orgId?.id ? ['common', orgId.id] : ['common'])
+ const variants = useCustomVariant()
+ const theme = useMantineTheme()
+ const { classes } = useCommonStyles()
+ const { data } = api.orgWebsite.forContactInfoEdit.useQuery({ parentId })
+ // eslint-disable-next-line no-useless-escape
+ const domainExtract = /https?:\/\/([^:\/\n?]+)/
+
+ if (!data?.length) return null
+
+ for (const website of data) {
+ const { id, url, description, published, deleted } = website
+ const urlMatch = url.match(domainExtract)
+ const urlBase = urlMatch?.length ? urlMatch[1] : undefined
+ if (!isExternal(url)) continue
+ if (!urlBase) continue
+ const desc = description?.key
+ ? t(description.key, { ns: orgId?.id, defaultText: description.defaultText })
+ : urlBase
+
+ const renderItem = () => {
+ switch (true) {
+ case deleted: {
+ return {desc}
+ }
+ case !published: {
+ return (
+
+
+ {desc}
+
+ )
+ }
+ default: {
+ return {desc}
+ }
+ }
+ }
+
+ const item = (
+
+ {renderItem()}
+
+ )
+ output.push(item)
+ }
+
+ if (!output.length) return null
+
+ return (
+
+ {t('website', { count: output.length })}
+
+ {output}
+
+
+ )
+}
diff --git a/packages/ui/components/data-display/ContactInfo/common.styles.ts b/packages/ui/components/data-display/ContactInfo/common.styles.ts
new file mode 100644
index 0000000000..c112a1fc42
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/common.styles.ts
@@ -0,0 +1,10 @@
+import { createStyles, rem } from '@mantine/core'
+
+export const useCommonStyles = createStyles((theme) => ({
+ overlay: {
+ backgroundColor: theme.fn.lighten(theme.other.colors.secondary.teal, 0.9),
+ borderRadius: rem(16),
+ margin: rem(-8),
+ padding: rem(8),
+ },
+}))
diff --git a/packages/ui/components/data-display/ContactInfo.stories.tsx b/packages/ui/components/data-display/ContactInfo/index.stories.tsx
similarity index 95%
rename from packages/ui/components/data-display/ContactInfo.stories.tsx
rename to packages/ui/components/data-display/ContactInfo/index.stories.tsx
index 8387cb9a8e..175ca2a4cb 100644
--- a/packages/ui/components/data-display/ContactInfo.stories.tsx
+++ b/packages/ui/components/data-display/ContactInfo/index.stories.tsx
@@ -6,7 +6,7 @@ import { orgPhone } from '~ui/mockData/orgPhone'
import { orgSocialMedia } from '~ui/mockData/orgSocialMedia'
import { orgWebsite } from '~ui/mockData/orgWebsite'
-import { ContactInfo } from './ContactInfo'
+import { ContactInfo } from './index'
export default {
title: 'Data Display/Contact Info - Individual API',
diff --git a/packages/ui/components/data-display/ContactInfo/index.tsx b/packages/ui/components/data-display/ContactInfo/index.tsx
new file mode 100644
index 0000000000..d5d22b9c09
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/index.tsx
@@ -0,0 +1,53 @@
+import { Stack } from '@mantine/core'
+
+import { Emails } from './Emails'
+import { PhoneNumbers } from './PhoneNumbers'
+import { SocialMedia } from './SocialMedia'
+import { type ContactInfoProps, type ContactMap, type PassedDataObject } from './types'
+import { Websites } from './Websites'
+
+export const ContactInfo = ({
+ passedData,
+ parentId,
+ order = ['website', 'phone', 'email', 'socialMedia'],
+ gap = 24,
+ ...commonProps
+}: ContactInfoProps) => {
+ const sections: ContactMap = {
+ website: (
+
+ ),
+ phone: (
+
+ ),
+ email: (
+
+ ),
+ socialMedia: (
+
+ ),
+ }
+ const items = order.map((item) => sections[item])
+ return {items}
+}
+
+export const hasContactInfo = (data: PassedDataObject) => {
+ const { websites, phones, emails, socialMedia } = data
+ return Boolean(websites.length || phones.length || emails.length || socialMedia.length)
+}
diff --git a/packages/ui/components/data-display/ContactInfo/types.ts b/packages/ui/components/data-display/ContactInfo/types.ts
new file mode 100644
index 0000000000..3020041dbf
--- /dev/null
+++ b/packages/ui/components/data-display/ContactInfo/types.ts
@@ -0,0 +1,48 @@
+import { type ReactElement } from 'react'
+
+import { type ApiOutput } from '@weareinreach/api'
+
+type ContactSections = 'phone' | 'email' | 'website' | 'socialMedia'
+
+export type ContactMap = {
+ [K in ContactSections]: ReactElement
+}
+
+export type ContactInfoProps = CommonProps & {
+ order?: ContactSections[]
+ gap?: number
+} & (ApiData | PassedDataProps)
+
+interface CommonProps {
+ direct?: boolean
+ locationOnly?: boolean
+ serviceOnly?: boolean
+ websiteDesc?: boolean
+ edit?: boolean
+}
+
+export type PhoneNumbersProps = CommonProps & (ApiData | PassedData<'orgPhone', 'forContactInfo'>)
+export type WebsitesProps = CommonProps & (ApiData | PassedData<'orgWebsite', 'forContactInfo'>)
+
+export type EmailsProps = CommonProps & (ApiData | PassedData<'orgEmail', 'forContactInfo'>)
+export type SocialMediaProps = CommonProps & (ApiData | PassedData<'orgSocialMedia', 'forContactInfo'>)
+
+type PassedData = {
+ passedData: ApiOutput[K1][K2]
+ parentId?: never
+}
+
+export interface PassedDataObject {
+ phones: ApiOutput['orgPhone']['forContactInfo']
+ emails: ApiOutput['orgEmail']['forContactInfo']
+ websites: ApiOutput['orgWebsite']['forContactInfo']
+ socialMedia: ApiOutput['orgSocialMedia']['forContactInfo']
+}
+type ApiData = {
+ parentId: string
+ passedData?: never
+}
+type PassedDataProps = {
+ passedData: PassedDataObject
+ parentId?: never
+}
diff --git a/packages/ui/components/data-display/index.ts b/packages/ui/components/data-display/index.ts
index f19984c9c7..5c16923778 100644
--- a/packages/ui/components/data-display/index.ts
+++ b/packages/ui/components/data-display/index.ts
@@ -1,5 +1,5 @@
-// codegen:start {preset: barrel, include: ./*.ts*, exclude: "*.stories.*"}
+// codegen:start {preset: barrel, include: ["./*.tsx", "./**/index.tsx"], exclude: "*.stories.*"}
export * from './AccessInfo'
-export * from './ContactInfo'
+export * from './ContactInfo/index'
export * from './Hours'
// codegen:end
diff --git a/packages/ui/components/data-portal/EmailDrawer/index.stories.tsx b/packages/ui/components/data-portal/EmailDrawer/index.stories.tsx
new file mode 100644
index 0000000000..691b571ee4
--- /dev/null
+++ b/packages/ui/components/data-portal/EmailDrawer/index.stories.tsx
@@ -0,0 +1,42 @@
+import { type Meta, type StoryObj } from '@storybook/react'
+
+import { Button } from '~ui/components/core/Button'
+import { organization } from '~ui/mockData/organization'
+import { orgEmail } from '~ui/mockData/orgEmail'
+
+import { EmailDrawer } from './index'
+
+export default {
+ title: 'Data Portal/Drawers/Email',
+ component: EmailDrawer,
+ parameters: {
+ layout: 'fullscreen',
+ rqDevtools: true,
+ // wdyr: true,
+ nextjs: {
+ router: {
+ pathname: '/org/[slug]/edit',
+ asPath: '/org/mock-org-slug',
+ query: {
+ slug: 'mock-org-slug',
+ },
+ },
+ },
+ msw: [organization.getIdFromSlug, orgEmail.forContactInfoEdit, orgEmail.forEditDrawer],
+ },
+ args: {
+ component: Button,
+ children: 'Open Drawer',
+ variant: 'inlineInvertedUtil1',
+ id: 'oeml_01GVH3VEVDX7QVQ4QA4C1XXVN3',
+ },
+} satisfies Meta
+
+type StoryDef = StoryObj
+
+export const WithData = {
+ args: {
+ locationId: 'oloc_01GVH3VEVBERFNA9PHHJYEBGA3',
+ },
+} satisfies StoryDef
+export const WithoutData = {} satisfies StoryDef
diff --git a/packages/ui/components/data-portal/EmailDrawer/index.tsx b/packages/ui/components/data-portal/EmailDrawer/index.tsx
new file mode 100644
index 0000000000..75f586cf0a
--- /dev/null
+++ b/packages/ui/components/data-portal/EmailDrawer/index.tsx
@@ -0,0 +1,191 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import {
+ Box,
+ createPolymorphicComponent,
+ createStyles,
+ Drawer,
+ Group,
+ LoadingOverlay,
+ Modal,
+ rem,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core'
+import { useDisclosure } from '@mantine/hooks'
+import { useRouter } from 'next/router'
+import { forwardRef, useEffect, useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { Checkbox, TextInput } from 'react-hook-form-mantine'
+import invariant from 'tiny-invariant'
+import { z } from 'zod'
+
+import { Breadcrumb } from '~ui/components/core/Breadcrumb'
+import { Button } from '~ui/components/core/Button'
+import { Icon } from '~ui/icon'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+const FormSchema = z.object({
+ firstName: z.string().nullish(),
+ lastName: z.string().nullish(),
+ primary: z.boolean(),
+ email: z.string().email(),
+ description: z.string().nullish(),
+ titleId: z.string().nullish(),
+ published: z.boolean(),
+ deleted: z.boolean(),
+ locationOnly: z.boolean(),
+ serviceOnly: z.boolean(),
+ id: z.string(),
+ orgId: z.string(),
+ descriptionId: z.string().nullish(),
+})
+type FormSchema = z.infer
+const useStyles = createStyles(() => ({
+ drawerContent: {
+ borderRadius: `${rem(32)} 0 0 0`,
+ minWidth: '40vw',
+ },
+}))
+export const _EmailDrawer = forwardRef(({ id, ...props }, ref) => {
+ const router = useRouter<'/org/[slug]/edit'>()
+ const { isFetching } = api.orgEmail.forEditDrawer.useQuery({ id })
+ const [drawerOpened, drawerHandler] = useDisclosure(false)
+ const [modalOpened, modalHandler] = useDisclosure(false)
+ const { classes } = useStyles()
+ const apiUtils = api.useUtils()
+ const { control, handleSubmit, formState, reset, getValues } = useForm({
+ resolver: zodResolver(FormSchema),
+ defaultValues: async () => {
+ const data = await apiUtils.orgEmail.forEditDrawer.fetch({ id })
+ const { id: orgId } = await apiUtils.organization.getIdFromSlug.fetch({ slug: router.query.slug ?? '' })
+ invariant(data, 'No data returned')
+ invariant(orgId, 'No orgId')
+ return { ...data, orgId }
+ },
+ })
+
+ const { isDirty: formIsDirty } = formState
+ const [isSaved, setIsSaved] = useState(formIsDirty)
+
+ const emailUpdate = api.orgEmail.update.useMutation({
+ onSettled: (data) => {
+ apiUtils.orgEmail.forEditDrawer.invalidate()
+ apiUtils.orgEmail.forContactInfoEdit.invalidate()
+ reset(data)
+ },
+ onSuccess: () => {
+ setIsSaved(true)
+ },
+ })
+ useEffect(() => {
+ if (isSaved && formIsDirty) {
+ setIsSaved(false)
+ }
+ }, [formIsDirty, isSaved])
+ const handleClose = () => {
+ if (formState.isDirty) {
+ return modalHandler.open()
+ } else {
+ return drawerHandler.close()
+ }
+ }
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+})
+_EmailDrawer.displayName = 'EmailDrawer'
+
+export const EmailDrawer = createPolymorphicComponent<'button', EmailDrawerProps>(_EmailDrawer)
+interface EmailDrawerProps {
+ id: string
+}
diff --git a/packages/ui/components/data-portal/InlineTextInput.stories.tsx b/packages/ui/components/data-portal/InlineTextInput.stories.tsx
index 183b81fe09..4c1a3e751d 100644
--- a/packages/ui/components/data-portal/InlineTextInput.stories.tsx
+++ b/packages/ui/components/data-portal/InlineTextInput.stories.tsx
@@ -1,14 +1,14 @@
-import { Box } from '@mantine/core'
-import { type Meta } from '@storybook/react'
+import { Textarea } from '@mantine/core'
+import { type Meta, type StoryObj } from '@storybook/react'
-import { InlineTextarea, InlineTextInput } from './InlineTextInput'
+import { InlineTextInput } from './InlineTextInput'
export default {
title: 'Data Portal/Fields/Inline Text Field',
component: InlineTextInput,
parameters: {
layout: 'fullscreen',
- layoutWrapper: 'centeredHalf',
+ layoutWrapper: 'gridDouble',
},
argTypes: {
fontSize: {
@@ -21,25 +21,16 @@ export default {
value: 'Test value',
},
} satisfies Meta
-
-export const TextInput = {
+type StoryDef = StoryObj
+export const SingleLine = {
args: {
fontSize: 'h1',
},
- render: (args) => (
-
-
-
- ),
-} satisfies Meta
+} satisfies StoryDef
-export const Textarea = {
+export const MultiLine = {
args: {
fontSize: 'utility1',
+ component: Textarea,
},
- render: (args) => (
-
-
-
- ),
-} satisfies Meta
+} satisfies StoryDef
diff --git a/packages/ui/components/data-portal/InlineTextInput.tsx b/packages/ui/components/data-portal/InlineTextInput.tsx
index 06da4cef2f..69aeb73646 100644
--- a/packages/ui/components/data-portal/InlineTextInput.tsx
+++ b/packages/ui/components/data-portal/InlineTextInput.tsx
@@ -1,11 +1,12 @@
import {
+ Box,
+ createPolymorphicComponent,
createStyles,
rem,
- Textarea,
- type TextareaProps,
TextInput,
type TextInputProps,
} from '@mantine/core'
+import { forwardRef } from 'react'
const useStyles = createStyles((theme) => ({
...theme.other.utilityFonts,
@@ -22,59 +23,39 @@ const useBaseStyles = createStyles((theme) => ({
'&:focus, &:focus-within': {
borderColor: theme.other.colors.secondary.black,
borderWidth: rem(1),
+ backgroundColor: theme.other.colors.secondary.white,
},
'&[data-isDirty=true]': {
- backgroundColor: theme.other.colors.primary.lightGray,
+ backgroundColor: theme.fn.lighten(theme.other.colors.secondary.teal, 0.6),
},
},
}))
-const useFontSize = ({ fontSize, classNames }: SingleLineTextProps | MultiLineTextProps) => {
+const useFontSize = ({ fontSize, classNames }: InlineEditProps) => {
const { classes } = useStyles()
const { classes: baseClasses, cx } = useBaseStyles()
return {
...classNames,
- input: fontSize ? cx(classes[fontSize], baseClasses.input) : baseClasses.input,
+ input: fontSize
+ ? cx(classNames?.input, classes[fontSize], baseClasses.input)
+ : cx(classNames?.input, baseClasses.input),
}
}
-/**
- * Components works like TextInput. To use one of the variant fonts pass a string like 'h1', 'h2', 'utility1',
- * etc to the fontSize prop.
- *
- * @param props - TextInputProps
- * @param props.fontSize - HeadingSizes | utilitySizes
- * @returns JSX.Element
- */
-export const InlineTextInput = ({ fontSize, ...props }: SingleLineTextProps) => {
- const variant = useFontSize({ fontSize, ...props })
+const _InlineTextInput = forwardRef(
+ ({ fontSize, classNames, ...rest }, ref) => {
+ const variant = useFontSize({ fontSize, ...rest })
- return
-}
-
-/**
- * Component works like Textarea. To use one of the variant fonts pass a string like 'h1', 'h2', 'utility1',
- * etc to the fontSize prop.
- *
- * @param props - TextareaProps
- * @param props.fontSize - HeadingSizes | utilitySizes
- * @returns JSX.Element
- */
-export const InlineTextarea = ({ fontSize, ...props }: MultiLineTextProps) => {
- const variant = useFontSize({ fontSize, ...props })
-
- return
-}
+ return
+ }
+)
+_InlineTextInput.displayName = 'InlineEdit'
+export const InlineTextInput = createPolymorphicComponent<'input', InlineEditProps>(_InlineTextInput)
type FontSizes = keyof ReturnType['classes']
-interface SingleLineTextProps extends TextInputProps {
+interface InlineEditProps extends TextInputProps {
fontSize?: FontSizes
/** Flag if background color should change to indicate that the field was edited */
'data-isDirty'?: boolean
}
-
-interface MultiLineTextProps extends TextareaProps {
- fontSize?: FontSizes
- 'data-isDirty'?: boolean
-}
diff --git a/packages/ui/components/data-portal/PhoneDrawer/index.stories.tsx b/packages/ui/components/data-portal/PhoneDrawer/index.stories.tsx
new file mode 100644
index 0000000000..abbcd1c46d
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneDrawer/index.stories.tsx
@@ -0,0 +1,38 @@
+import { type Meta, type StoryObj } from '@storybook/react'
+
+import { Button } from '~ui/components/core/Button'
+import { fieldOpt } from '~ui/mockData/fieldOpt'
+import { organization } from '~ui/mockData/organization'
+import { orgPhone } from '~ui/mockData/orgPhone'
+
+import { PhoneDrawer } from '.'
+
+export default {
+ title: 'Data Portal/Drawers/Phone',
+ component: PhoneDrawer,
+ parameters: {
+ layout: 'fullscreen',
+ rqDevtools: true,
+ nextjs: {
+ router: {
+ pathname: '/org/[slug]/edit',
+ asPath: '/org/mock-org-slug',
+ query: {
+ slug: 'mock-org-slug',
+ },
+ },
+ },
+ msw: [organization.getIdFromSlug, fieldOpt.countries, orgPhone.forEditDrawer, fieldOpt.phoneTypes],
+ },
+ args: {
+ component: Button,
+ children: 'Open Drawer',
+ variant: 'inlineInvertedUtil1',
+ id: 'oweb_01H29ENF8JTJ3FNJ5BQXDH4PMA',
+ },
+} satisfies Meta
+
+type StoryDef = StoryObj
+
+export const Default = {} satisfies StoryDef
+// export const WithoutData = {} satisfies StoryDef
diff --git a/packages/ui/components/data-portal/PhoneDrawer/index.tsx b/packages/ui/components/data-portal/PhoneDrawer/index.tsx
new file mode 100644
index 0000000000..92d7edc3c2
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneDrawer/index.tsx
@@ -0,0 +1,224 @@
+// import { DevTool } from '@hookform/devtools'
+import { zodResolver } from '@hookform/resolvers/zod'
+import {
+ Box,
+ createPolymorphicComponent,
+ createStyles,
+ Drawer,
+ Group,
+ LoadingOverlay,
+ Modal,
+ rem,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core'
+import { useDisclosure } from '@mantine/hooks'
+import { useTranslation } from 'next-i18next'
+import { forwardRef, useEffect, useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { Checkbox, Select, TextInput } from 'react-hook-form-mantine'
+import { z } from 'zod'
+
+import { Breadcrumb } from '~ui/components/core/Breadcrumb'
+import { Button } from '~ui/components/core/Button'
+import { PhoneNumberEntry } from '~ui/components/data-portal/PhoneNumberEntry/withHookForm'
+import { useOrgInfo } from '~ui/hooks/useOrgInfo'
+import { parsePhoneNumber } from '~ui/hooks/usePhoneNumber'
+import { Icon } from '~ui/icon'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+const useStyles = createStyles((theme) => ({
+ drawerContent: {
+ borderRadius: `${rem(32)} 0 0 0`,
+ minWidth: '40vw',
+ },
+}))
+
+const FormSchema = z.object({
+ id: z.string(),
+ number: z.string(),
+ ext: z.string().nullish(),
+ primary: z.boolean(),
+ published: z.boolean(),
+ deleted: z.boolean(),
+ countryId: z.string(),
+ phoneTypeId: z.string().nullable(),
+ description: z.string().nullable(),
+ locationOnly: z.boolean(),
+ serviceOnly: z.boolean(),
+})
+type FormSchema = z.infer
+const _PhoneDrawer = forwardRef(({ id, ...props }, ref) => {
+ const { t } = useTranslation(['phone-type'])
+ const { id: orgId } = useOrgInfo()
+ const apiUtils = api.useUtils()
+ const { isFetching } = api.orgPhone.forEditDrawer.useQuery(
+ { id },
+ {
+ select: (data) => ({ ...data, orgId: orgId ?? '' }),
+ }
+ )
+ const { data: phoneTypes } = api.fieldOpt.phoneTypes.useQuery(undefined, {
+ initialData: [],
+ select: (data) => data.map(({ id, tsKey, tsNs }) => ({ value: id, label: t(tsKey, { ns: tsNs }) })),
+ })
+ const [drawerOpened, drawerHandler] = useDisclosure(false)
+ const [modalOpened, modalHandler] = useDisclosure(false)
+
+ const { classes } = useStyles()
+ const { control, handleSubmit, formState, reset, getValues, watch } = useForm({
+ resolver: zodResolver(FormSchema),
+ // values: data,
+ defaultValues: async () => {
+ const data = await apiUtils.orgPhone.forEditDrawer.fetch({ id })
+ if (!data) throw new Error('Failed to fetch data')
+ const parsedPhone = parsePhoneNumber(data.number, data.country)
+ return { ...data, number: parsedPhone?.number ?? data.number }
+ },
+ })
+ const { isDirty: formIsDirty } = formState
+ const [isSaved, setIsSaved] = useState(formIsDirty)
+ const siteUpdate = api.orgPhone.update.useMutation({
+ onSettled: (data) => {
+ apiUtils.orgPhone.forEditDrawer.invalidate()
+ apiUtils.orgPhone.forContactInfoEdit.invalidate()
+ const parsedPhone = parsePhoneNumber(data?.number ?? '')
+ reset({ ...data, number: parsedPhone?.number ?? data?.number })
+ },
+ onSuccess: () => {
+ setIsSaved(true)
+ },
+ })
+ // const isSaved = /*siteUpdate.isSuccess &&*/ !formState.isDirty
+
+ useEffect(() => {
+ if (isSaved && formIsDirty) {
+ setIsSaved(false)
+ }
+ }, [formIsDirty, isSaved])
+
+ const values = {
+ phoneTypeId: watch('phoneTypeId'),
+ }
+
+ const handleClose = () => {
+ if (formIsDirty) {
+ return modalHandler.open()
+ } else {
+ return drawerHandler.close()
+ }
+ }
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* */}
+ >
+ )
+})
+_PhoneDrawer.displayName = 'PhoneDrawer'
+
+export const PhoneDrawer = createPolymorphicComponent<'button', PhoneDrawerProps>(_PhoneDrawer)
+
+interface PhoneDrawerProps {
+ id: string
+}
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry.stories.tsx b/packages/ui/components/data-portal/PhoneNumberEntry.stories.tsx
deleted file mode 100644
index b905f90179..0000000000
--- a/packages/ui/components/data-portal/PhoneNumberEntry.stories.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { type Meta, type StoryFn, type StoryObj } from '@storybook/react'
-
-import { fieldOpt } from '~ui/mockData/fieldOpt'
-import {
- formHookParams,
- PhoneEmailFormProvider,
- useForm,
- useFormContext,
-} from '~ui/modals/dataPortal/PhoneEmail/context'
-
-import { PhoneNumberEntry } from './PhoneNumberEntry'
-
-const FormContextDecorator = (Story: StoryFn) => {
- const form = useForm(formHookParams)
- return (
-
-
-
- )
-}
-export default {
- title: 'Data Portal/Fields/Phone Number Entry',
- component: PhoneNumberEntry,
- parameters: {
- msw: [fieldOpt.countries],
- },
- decorators: [FormContextDecorator],
- render: function Render() {
- const form = useFormContext()
-
- return (
- form.setFieldError('phoneNumber', err),
- }}
- />
- )
- },
-} satisfies Meta
-
-type StoryDef = StoryObj
-
-export const Default = {} satisfies StoryDef
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry/CountrySelectItem.tsx b/packages/ui/components/data-portal/PhoneNumberEntry/CountrySelectItem.tsx
new file mode 100644
index 0000000000..750da69cd5
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneNumberEntry/CountrySelectItem.tsx
@@ -0,0 +1,24 @@
+import { Group, Text } from '@mantine/core'
+import { type ComponentPropsWithoutRef, forwardRef } from 'react'
+
+import { type ApiOutput } from '@weareinreach/api'
+
+export const CountrySelectItem = forwardRef(
+ ({ data, value, label, ...props }, ref) => {
+ const { name } = data
+ return (
+
+ {label}
+ {name}
+
+ )
+ }
+)
+CountrySelectItem.displayName = 'CountrySelectItem'
+
+type CountryList = ApiOutput['fieldOpt']['countries']
+export interface CountrySelectItem extends ComponentPropsWithoutRef<'div'> {
+ label: string
+ value: string
+ data: Pick
+}
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry/index.stories.tsx b/packages/ui/components/data-portal/PhoneNumberEntry/index.stories.tsx
new file mode 100644
index 0000000000..918155ca0b
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneNumberEntry/index.stories.tsx
@@ -0,0 +1,81 @@
+import { DevTool } from '@hookform/devtools'
+import { type Meta, type StoryFn, type StoryObj } from '@storybook/react'
+import { FormProvider, useForm as useHookForm } from 'react-hook-form'
+
+import { fieldOpt } from '~ui/mockData/fieldOpt'
+import {
+ formHookParams,
+ PhoneEmailFormProvider,
+ useForm,
+ useFormContext,
+} from '~ui/modals/dataPortal/PhoneEmail/context'
+
+import { PhoneNumberEntry } from './index'
+import { PhoneNumberEntry as PhoneNumberEntryHookForm } from './withHookForm'
+
+const FormContextDecorator = (Story: StoryFn) => {
+ const form = useForm(formHookParams)
+ return (
+
+
+
+ )
+}
+const HookFormContextDecorator = (Story: StoryFn) => {
+ const form = useHookForm()
+ return (
+
+
+
+ )
+}
+export default {
+ title: 'Data Portal/Fields/Phone Number Entry',
+ component: PhoneNumberEntry,
+ parameters: {
+ msw: [fieldOpt.countries],
+ rqDevtools: true,
+ },
+} satisfies Meta
+
+type StoryDef = StoryObj
+
+export const Default = {
+ decorators: [FormContextDecorator],
+ render: function Render() {
+ const form = useFormContext()
+
+ return (
+ form.setFieldError('phoneNumber', err),
+ }}
+ />
+ )
+ },
+} satisfies StoryDef
+
+export const WithReactHookForm = {
+ decorators: [HookFormContextDecorator],
+
+ render: function Render() {
+ const form = useHookForm({ mode: 'onTouched' })
+
+ return (
+
+ )
+ },
+} satisfies StoryObj
+
+type HookFormParams = { countryId: string; number: string }
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry.tsx b/packages/ui/components/data-portal/PhoneNumberEntry/index.tsx
similarity index 99%
rename from packages/ui/components/data-portal/PhoneNumberEntry.tsx
rename to packages/ui/components/data-portal/PhoneNumberEntry/index.tsx
index 3346d2204c..c5830c4117 100644
--- a/packages/ui/components/data-portal/PhoneNumberEntry.tsx
+++ b/packages/ui/components/data-portal/PhoneNumberEntry/index.tsx
@@ -56,19 +56,6 @@ const usePhoneEntryStyles = createStyles((theme) => ({
borderLeft: `1px solid ${theme.other.colors.primary.lightGray}`,
},
}))
-export interface PhoneNumberEntryProps {
- countrySelectProps: Omit
- phoneEntryProps: Omit<
- SetOptional<
- PhoneInputProps>,
- 'onChange'
- >,
- 'country' | 'defaultCountry' | 'itemComponent'
- > & {
- setError?: (err: string) => void
- 'data-autofocus'?: boolean
- }
-}
export const PhoneNumberEntry = ({
countrySelectProps,
@@ -163,6 +150,21 @@ export const PhoneNumberEntry = ({
)
}
+export interface PhoneNumberEntryProps {
+ countrySelectProps: Omit
+ phoneEntryProps: Omit<
+ SetOptional<
+ PhoneInputProps>,
+ 'onChange'
+ >,
+ 'country' | 'defaultCountry' | 'itemComponent'
+ > & {
+ setError?: (err: string) => void
+ 'data-autofocus'?: boolean
+ }
+ hookForm?: boolean
+}
+
type CountryList = ApiOutput['fieldOpt']['countries']
interface PhoneCountrySelectItem extends ComponentPropsWithoutRef<'div'>, PhoneCountryItem {}
interface PhoneCountryItem {
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry/lib.ts b/packages/ui/components/data-portal/PhoneNumberEntry/lib.ts
new file mode 100644
index 0000000000..28a2d7fd85
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneNumberEntry/lib.ts
@@ -0,0 +1,19 @@
+import { type ApiOutput } from '@weareinreach/api'
+
+export const topCountries = ['US', 'CA', 'MX']
+
+export const transformCountryList = (data: ApiOutput['fieldOpt']['countries']) =>
+ data
+ .map(({ id, flag, name, cca2 }) => ({
+ value: id,
+ label: `${flag}`,
+ data: { name, cca2 },
+ group: topCountries.includes(cca2) ? 'Common' : 'Others',
+ }))
+ .sort((a, b) => {
+ if (topCountries.includes(a.data.cca2) && !topCountries.includes(b.data.cca2)) {
+ return -1
+ } else if (topCountries.includes(b.data.cca2) && !topCountries.includes(a.data.cca2)) {
+ return 1
+ } else return a.data.cca2.localeCompare(b.data.cca2)
+ })
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry/styles.ts b/packages/ui/components/data-portal/PhoneNumberEntry/styles.ts
new file mode 100644
index 0000000000..b99ca3e0dc
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneNumberEntry/styles.ts
@@ -0,0 +1,27 @@
+import { createStyles, rem } from '@mantine/core'
+
+export const useCountrySelectStyles = createStyles((theme) => ({
+ dropdown: {
+ width: 'max-content !important',
+ left: 'unset !important',
+ // right: 0,
+ },
+ root: {
+ width: rem(48),
+ },
+ input: {
+ border: 'none',
+ padding: 0,
+ height: '2rem',
+ },
+ rightSection: {
+ paddingRight: 0,
+ },
+}))
+export const usePhoneEntryStyles = createStyles((theme) => ({
+ rightSection: {
+ padding: `0 ${rem(4)}`,
+ margin: `${rem(2)} 0`,
+ borderLeft: `1px solid ${theme.other.colors.primary.lightGray}`,
+ },
+}))
diff --git a/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx b/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx
new file mode 100644
index 0000000000..274eff68f0
--- /dev/null
+++ b/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx
@@ -0,0 +1,169 @@
+import { ErrorMessage } from '@hookform/error-message'
+import { TextInput, type TextInputProps } from '@mantine/core'
+import { AsYouType } from 'libphonenumber-js'
+import { useEffect, useMemo } from 'react'
+import {
+ type Control,
+ type FieldValues,
+ useController,
+ type UseControllerProps,
+ useWatch,
+} from 'react-hook-form'
+import { Select, type SelectProps } from 'react-hook-form-mantine'
+import { parsePhoneNumber } from 'react-phone-number-input'
+import PhoneInput, { type Props as PhoneInputProps } from 'react-phone-number-input/react-hook-form-input'
+
+import { isCountryCode } from '~ui/hooks/usePhoneNumber'
+import { trpc as api } from '~ui/lib/trpcClient'
+
+import { CountrySelectItem } from './CountrySelectItem'
+import { transformCountryList } from './lib'
+import { useCountrySelectStyles, usePhoneEntryStyles } from './styles'
+
+const DEFAULT_COUNTRY = 'US'
+
+export const PhoneNumberEntry = ({
+ countrySelect,
+ phoneInput,
+ control,
+ label = 'Phone Number',
+ required,
+}: PhoneNumberEntryProps) => {
+ const { data: countryList } = api.fieldOpt.countries.useQuery(
+ { activeForOrgs: true },
+ {
+ initialData: [],
+ select: (data) => transformCountryList(data),
+ }
+ )
+ const validCountries = countryList.map(({ data }) => data.cca2)
+
+ const {
+ name: peName,
+ defaultValue: peDefaultValue,
+ rules: peRules,
+ shouldUnregister: peShouldUnregister,
+ ...propsPhoneInput
+ } = phoneInput
+ const {
+ name: csName,
+ defaultValue: csDefaultValue,
+ rules: csRules,
+ shouldUnregister: csShouldUnregister,
+ ...propsCountrySelect
+ } = countrySelect
+ const phoneNumbControl = useController({
+ name: peName,
+ control,
+ defaultValue: peDefaultValue,
+ rules: peRules,
+ shouldUnregister: peShouldUnregister,
+ })
+ // const phoneNumber = phoneNumbControl.field.value
+ const phoneNumber = useWatch({ name: peName, control })
+
+ const countryControl = useController({
+ name: csName,
+ control,
+ defaultValue: csDefaultValue,
+ rules: csRules,
+ shouldUnregister: csShouldUnregister,
+ })
+ // const selectedCountry = countryControl.field.value
+ const selectedCountry = useWatch({ name: csName, control })
+
+ const { classes: countrySelectClasses } = useCountrySelectStyles()
+ const { classes: phoneEntryClasses } = usePhoneEntryStyles()
+
+ const activeCountry = useMemo(() => {
+ const result = countryList?.find(({ value }) => value === selectedCountry)?.data.cca2
+ if (result && isCountryCode(result)) return result
+ return undefined
+ }, [selectedCountry, countryList])
+
+ const phoneFormatter = new AsYouType(activeCountry)
+
+ useEffect(() => {
+ if (phoneNumber) {
+ phoneFormatter.input(phoneNumber)
+ const phoneCountry = phoneFormatter.getNumber()?.country
+ if ((!phoneCountry && !selectedCountry) || phoneCountry !== selectedCountry) {
+ const countryId = countryList.find(({ data }) => data.cca2 === phoneCountry)?.value
+
+ if (countryId) {
+ countryControl.field.onChange(countryId)
+ if (countrySelect.onChange && typeof countrySelect.onChange === 'function') {
+ countrySelect.onChange(countryId)
+ }
+ }
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [phoneNumber])
+
+ const countrySelection = (
+