From 4f429c6f10223c13a458f3830a1e784c91b84314 Mon Sep 17 00:00:00 2001 From: John Ballesteros Date: Wed, 15 May 2024 10:03:43 +0800 Subject: [PATCH 1/6] feat: add internal-error-result component --- .../components/internal-error-result.tsx | 43 +++++++++++++++++++ src/shared/core/types.ts | 5 +++ 2 files changed, 48 insertions(+) create mode 100644 src/shared/components/internal-error-result.tsx diff --git a/src/shared/components/internal-error-result.tsx b/src/shared/components/internal-error-result.tsx new file mode 100644 index 00000000..e84ae4ea --- /dev/null +++ b/src/shared/components/internal-error-result.tsx @@ -0,0 +1,43 @@ +import { ErrorBoundaryProps } from '@/shared/core/types'; + +import { ErrorAction } from './error-action'; +import { ErrorActionButton } from './error-action-button'; + +const IMG_SRC = '/vortex.png'; +const IMAGE_CLASSNAME = 'animate-spin-slow'; +const HEADING_TEXT = 'Serious Error'; +const MESSAGE_TEXT = + 'All shortcuts have disappeared. Screen. Mind. Both are blank'; + +interface Props extends Partial { + onReset?: () => void; +} + +export const InternalErrorResult = ({ onReset, reset }: Props) => { + const text = { + heading: HEADING_TEXT, + message: MESSAGE_TEXT, + }; + + const img = { + src: IMG_SRC, + }; + + const classNames = { + image: IMAGE_CLASSNAME, + }; + + const onClick = () => { + if (reset) reset(); + if (onReset) onReset(); + }; + + return ( + } + /> + ); +}; diff --git a/src/shared/core/types.ts b/src/shared/core/types.ts index 637870d3..641445cf 100644 --- a/src/shared/core/types.ts +++ b/src/shared/core/types.ts @@ -6,3 +6,8 @@ export type InfoTagProps = { }; export type EnabledTagsConfig = Partial>; + +export type ErrorBoundaryProps = { + error: Error & { digest?: string }; + reset: () => void; +}; From 096a5db3beb919fcebac1720247481105a92b432 Mon Sep 17 00:00:00 2001 From: John Ballesteros Date: Wed, 15 May 2024 10:04:41 +0800 Subject: [PATCH 2/6] feat: add virtual-wrapper component --- src/shared/components/virtual-wrapper.tsx | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/shared/components/virtual-wrapper.tsx diff --git a/src/shared/components/virtual-wrapper.tsx b/src/shared/components/virtual-wrapper.tsx new file mode 100644 index 00000000..303ae502 --- /dev/null +++ b/src/shared/components/virtual-wrapper.tsx @@ -0,0 +1,59 @@ +import { useRef } from 'react'; + +import { useVirtualizer } from '@tanstack/react-virtual'; + +const OVERSCAN = 5; +const ESTIMATE_SIZE = 500; + +export const useVirtualWrapper = (count: number) => { + const parentRef = useRef(null); + const virtualizer = useVirtualizer({ + count, + getScrollElement: () => parentRef.current, + estimateSize: () => ESTIMATE_SIZE, + overscan: OVERSCAN, + scrollMargin: parentRef.current?.offsetTop ?? 0, + initialRect: { + width: ESTIMATE_SIZE, + height: ESTIMATE_SIZE * 20, + }, + }); + const items = virtualizer.getVirtualItems(); + + return { parentRef, virtualizer, items }; +}; + +interface Props { + count: number; + children: (index: number) => JSX.Element; +} + +export const VirtualWrapper = ({ count, children }: Props) => { + const { parentRef, virtualizer, items } = useVirtualWrapper(count); + + const getTotalSize = virtualizer.getTotalSize(); + const translateY = items[0] + ? items[0].start - virtualizer.options.scrollMargin + : 0; + + return ( +
+
+
+ {items.map((item) => ( +
+ {children(item.index)} +
+ ))} +
+
+
+ ); +}; From 11211aaf62963b98350f97b8b30976d9dac29a97 Mon Sep 17 00:00:00 2001 From: John Ballesteros Date: Wed, 15 May 2024 10:09:02 +0800 Subject: [PATCH 3/6] feat: add use-org-list hook --- src/orgs/components/org-list/use-org-list.ts | 80 ++++++++++++++++++++ src/orgs/core/atoms.ts | 4 + src/orgs/data/get-org-details.ts | 17 +++++ 3 files changed, 101 insertions(+) create mode 100644 src/orgs/components/org-list/use-org-list.ts create mode 100644 src/orgs/data/get-org-details.ts diff --git a/src/orgs/components/org-list/use-org-list.ts b/src/orgs/components/org-list/use-org-list.ts new file mode 100644 index 00000000..01573275 --- /dev/null +++ b/src/orgs/components/org-list/use-org-list.ts @@ -0,0 +1,80 @@ +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; + +import { useAtom } from 'jotai'; + +import { HREFS } from '@/shared/core/constants'; +import { getQueryClient } from '@/shared/utils/get-query-client'; + +import { orgQueryKeys } from '@/orgs/core/query-keys'; +import { initOrgAtom, orgTotalCountAtom } from '@/orgs/core/atoms'; +import { initPathAtom } from '@/shared/core/atoms'; + +import { useOrgListQuery } from './use-org-list-query'; + +import { getOrgDetails } from '@/orgs/data/get-org-details'; + +export const useOrgList = () => { + const queryClient = getQueryClient(); + + const { + data, + error, + fetchNextPage, + hasNextPage, + isSuccess, + isPending, + isFetching, + } = useOrgListQuery(); + + // Sync total count + const [totalCount, setTotalCount] = useAtom(orgTotalCountAtom); + useEffect(() => { + const currentTotal = data?.pages[0].total ?? 0; + if (data && currentTotal !== totalCount) { + setTotalCount(currentTotal); + } + }, [data, setTotalCount, totalCount]); + + // Prefetch org details + useEffect(() => { + if (isSuccess && data) { + const items = data.pages.flatMap((d) => d.data); + for (const item of items) { + const { orgId } = item; + queryClient.prefetchQuery({ + queryKey: orgQueryKeys.details(orgId), + queryFn: () => getOrgDetails(orgId), + }); + } + } + }, [data, isSuccess, queryClient]); + + // Next page fetch on scroll + const { ref: inViewRef } = useInView({ + threshold: 1, + onChange: (inView) => { + if (inView && !error && !isFetching) fetchNextPage(); + }, + }); + + const [initPath] = useAtom(initPathAtom); + const isOrgListSSR = initPath === HREFS.ORGS_PAGE; + + const [initOrg] = useAtom(initOrgAtom); + const allOrgs = data?.pages.flatMap((d) => d.data) ?? []; + + // Dedupe init-card if not list-page ssr + const orgs = !isOrgListSSR + ? allOrgs.filter((d) => d.orgId !== initOrg?.orgId) + : allOrgs; + + return { + orgs, + error, + inViewRef, + hasNextPage, + isSuccess, + isPending, + }; +}; diff --git a/src/orgs/core/atoms.ts b/src/orgs/core/atoms.ts index 7d66979f..14bf451b 100644 --- a/src/orgs/core/atoms.ts +++ b/src/orgs/core/atoms.ts @@ -1,3 +1,7 @@ import { atom } from 'jotai'; +import { OrgDetails } from './schemas'; + export const activeOrgIdAtom = atom(null); +export const initOrgAtom = atom(null); +export const orgTotalCountAtom = atom(null); diff --git a/src/orgs/data/get-org-details.ts b/src/orgs/data/get-org-details.ts new file mode 100644 index 00000000..fd33f7db --- /dev/null +++ b/src/orgs/data/get-org-details.ts @@ -0,0 +1,17 @@ +import { MW_URL } from '@/shared/core/envs'; +import { mwGET } from '@/shared/utils/mw-get'; + +import { orgDetailsSchema } from '@/orgs/core/schemas'; + +const label = 'getOrgDetails'; + +export const getOrgDetails = async (orgId: string) => { + const url = `${MW_URL}/organizations/details/${orgId}`; + + return mwGET({ + url, + label, + responseSchema: orgDetailsSchema, + options: { next: { revalidate: 60 * 60 } }, + }); +}; From e093ba0a9794316c1c06695f035c4b6e2d99cb3d Mon Sep 17 00:00:00 2001 From: John Ballesteros Date: Wed, 15 May 2024 10:11:13 +0800 Subject: [PATCH 4/6] feat: add init-org-card component --- src/orgs/components/init-org-card.tsx | 59 +++++++++++++++++++++++ src/orgs/utils/get-funding-rounds-data.ts | 17 +++++++ 2 files changed, 76 insertions(+) create mode 100644 src/orgs/components/init-org-card.tsx create mode 100644 src/orgs/utils/get-funding-rounds-data.ts diff --git a/src/orgs/components/init-org-card.tsx b/src/orgs/components/init-org-card.tsx new file mode 100644 index 00000000..8425f29d --- /dev/null +++ b/src/orgs/components/init-org-card.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { usePathname } from 'next/navigation'; + +import { Spinner } from '@nextui-org/spinner'; +import { useAtomValue } from 'jotai'; + +import { HREFS } from '@/shared/core/constants'; + +import { OrgListItem } from '@/orgs/core/schemas'; +import { getFundingRoundsData } from '@/orgs/utils/get-funding-rounds-data'; +import { initOrgAtom } from '@/orgs/core/atoms'; +import { initPathAtom } from '@/shared/core/atoms'; + +import { OrgCard } from './org-card'; + +interface Props { + filterParamsString: string; +} + +export const InitOrgCard = ({ filterParamsString }: Props) => { + const pathname = usePathname(); + + const initPath = useAtomValue(initPathAtom); + const initOrg = useAtomValue(initOrgAtom); + + // Do not render if initially on list page + if (initPath === HREFS.ORGS_PAGE) return null; + + // Do not render if on list page and no initOrg (mobile) + if (!initOrg && pathname === HREFS.ORGS_PAGE) return null; + + // Render initOrg if set + if (initOrg) { + const { lastFundingAmount, lastFundingDate } = getFundingRoundsData( + initOrg.fundingRounds, + ); + + const orgItem: OrgListItem = { + ...initOrg, + url: initOrg.website!, + jobCount: initOrg.jobs.length, + projectCount: initOrg.projects.length, + lastFundingAmount, + lastFundingDate, + community: [], // TODO: require mw to return community in org details + }; + return ( + + ); + } + + // Defaults to a skeleton + return ; +}; diff --git a/src/orgs/utils/get-funding-rounds-data.ts b/src/orgs/utils/get-funding-rounds-data.ts new file mode 100644 index 00000000..e037a6e5 --- /dev/null +++ b/src/orgs/utils/get-funding-rounds-data.ts @@ -0,0 +1,17 @@ +import { FundingRound } from '@/shared/core/schemas'; + +export const getFundingRoundsData = (fundingRounds: FundingRound[]) => { + let lastFundingAmount = 0; + let lastFundingDate = 0; + + if (fundingRounds.length > 0) { + for (const fundingRound of fundingRounds) { + if (fundingRound.date && fundingRound.date > (lastFundingDate ?? 0)) { + lastFundingDate = fundingRound.date; + lastFundingAmount = fundingRound.raisedAmount ?? 0; + } + } + } + + return { lastFundingAmount, lastFundingDate }; +}; From b1186281a57579f1b3a0ae59255fcf4bf506ce2b Mon Sep 17 00:00:00 2001 From: John Ballesteros Date: Wed, 15 May 2024 10:13:01 +0800 Subject: [PATCH 5/6] feat: add org-list component --- package.json | 1 + pnpm-lock.yaml | 3 ++ src/orgs/components/org-list/index.tsx | 73 ++++++++++++++++++++++++++ tailwind.config.ts | 2 +- 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/orgs/components/org-list/index.tsx diff --git a/package.json b/package.json index beb6ac4d..9a584c58 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@nextui-org/button": "^2.0.31", "@nextui-org/link": "^2.0.29", "@nextui-org/skeleton": "^2.0.27", + "@nextui-org/spinner": "^2.0.28", "@nextui-org/system": "0.0.0-canary-20240504162810", "@nextui-org/theme": "^2.2.3", "@tanstack/react-query": "^5.36.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0da14d31..2650721b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@nextui-org/skeleton': specifier: ^2.0.27 version: 2.0.27(@nextui-org/theme@2.2.3(tailwindcss@3.4.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-variants@0.2.1(tailwindcss@3.4.3)) + '@nextui-org/spinner': + specifier: ^2.0.28 + version: 2.0.28(@nextui-org/theme@2.2.3(tailwindcss@3.4.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-variants@0.2.1(tailwindcss@3.4.3)) '@nextui-org/system': specifier: 0.0.0-canary-20240504162810 version: 0.0.0-canary-20240504162810(@nextui-org/theme@2.2.3(tailwindcss@3.4.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/src/orgs/components/org-list/index.tsx b/src/orgs/components/org-list/index.tsx new file mode 100644 index 00000000..0e763978 --- /dev/null +++ b/src/orgs/components/org-list/index.tsx @@ -0,0 +1,73 @@ +import { Spinner } from '@nextui-org/spinner'; + +import { cn } from '@/shared/utils/cn'; +import { reloadPage } from '@/shared/utils/reload-page'; +import { InternalErrorResult } from '@/shared/components/internal-error-result'; +import { VirtualWrapper } from '@/shared/components/virtual-wrapper'; + +import { InitOrgCard } from '@/orgs/components/init-org-card'; +import { OrgCard } from '@/orgs/components/org-card'; +import { useFiltersContext } from '@/filters/providers/filters-provider/context'; + +import { useOrgList } from './use-org-list'; + +export const OrgList = () => { + const { + orgs, + error, + inViewRef, + hasNextPage, + isSuccess, + isPending: isPendingOrgs, + } = useOrgList(); + + const hasOrgs = orgs.length > 0; + + const { isPendingFilters, filterSearchParams } = useFiltersContext(); + + const isPending = isPendingFilters || isPendingOrgs; + + const filterParamsString = + filterSearchParams.size > 0 ? `?${filterSearchParams}` : ''; + + if (error) { + return ; + } + + if (isPending) { + return ; + } + + if (isSuccess) { + if (hasOrgs) { + return ( + <> +
+ +
+ + {(index) => ( +
0 })}> + +
+ )} +
+ {hasNextPage ? ( +
+ +
+ ) : ( +

No more organizations available.

+ )} + + ); + } else { + return

No organizations found.

; + } + } + + return null; +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index cbadb126..c8dc79c9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -5,7 +5,7 @@ const config: Config = { content: [ './src/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', - './node_modules/@nextui-org/theme/dist/components/(button|link|skeleton).js', + './node_modules/@nextui-org/theme/dist/components/(button|link|skeleton|spinner).js', ], safelist: [ { From 6b08521f2de61f293f1b3afba1ecc7dbf02b2952 Mon Sep 17 00:00:00 2001 From: John Ballesteros Date: Wed, 15 May 2024 10:15:00 +0800 Subject: [PATCH 6/6] chore: update eslint import sort rule --- .eslintrc.json | 4 ++-- src/orgs/components/org-list/use-org-list.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7299f093..708763fd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,7 +33,7 @@ "^@/shared/core", "^@/shared/utils", "^@/shared/atoms", - "^@/shared/api", + "^@/shared/data", "^@/shared/hooks", "^@/shared/components", "^@/shared/providers" @@ -43,7 +43,7 @@ "^(@)(/.*/core|$)", "^(@)(/.*/utils|$)", "^(@)(/.*/atoms|$)", - "^(@)(/.*/api|$)", + "^(@)(/.*/data|$)", "^(@)(/.*/hooks|$)", "^(@)(/.*/components|$)", "^(@)(/.*/providers|$)" diff --git a/src/orgs/components/org-list/use-org-list.ts b/src/orgs/components/org-list/use-org-list.ts index 01573275..83e5f8e5 100644 --- a/src/orgs/components/org-list/use-org-list.ts +++ b/src/orgs/components/org-list/use-org-list.ts @@ -9,11 +9,10 @@ import { getQueryClient } from '@/shared/utils/get-query-client'; import { orgQueryKeys } from '@/orgs/core/query-keys'; import { initOrgAtom, orgTotalCountAtom } from '@/orgs/core/atoms'; import { initPathAtom } from '@/shared/core/atoms'; +import { getOrgDetails } from '@/orgs/data/get-org-details'; import { useOrgListQuery } from './use-org-list-query'; -import { getOrgDetails } from '@/orgs/data/get-org-details'; - export const useOrgList = () => { const queryClient = getQueryClient();