diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 69fdefa3c4..65bce2d3b6 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -18,6 +18,7 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} DATABASE_URL: ${{ secrets.DATABASE_URL }} DB_DIRECT_URL: ${{ secrets.DB_DIRECT_URL }} + NEXT_PUBLIC_GOOGLE_MAPS_API: '' CI: true OVERRIDE_CI: true FORCE_COLOR: true @@ -30,29 +31,18 @@ jobs: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3 with: fetch-depth: 0 - - name: Install Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3 - with: - node-version: 18 - uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598 # v2.4.0 name: Install pnpm id: pnpm-install with: run_install: false - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: 'echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT' - - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3 - name: Setup pnpm cache + - name: Install Node.js + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3 with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - pnpm-store- - # - name: Install dependencies - # run: pnpm install - # 👇 Runs pnpm in ./packages/ui + node-version-file: .nvmrc + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - name: Install dependencies run: pnpm install working-directory: packages/ui diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da3e2b1963..9202ccfe7e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,30 +30,19 @@ jobs: git_config_global: true git_commit_gpgsign: true - - name: Install Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3 - with: - node-version: 18 - - uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598 # v2.4.0 name: Install pnpm id: pnpm-install with: run_install: false + # standalone: true - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3 - name: Setup pnpm cache + - name: Install Node.js + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3 with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + node-version-file: .nvmrc + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - name: Install dependencies run: pnpm install @@ -89,7 +78,7 @@ jobs: with: message: 'chore: lint & format' commit: --signoff --no-verify - committer_name: InReach Bot + committer_name: InReach [bot] committer_email: 108850934+InReach-svc@users.noreply.github.com - author_name: InReach Bot + author_name: InReach [bot] author_email: 108850934+InReach-svc@users.noreply.github.com diff --git a/InReach.code-workspace b/InReach.code-workspace index dfc1c5ebed..d932e9b9bb 100644 --- a/InReach.code-workspace +++ b/InReach.code-workspace @@ -137,6 +137,8 @@ "eslint.options": { "cache": true }, + "eslint.ignoreUntitled": true, + "eslint.nodeEnv": "development", "eslint.rules.customizations": [ { "rule": "import/order", @@ -160,9 +162,7 @@ }, { "rule": "sort-imports", "severity": "off" } ], - "eslint.runtime": "node", "eslint.useESLintClass": true, - "eslint.workingDirectories": [{ "pattern": "./packages/*/" }, { "pattern": "./apps/*/" }], "explorer.decorations.badges": true, "explorer.expandSingleFolderWorkspaces": false, "explorer.fileNesting.enabled": true, diff --git a/apps/app/package.json b/apps/app/package.json index ff53a38e80..d6afb8c11b 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -96,6 +96,7 @@ "just-compact": "3.2.0", "just-compare": "2.3.0", "luxon": "3.4.0", + "mantine-react-table": "1.1.2", "next": "13.4.18", "next-auth": "4.23.1", "next-i18next": "14.0.0", diff --git a/apps/app/public/locales/en/common.json b/apps/app/public/locales/en/common.json index 097037af9b..6f761ecd85 100644 --- a/apps/app/public/locales/en/common.json +++ b/apps/app/public/locales/en/common.json @@ -84,6 +84,9 @@ "enter-password-placeholder": "Enter password...", "enter-review": "Enter your review...", "errors": { + "401-title": "You must be logged in to do that.", + "403-body": "You do not have permission to access this page. If you feel that you have reached this page in error, please contact your supervisor.", + "403-title": "403: Forbidden", "404-body": "We're sorry, the page you're looking for doesn't exist or has been moved. Start a search below to find safe, verified resources for the diverse LGBTQ+ community in your area.", "404-title": "404: Page not found.", "500-body": "We're sorry, something went wrong with our server. Please try again later, or start a search below to find safe, verified LGBTQ+ resources in your area.", @@ -368,6 +371,7 @@ "visit": "Visit", "website_one": "Website", "website_other": "Websites", + "welcome-name": "Welcome, {{name}}!", "words": { "accept": "Accept", "account": "Account", diff --git a/apps/app/src/pages/401.tsx b/apps/app/src/pages/401.tsx new file mode 100644 index 0000000000..77ddde81df --- /dev/null +++ b/apps/app/src/pages/401.tsx @@ -0,0 +1,52 @@ +import { Container, rem, Stack, Title } from '@mantine/core' +import { type GetStaticProps } from 'next' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' +import { type Route } from 'nextjs-routes' +import { z } from 'zod' + +import { getServerSideTranslations } from '~app/utils/i18n' +import { LoginBody } from '~ui/modals/Login' + +const RouteSchema = z.object({ + pathname: z.string(), + query: z.record(z.string()).optional(), + locale: z.string().optional(), +}) + +const Unauthorized = () => { + const { t } = useTranslation('common') + const router = useRouter() + const callback = + typeof router.query.callbackUrl === 'string' + ? RouteSchema.safeParse(JSON.parse(Buffer.from(router.query.callbackUrl, 'base64').toString('utf-8'))) + : undefined + + return ( + + + + {/* eslint-disable-next-line i18next/no-literal-string */} + 🔐 + {t('errors.401-title')} + + + + + ) +} + +export const getStaticProps: GetStaticProps = async ({ locale }) => { + return { + props: { + ...(await getServerSideTranslations(locale, ['common'])), + }, + revalidate: 60 * 60 * 24 * 7, + } +} + +export default Unauthorized diff --git a/apps/app/src/pages/403.tsx b/apps/app/src/pages/403.tsx new file mode 100644 index 0000000000..40443c403d --- /dev/null +++ b/apps/app/src/pages/403.tsx @@ -0,0 +1,37 @@ +import { Container, rem, Stack, Text, Title } from '@mantine/core' +import { type GetStaticProps } from 'next' +import { useTranslation } from 'next-i18next' + +import { getServerSideTranslations } from '~app/utils/i18n' + +const Forbidden = () => { + const { t } = useTranslation('common') + + return ( + + + + {/* eslint-disable-next-line i18next/no-literal-string */} + ⛔️ + {t('errors.403-title')} + + {t('errors.403-body')} + + + ) +} + +export const getStaticProps: GetStaticProps = async ({ locale }) => { + return { + props: { + ...(await getServerSideTranslations(locale, ['common'])), + }, + revalidate: 60 * 60 * 24 * 7, + } +} + +export default Forbidden diff --git a/apps/app/src/pages/admin/index.tsx b/apps/app/src/pages/admin/index.tsx new file mode 100644 index 0000000000..37563e4d4c --- /dev/null +++ b/apps/app/src/pages/admin/index.tsx @@ -0,0 +1,62 @@ +import { Container, Stack, Title } from '@mantine/core' +import { type GetServerSideProps, type NextPage } from 'next' +import Head from 'next/head' +import { useSession } from 'next-auth/react' +import { useTranslation } from 'next-i18next' +import { type Route, route } from 'nextjs-routes' + +import { checkPermissions, getServerSession } from '@weareinreach/auth' +import { OrganizationTable } from '@weareinreach/ui/components/data-portal/OrganizationTable' +import { getServerSideTranslations } from '~app/utils/i18n' + +const AdminIndex: NextPage = () => { + const { t } = useTranslation(['common']) + const { data: session, status } = useSession() + return ( + <> + + {t('page-title.base', { title: 'Data Admin' })} + + {/* */} + + {t('welcome-name', { name: session?.user?.name })} + + + {/* */} + + ) +} +export default AdminIndex + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const session = await getServerSession(ctx) + if (!session) { + const callbackRoute: Route = { + pathname: '/admin', + } + const callbackUrl = Buffer.from(JSON.stringify(callbackRoute)).toString('base64url') + return { + redirect: { + destination: route({ pathname: '/401', query: { callbackUrl } }), + permanent: false, + }, + } + } + const hasPermissions = checkPermissions({ session, permissions: 'root', has: 'some' }) + + if (!hasPermissions) { + return { + redirect: { + destination: '/403', + permanent: false, + }, + } + } + + return { + props: { + session, + ...(await getServerSideTranslations(ctx.locale, ['common'])), + }, + } +} diff --git a/apps/app/src/types/nextjs-routes.d.ts b/apps/app/src/types/nextjs-routes.d.ts index 371fc7a4e9..209036fa75 100644 --- a/apps/app/src/types/nextjs-routes.d.ts +++ b/apps/app/src/types/nextjs-routes.d.ts @@ -11,11 +11,14 @@ declare module "nextjs-routes" { } from "next"; export type Route = + | StaticRoute<"/401"> + | StaticRoute<"/403"> | StaticRoute<"/404"> | StaticRoute<"/500"> | StaticRoute<"/account"> | StaticRoute<"/account/reviews"> | StaticRoute<"/account/saved"> + | StaticRoute<"/admin"> | StaticRoute<"/admin/quicklink/email"> | StaticRoute<"/admin/quicklink"> | StaticRoute<"/admin/quicklink/phone"> diff --git a/package.json b/package.json index 69a8d3eb1c..2ccf930cac 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "chokidar": ">=3.0.0", "csstype": "^3.1.2", "eslint-plugin-import": "npm:eslint-plugin-i@latest", + "eslint-plugin-node": "npm:eslint-plugin-n@latest", "glob-parent@<5.1.2": "^5.1.2", "http-cache-semantics@<=4.1.0": "^4.1.1", "listr2@<5": "^5.0.5", @@ -91,7 +92,6 @@ }, "patchedDependencies": { "@crowdin/ota-client@1.0.0": "patches/@crowdin__ota-client@1.0.0.patch", - "eslint-plugin-node@11.1.0": "patches/eslint-plugin-node@11.1.0.patch", "iso-google-locales@3.0.4": "patches/iso-google-locales@3.0.4.patch", "trpc-panel@1.3.4": "patches/trpc-panel@1.3.4.patch" }, diff --git a/packages/api/router/location/query.forGoogleMaps.handler.ts b/packages/api/router/location/query.forGoogleMaps.handler.ts index 1890cbbecb..6d33ccfd88 100644 --- a/packages/api/router/location/query.forGoogleMaps.handler.ts +++ b/packages/api/router/location/query.forGoogleMaps.handler.ts @@ -44,9 +44,12 @@ export const forGoogleMaps = async ({ input }: TRPCHandlerParams 1 ? getBoundary(coordsForBounds) : null + const singleLat = result.at(0)?.latitude + const singleLon = result.at(0)?.longitude + const center = - result.length === 1 && result.at(0)?.latitude && result.at(0)?.longitude - ? ({ lat: result.at(0)!.latitude, lng: result.at(0)!.longitude } as { lat: number; lng: number }) + result.length === 1 && singleLat && singleLon + ? ({ lat: singleLat, lng: singleLon } satisfies google.maps.LatLngLiteral) : getCenter(coordsForBounds) const zoom = result.length === 1 ? 17 : null diff --git a/packages/config/tsconfig/base.json b/packages/config/tsconfig/base.json index 97519a9275..8b042c2e75 100644 --- a/packages/config/tsconfig/base.json +++ b/packages/config/tsconfig/base.json @@ -17,7 +17,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "es2017" + "target": "es2020" }, "display": "Default", "include": ["../../@types/**/*.ts"], diff --git a/packages/eslint-config/base.js b/packages/eslint-config/base.js index 77928b322c..27e40fa271 100644 --- a/packages/eslint-config/base.js +++ b/packages/eslint-config/base.js @@ -1,4 +1,3 @@ -/* eslint-disable import/no-unused-modules */ /** @type {import('eslint').ESLint.ConfigData} */ const config = { plugins: ['codegen', 'turbo', 'node', /*'import',*/ '@tanstack/query'], @@ -12,6 +11,10 @@ const config = { 'prettier', ], rules: { + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' }, + ], '@typescript-eslint/consistent-type-imports': [ 'error', { @@ -32,7 +35,11 @@ const config = { ], '@typescript-eslint/no-empty-function': 'off', 'no-duplicate-imports': 'off', + 'node/no-deprecated-api': 'error', 'node/no-process-env': 'warn', + 'node/no-unsupported-features/es-builtins': 'error', + 'node/no-unsupported-features/es-syntax': 'error', + 'node/no-unsupported-features/node-builtins': 'error', 'codegen/codegen': 'error', 'react/jsx-key': 'off', 'react/no-unescaped-entities': 'off', @@ -103,21 +110,28 @@ const config = { ], parser: '@typescript-eslint/parser', parserOptions: { - project: ['./packages/*/tsconfig.json', './apps/*/tsconfig.json', './tsconfig.json'], + EXPERIMENTAL_useProjectService: true, + // project: [ './packages/*/tsconfig.json', './apps/*/tsconfig.json', './tsconfig.json' ], + emitDecoratorMetadata: true, + ecmaVersion: 2020, }, ignorePatterns: ['!.*', 'node_modules', 'dist/', '.next/'], settings: { + 'import/cache': { + lifetime: 60, + }, 'import/extensions': ['.js', '.jsx', '.cjs', '.mjs', '.ts', '.mts', '.tsx'], + 'import/internal-regex': '^(?:(?:@weareinreach\\/)|(?:~\\w*\\/)).*', + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx', '.mts'], + }, 'import/resolver': { node: true, typescript: { alwaysTryTypes: true, + project: ['./packages/*/tsconfig.json', './apps/*/tsconfig.json', './tsconfig.json'], }, }, - 'import/cache': { - lifetime: 10, - }, - 'import/internal-regex': '^(?:(?:@weareinreach\\/)|(?:~\\w*\\/)).*', }, env: { node: true, diff --git a/packages/ui/.storybook/i18next.ts b/packages/ui/.storybook/i18next.ts index d33102dd39..1a4f8e1dfb 100644 --- a/packages/ui/.storybook/i18next.ts +++ b/packages/ui/.storybook/i18next.ts @@ -36,7 +36,7 @@ i18n interpolation: { escapeValue: true, skipOnVariables: false, - format: (value, format, lng, edit) => { + format: (value, format) => { switch (format) { case 'lowercase': { if (typeof value === 'string') return value.toLowerCase() diff --git a/packages/ui/components/core/SearchBox.stories.tsx b/packages/ui/components/core/SearchBox.stories.tsx index 6ed272e5bd..09111112df 100644 --- a/packages/ui/components/core/SearchBox.stories.tsx +++ b/packages/ui/components/core/SearchBox.stories.tsx @@ -49,7 +49,7 @@ export default { ), -} as Meta +} satisfies Meta type StoryDef = StoryObj export const ByLocation = { diff --git a/packages/ui/components/core/SocialLink.stories.tsx b/packages/ui/components/core/SocialLink.stories.tsx index e8f4d0fcea..f341320cfb 100644 --- a/packages/ui/components/core/SocialLink.stories.tsx +++ b/packages/ui/components/core/SocialLink.stories.tsx @@ -17,7 +17,7 @@ export default { type: 'string', }, }, -} as Meta +} satisfies Meta type StoryDef = StoryObj type StoryGroupDef = StoryObj diff --git a/packages/ui/components/core/UserMenu.stories.tsx b/packages/ui/components/core/UserMenu.stories.tsx index b70fb44cf7..b77230f62a 100644 --- a/packages/ui/components/core/UserMenu.stories.tsx +++ b/packages/ui/components/core/UserMenu.stories.tsx @@ -5,7 +5,7 @@ import { UserMenu as UserMenuComponent } from '.' export default { title: 'Sections/Navbar/User Menu', component: UserMenuComponent, -} as Meta +} satisfies Meta export const LoggedOut = { parameters: { diff --git a/packages/ui/components/data-portal/OrganizationTable.tsx b/packages/ui/components/data-portal/OrganizationTable.tsx index 73790973a1..3b833479da 100644 --- a/packages/ui/components/data-portal/OrganizationTable.tsx +++ b/packages/ui/components/data-portal/OrganizationTable.tsx @@ -5,13 +5,17 @@ import { type MRT_ColumnDef, type MRT_ColumnFilterFnsState, type MRT_ColumnFiltersState, + type MRT_Row, type MRT_SortingState, + type MRT_TableInstance, type MRT_Virtualizer, useMantineReactTable, } from 'mantine-react-table' -import { useRouter } from 'next/router' +import { type Route } from 'nextjs-routes' import { type Dispatch, type SetStateAction, useMemo, useRef, useState } from 'react' +import { type ApiOutput } from '@weareinreach/api' +import { Link } from '~ui/components/core/Link' import { Icon } from '~ui/icon' import { trpc as api } from '~ui/lib/trpcClient' @@ -27,6 +31,25 @@ const useStyles = createStyles((theme) => ({ }, })) +const getAlertBanner = ({ + isError, + isFetching, + isLoading, +}: Record<'isError' | 'isFetching' | 'isLoading', boolean>) => { + switch (true) { + case isError: { + return { color: 'red', children: 'Error fetching data' } + } + case isFetching: + case isLoading: { + return { color: 'green', children: 'Loading data' } + } + default: { + return { color: 'white', children: null, sx: { backgroundColor: 'transparent' } } + } + } +} + const ToolbarButtons = ({ columnFilters, setColumnFilters }: ToolbarButtonsProps) => { const theme = useMantineTheme() const toggle = (key: 'published' | 'deleted') => { @@ -101,15 +124,77 @@ const ToolbarButtons = ({ columnFilters, setColumnFilters }: ToolbarButtonsProps ) } +const BottomBar = ({ table }: BottomBarProps) => { + const { classes } = useStyles() + const filteredRowCount = table.getFilteredRowModel().rows.length + const preFilteredRowCount = table.getPreFilteredRowModel().rows.length + + if (preFilteredRowCount !== filteredRowCount) { + return ( +
+ + Showing {filteredRowCount} of {preFilteredRowCount} results + +
+ ) + } + + return ( +
+ {preFilteredRowCount} results +
+ ) +} + +const RowAction = ({ row }: RowActionProps) => { + const getViewUrl = (): Route => { + const parent = row.getParentRow() + if (parent) { + return { + pathname: '/org/[slug]/[orgLocationId]', + query: { slug: parent.original.slug, orgLocationId: row.original.id }, + } + } else { + return { pathname: '/org/[slug]', query: { slug: row.original.slug } } + } + } + const getEditUrl = (): Route => { + const parent = row.getParentRow() + if (parent) { + return { + pathname: '/org/[slug]/[orgLocationId]/edit', + query: { slug: parent.original.slug, orgLocationId: row.original.id }, + } + } else { + return { pathname: '/org/[slug]/edit', query: { slug: row.original.slug } } + } + } + return ( + + + + + + + + + + + + + ) +} + export const OrganizationTable = () => { const { classes } = useStyles() - const router = useRouter() const { data, isLoading, isError, isFetching } = api.organization.forOrganizationTable.useQuery(undefined, { placeholderData: [], select: (data) => data.map(({ locations, ...rest }) => ({ ...rest, subRows: locations })), + refetchOnWindowFocus: false, }) - const columns = useMemo[number]>[]>( + // #region Column Definitions + const columns = useMemo[]>( () => [ { accessorKey: 'name', @@ -118,6 +203,15 @@ export const OrganizationTable = () => { filterVariant: 'autocomplete', enableResizing: true, minSize: 250, + enableColumnFilter: false, + Cell: ({ cell, row }) => + row.original.published ? ( + cell.getValue() + ) : ( + + {cell.getValue()} + + ), }, { accessorKey: 'lastVerified', @@ -200,7 +294,9 @@ export const OrganizationTable = () => { // eslint-disable-next-line react-hooks/exhaustive-deps [] ) + // #endregion + // #region State const [columnFilters, setColumnFilters] = useState([ { id: 'deleted', value: false }, ]) @@ -220,22 +316,9 @@ export const OrganizationTable = () => { { id: 'name', desc: false }, ]) const rowVirtualizerInstanceRef = useRef>(null) + // #endregion - const getAlertBanner = () => { - switch (true) { - case isError: { - return { color: 'red', children: 'Error fetching data' } - } - case isFetching: - case isLoading: { - return { color: 'green', children: 'Loading data' } - } - default: { - return { color: 'white', children: null, sx: { backgroundColor: 'transparent' } } - } - } - } - + // #region Table Setup const table = useMantineReactTable({ // #region Basic Props columns, @@ -258,10 +341,11 @@ export const OrganizationTable = () => { enableHiding: true, getRowId: (originalRow) => originalRow.id, isMultiSortEvent: () => true, + maxLeafRowFilterDepth: 0, positionGlobalFilter: 'left', rowCount: data?.length ?? 0, rowVirtualizerInstanceRef, - rowVirtualizerProps: { overscan: 5 }, + rowVirtualizerProps: { overscan: 10, estimateSize: () => 56 }, // #endregion // #region State initialState: { @@ -278,7 +362,7 @@ export const OrganizationTable = () => { columnFilters, globalFilter, isLoading, - showAlertBanner: getAlertBanner !== undefined, + showAlertBanner: isError || isFetching || isLoading, showProgressBars: isFetching, sorting, density: 'xs', @@ -288,91 +372,22 @@ export const OrganizationTable = () => { mantinePaperProps: { miw: '85%' }, mantineProgressProps: ({ isTopToolbar }) => ({ style: { display: isTopToolbar ? 'block' : 'none' } }), mantineSelectCheckboxProps: ({ row }) => ({ style: { display: row.getCanSelect() ? 'block' : 'none' } }), + mantineTableBodyProps: { mah: '60vh' }, mantineTableBodyCellProps: ({ row }) => ({ sx: (theme) => ({ textDecoration: row.original.deleted ? 'line-through' : 'none', color: row.original.published ? undefined : theme.other.colors.secondary.darkGray, }), }), - mantineToolbarAlertBannerProps: getAlertBanner(), + mantineToolbarAlertBannerProps: getAlertBanner({ isLoading, isFetching, isError }), mantineTableProps: { striped: true }, // #endregion // #region Override sections renderToolbarInternalActions: () => ( ), - renderBottomToolbar: ({ table }) => { - if (table.getPreFilteredRowModel().rows.length !== table.getFilteredRowModel().rows.length) { - return ( -
- - Showing {table.getFilteredRowModel().rows.length} of{' '} - {table.getPreFilteredRowModel().rows.length} results - -
- ) - } - return ( -
- {table.getFilteredRowModel().rows.length} results -
- ) - }, - renderRowActions: ({ row }) => { - const handleView = () => { - const parent = row.getParentRow() - if (parent) { - router.push({ - pathname: '/org/[slug]/[orgLocationId]', - query: { - slug: parent.original.slug, - orgLocationId: row.original.id, - }, - }) - } else { - router.push({ - pathname: '/org/[slug]', - query: { - slug: row.original.slug, - }, - }) - } - } - const handleEdit = () => { - const parent = row.getParentRow() - if (parent) { - router.push({ - pathname: '/org/[slug]/[orgLocationId]/edit', - query: { - slug: parent.original.slug, - orgLocationId: row.original.id, - }, - }) - } else { - router.push({ - pathname: '/org/[slug]/edit', - query: { - slug: row.original.slug, - }, - }) - } - } - - return ( - - - - - - - - - - - - - ) - }, + renderBottomToolbar: ({ table }) => , + renderRowActions: ({ row }) => , // #endregion // #region Events onColumnFilterFnsChange: setColumnFilterFns, @@ -381,6 +396,7 @@ export const OrganizationTable = () => { onSortingChange: setSorting, // #endregion }) + // #endregion return } @@ -389,3 +405,12 @@ interface ToolbarButtonsProps { columnFilters: MRT_ColumnFiltersState setColumnFilters: Dispatch> } +interface BottomBarProps { + table: MRT_TableInstance +} +interface RowActionProps { + row: MRT_Row +} +type RestucturedDataItem = Omit & { + subRows: ApiOutput['organization']['forOrganizationTable'][number]['locations'] +} diff --git a/packages/ui/lib/trpcClient.ts b/packages/ui/lib/trpcClient.ts index 14e87a4f55..995844a737 100644 --- a/packages/ui/lib/trpcClient.ts +++ b/packages/ui/lib/trpcClient.ts @@ -1,6 +1,10 @@ /* eslint-disable turbo/no-undeclared-env-vars */ /* eslint-disable node/no-process-env */ -import { httpBatchLink, loggerLink } from '@trpc/client' +import { + // httpBatchLink, + unstable_httpBatchStreamLink as httpBatchStreamLink, + loggerLink, +} from '@trpc/client' import { createTRPCNext } from '@trpc/next' import { createTRPCReact } from '@trpc/react-query' import { devtoolsLink } from 'trpc-client-devtools-link' @@ -12,7 +16,7 @@ import { getEnv } from '@weareinreach/env' export const getBaseUrl = () => { if (typeof window !== 'undefined') return '' // browser should use relative url if (getEnv('VERCEL_URL')) return `https://${getEnv('VERCEL_URL')}` // SSR should use vercel url - return `http://localhost:${getEnv('PORT') ?? 6006}` // dev SSR should use localhost + return `http://localhost:${getEnv('PORT') ?? process.env.STORYBOOK ? 6006 : 3000}` // dev SSR should use localhost } export const nextTRPC = () => @@ -22,18 +26,19 @@ export const nextTRPC = () => transformer, links: [ devtoolsLink({ - // eslint-disable-next-line node/no-process-env enabled: process.env.NODE_ENV === 'development', }), loggerLink({ enabled: (opts) => - // eslint-disable-next-line node/no-process-env process.env.NODE_ENV === 'development' || (opts.direction === 'down' && opts.result instanceof Error), }), - httpBatchLink({ + httpBatchStreamLink({ url: `${getBaseUrl()}/api/trpc`, }), + // httpBatchLink({ + // url: `${getBaseUrl()}/api/trpc`, + // }), ], queryClientConfig: { defaultOptions: { diff --git a/packages/ui/modals/Login.stories.tsx b/packages/ui/modals/Login.stories.tsx index 7e742c09cb..e3430c924d 100644 --- a/packages/ui/modals/Login.stories.tsx +++ b/packages/ui/modals/Login.stories.tsx @@ -1,9 +1,9 @@ -import { type Meta } from '@storybook/react' +import { type Meta, type StoryObj } from '@storybook/react' import { Button } from '~ui/components/core/Button' import { cognito, csrf, providers, signin } from '~ui/mockData/login' -import { LoginModalLauncher } from './Login' +import { LoginBody, LoginModalLauncher } from './Login' export default { title: 'Modals/Login', @@ -26,3 +26,9 @@ export default { } satisfies Meta export const Modal = {} +export const BodyOnly = { + parameters: { + layoutWrapper: 'centeredFullscreen', + }, + render: () => , +} satisfies StoryObj diff --git a/packages/ui/modals/Login.tsx b/packages/ui/modals/Login.tsx index 41f1c2e717..22c6e1896f 100644 --- a/packages/ui/modals/Login.tsx +++ b/packages/ui/modals/Login.tsx @@ -12,8 +12,10 @@ import { } from '@mantine/core' import { useForm, zodResolver } from '@mantine/form' import { useDisclosure } from '@mantine/hooks' +import { useRouter } from 'next/router' import { signIn } from 'next-auth/react' import { Trans, useTranslation } from 'next-i18next' +import { type Route } from 'nextjs-routes' import { forwardRef, useState } from 'react' import { z } from 'zod' @@ -26,43 +28,119 @@ import { ModalTitle } from './ModalTitle' import { PrivacyStatementModal } from './PrivacyStatement' import { SignupModalLauncher } from './SignUp' +interface LoginBodyProps { + activateShake?: () => void + modalHandler?: { + readonly open: () => void + readonly close: () => void + readonly toggle: () => void + } + hideTitle?: boolean + callbackUrl?: Route +} +export const LoginBody = forwardRef( + ({ activateShake, modalHandler, hideTitle, callbackUrl }, ref) => { + const [isLoading, setLoading] = useState(false) + const variants = useCustomVariant() + const { t } = useTranslation(['common']) + const router = useRouter() + const loginErrors = new Map([[401, t('login.error-username-password')]]) + const LoginSchema = z.object({ + email: z.string().email({ message: t('form-error-enter-valid-email') as string }), + password: z.string().min(1, t('form-error-password-blank') as string), + }) + const form = useForm({ + validate: zodResolver(LoginSchema), + validateInputOnBlur: true, + }) + const loginHandle = async (email: string, password: string) => { + try { + setLoading(true) + if (!form.isValid()) return + const result = await signIn('cognito', { email, password, redirect: false }) + if (result?.error) { + const message = loginErrors.get(result.status) + form.setFieldError('password', message ?? t('login.error-generic')) + if (typeof activateShake === 'function') { + activateShake() + } + } + if (result?.ok) { + if (modalHandler) { + modalHandler.close() + } else if (callbackUrl) { + router.push(callbackUrl) + } + } + } finally { + setLoading(false) + } + } + return ( + + {hideTitle ? null : {t('log-in')}} + + + + + + Privacy Policy + + ), + link2: ( + + Terms of Use + + ), + }} + /> + + + {t('forgot-password')} + {t('dont-have-account')} + + + ) + } +) +LoginBody.displayName = 'LoginBody' + export const LoginModalBody = forwardRef((props, ref) => { - const { t } = useTranslation(['common']) const [opened, handler] = useDisclosure(false) const { animateCSS, fireEvent } = useShake({ variant: 1 }) - const [isLoading, setLoading] = useState(false) const { isMobile } = useScreenSize() - const loginErrors = new Map([[401, t('login.error-username-password')]]) - const modalTitle = handler.close() }} /> - - const LoginSchema = z.object({ - email: z.string().email({ message: t('form-error-enter-valid-email') as string }), - password: z.string().min(1, t('form-error-password-blank') as string), - }) - const form = useForm({ - validate: zodResolver(LoginSchema), - validateInputOnBlur: true, - }) - const variants = useCustomVariant() - const loginHandle = async (email: string, password: string) => { - try { - setLoading(true) - if (!form.isValid()) return - const result = await signIn('cognito', { email, password, redirect: false }) - if (result?.error) { - const message = loginErrors.get(result.status) - form.setFieldError('password', message ?? t('login.error-generic')) - fireEvent() - } - if (result?.ok) { - handler.close() - } - } finally { - setLoading(false) - } - } - return ( <> className={animateCSS} fullScreen={isMobile} > - - {t('log-in')} - - - - - - Privacy Policy - - ), - link2: ( - - Terms of Use - - ), - }} - /> - - - {t('forgot-password')} - {t('dont-have-account')} - - + handler.open()} {...props} /> diff --git a/packages/ui/modals/MoreFilter.stories.tsx b/packages/ui/modals/MoreFilter.stories.tsx index e9bf4505ab..13dcdcf676 100644 --- a/packages/ui/modals/MoreFilter.stories.tsx +++ b/packages/ui/modals/MoreFilter.stories.tsx @@ -35,6 +35,6 @@ export default { }, [filter]) return }, -} as Meta +} satisfies Meta export const MoreFilterExample = {} diff --git a/packages/ui/modals/ServiceFilter.stories.tsx b/packages/ui/modals/ServiceFilter.stories.tsx index acdcefc75f..083b776fcc 100644 --- a/packages/ui/modals/ServiceFilter.stories.tsx +++ b/packages/ui/modals/ServiceFilter.stories.tsx @@ -39,6 +39,6 @@ export default { return }, -} as Meta +} satisfies Meta export const ServiceFilterExample = {} diff --git a/packages/ui/theme/colors.ts b/packages/ui/theme/colors.ts index acbfa541bf..0f90779dc7 100644 --- a/packages/ui/theme/colors.ts +++ b/packages/ui/theme/colors.ts @@ -287,7 +287,7 @@ export const customColors = { '#136776', '#0e4b56', ], -} as DefineColors +} satisfies DefineColors /** Merge custom color names with Mantine's presets */ type ExtendedCustomColors = CustomColors | DefaultMantineColor diff --git a/patches/eslint-plugin-node@11.1.0.patch b/patches/eslint-plugin-node@11.1.0.patch deleted file mode 100644 index 89d0492f73..0000000000 --- a/patches/eslint-plugin-node@11.1.0.patch +++ /dev/null @@ -1,81 +0,0 @@ -diff --git a/lib/rules/no-process-env.js b/lib/rules/no-process-env.js -index f46f00ec06b6bc870588579a5a8a7bd82c06eaea..5226a1e44c805b7542c70c592dd076d5ab53f251 100644 ---- a/lib/rules/no-process-env.js -+++ b/lib/rules/no-process-env.js -@@ -2,44 +2,44 @@ - * @author Vignesh Anand - * See LICENSE file in root directory for full license. - */ --"use strict" -+'use strict' - - //------------------------------------------------------------------------------ - // Rule Definition - //------------------------------------------------------------------------------ - - module.exports = { -- meta: { -- type: "suggestion", -- docs: { -- description: "disallow the use of `process.env`", -- category: "Stylistic Issues", -- recommended: false, -- url: -- "https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/no-process-env.md", -- }, -- fixable: null, -- schema: [], -- messages: { -- unexpectedProcessEnv: "Unexpected use of process.env.", -- }, -- }, -+ meta: { -+ type: 'suggestion', -+ docs: { -+ description: 'disallow the use of `process.env`', -+ category: 'Stylistic Issues', -+ recommended: false, -+ url: 'https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/no-process-env.md', -+ }, -+ fixable: null, -+ schema: [], -+ messages: { -+ unexpectedProcessEnv: -+ "Do not use 'process.env' - use the 'env' object from @weareinreach/config.", -+ }, -+ }, - -- create(context) { -- return { -- MemberExpression(node) { -- const objectName = node.object.name -- const propertyName = node.property.name -+ create(context) { -+ return { -+ MemberExpression(node) { -+ const objectName = node.object.name -+ const propertyName = node.property.name - -- if ( -- objectName === "process" && -- !node.computed && -- propertyName && -- propertyName === "env" -- ) { -- context.report({ node, messageId: "unexpectedProcessEnv" }) -- } -- }, -- } -- }, -+ if ( -+ objectName === 'process' && -+ !node.computed && -+ propertyName && -+ propertyName === 'env' -+ ) { -+ context.report({ node, messageId: 'unexpectedProcessEnv' }) -+ } -+ }, -+ } -+ }, - } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b93ffe6ed4..19d7bf56b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ overrides: chokidar: '>=3.0.0' csstype: ^3.1.2 eslint-plugin-import: npm:eslint-plugin-i@latest + eslint-plugin-node: npm:eslint-plugin-n@latest glob-parent@<5.1.2: ^5.1.2 http-cache-semantics@<=4.1.0: ^4.1.1 listr2@<5: ^5.0.5 @@ -32,9 +33,6 @@ patchedDependencies: '@crowdin/ota-client@1.0.0': hash: refrge56ym5gomc3tkglzjdymy path: patches/@crowdin__ota-client@1.0.0.patch - eslint-plugin-node@11.1.0: - hash: 45p4dc3r2kwi3h2jyimmny42ju - path: patches/eslint-plugin-node@11.1.0.patch iso-google-locales@3.0.4: hash: ltnamflm7ayajalculwqyezjya path: patches/iso-google-locales@3.0.4.patch @@ -328,6 +326,9 @@ importers: luxon: specifier: 3.4.0 version: 3.4.0 + mantine-react-table: + specifier: 1.1.2 + version: 1.1.2(@emotion/react@11.11.1)(@mantine/core@6.0.19)(@mantine/dates@6.0.19)(@mantine/hooks@6.0.19)(@tabler/icons-react@2.30.0)(react-dom@18.2.0)(react@18.2.0) next: specifier: 13.4.18 version: 13.4.18(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) @@ -1343,8 +1344,8 @@ importers: specifier: npm:eslint-plugin-i@latest version: /eslint-plugin-i@2.28.0-2(@typescript-eslint/parser@6.4.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.47.0) eslint-plugin-node: - specifier: 11.1.0 - version: 11.1.0(patch_hash=45p4dc3r2kwi3h2jyimmny42ju)(eslint@8.47.0) + specifier: npm:eslint-plugin-n@latest + version: /eslint-plugin-n@16.0.1(eslint@8.47.0) eslint-plugin-react: specifier: 7.33.2 version: 7.33.2(eslint@8.47.0) @@ -10011,7 +10012,6 @@ packages: engines: {node: '>=12'} dependencies: remove-accents: 0.4.2 - dev: true /@tanstack/query-core@4.32.6: resolution: {integrity: sha512-YVB+mVWENQwPyv+40qO7flMgKZ0uI41Ph7qXC2Zf1ft5AIGfnXnMZyifB2ghhZ27u+5wm5mlzO4Y6lwwadzxCA==} @@ -10089,7 +10089,6 @@ packages: dependencies: '@tanstack/virtual-core': 3.0.0-beta.54 react: 18.2.0 - dev: true /@tanstack/table-core@8.9.3: resolution: {integrity: sha512-NpHZBoHTfqyJk0m/s/+CSuAiwtebhYK90mDuf5eylTvgViNOujiaOaxNDxJkQQAsVvHWZftUGAx1EfO1rkKtLg==} @@ -10097,7 +10096,6 @@ packages: /@tanstack/virtual-core@3.0.0-beta.54: resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==} - dev: true /@terraformer/wkt@2.2.0: resolution: {integrity: sha512-i33rTSqPtmO4sRdeznI0IEc9gpIZZIXN5kGhZ4rTwVtDccDKL3h4uia9cmWdRJlJMlG4Febxatw5b9ylI5YYuA==} @@ -12653,7 +12651,6 @@ packages: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: semver: 7.5.4 - dev: false /bundle-name@3.0.0: resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} @@ -14664,15 +14661,15 @@ packages: - supports-color dev: true - /eslint-plugin-es@3.0.1(eslint@8.47.0): - resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} - engines: {node: '>=8.10.0'} + /eslint-plugin-es-x@7.2.0(eslint@8.47.0): + resolution: {integrity: sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - eslint: '>=4.19.1' + eslint: '>=8' dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0) + '@eslint-community/regexpp': 4.6.2 eslint: 8.47.0 - eslint-utils: 2.1.0 - regexpp: 3.2.0 dev: true /eslint-plugin-i18next@6.0.3: @@ -14731,21 +14728,22 @@ packages: semver: 6.3.1 dev: true - /eslint-plugin-node@11.1.0(patch_hash=45p4dc3r2kwi3h2jyimmny42ju)(eslint@8.47.0): - resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} - engines: {node: '>=8.10.0'} + /eslint-plugin-n@16.0.1(eslint@8.47.0): + resolution: {integrity: sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==} + engines: {node: '>=16.0.0'} peerDependencies: - eslint: '>=5.16.0' + eslint: '>=7.0.0' dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0) + builtins: 5.0.1 eslint: 8.47.0 - eslint-plugin-es: 3.0.1(eslint@8.47.0) - eslint-utils: 2.1.0 + eslint-plugin-es-x: 7.2.0(eslint@8.47.0) ignore: 5.2.4 + is-core-module: 2.13.0 minimatch: 3.1.2 resolve: 1.22.4 - semver: 6.3.1 + semver: 7.5.4 dev: true - patched: true /eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.47.0): resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} @@ -14826,18 +14824,6 @@ packages: estraverse: 5.3.0 dev: true - /eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - dependencies: - eslint-visitor-keys: 1.3.0 - dev: true - - /eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - dev: true - /eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -18681,7 +18667,6 @@ packages: '@tanstack/react-virtual': 3.0.0-beta.54(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} @@ -22173,11 +22158,6 @@ packages: define-properties: 1.2.0 functions-have-names: 1.2.3 - /regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} @@ -22325,7 +22305,6 @@ packages: /remove-accents@0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} - dev: true /renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==}