diff --git a/src/app/grants/[grantId]/grantees/[granteeId]/layout.tsx b/src/app/grants/[grantId]/grantees/[granteeId]/layout.tsx index 15c89b9..6874321 100644 --- a/src/app/grants/[grantId]/grantees/[granteeId]/layout.tsx +++ b/src/app/grants/[grantId]/grantees/[granteeId]/layout.tsx @@ -1,4 +1,3 @@ -import { getGrantDetails } from '@/grants/data/get-grant-details'; import { getGranteeDetails } from '@/grants/data/get-grantee-details'; import { GranteePageLayout } from '@/grants/pages/grantee-page-layout'; @@ -8,14 +7,10 @@ interface Props { params: { grantId: string; granteeId: string }; } -const GranteePage = async ({ - children, - params: { grantId, granteeId }, -}: Props) => { +const Layout = async ({ children, params: { grantId, granteeId } }: Props) => { const baseHref = `/grants/${grantId}/grantees/${granteeId}/projects`; - const grant = await getGrantDetails(grantId); - const grantee = await getGranteeDetails(grant.data.id); + const grantee = await getGranteeDetails(granteeId); return ( @@ -24,4 +19,4 @@ const GranteePage = async ({ ); }; -export default GranteePage; +export default Layout; diff --git a/src/app/grants/[grantId]/grantees/[granteeId]/page.tsx b/src/app/grants/[grantId]/grantees/[granteeId]/page.tsx index fdaf811..029a575 100644 --- a/src/app/grants/[grantId]/grantees/[granteeId]/page.tsx +++ b/src/app/grants/[grantId]/grantees/[granteeId]/page.tsx @@ -1,10 +1,22 @@ -import { GranteeProjectDefaultPage } from '@/grants/pages/grantee-project-default-page'; +import { getGranteeDetails } from '@/grants/data/get-grantee-details'; +import { getGranteeProject } from '@/grants/data/get-grantee-project'; + +import { GranteeDefaultSection } from '@/grants/pages/grantee-default-section'; interface Props { - params: { granteeId: string }; + params: { grantId: string; granteeId: string }; } -const Page = ({ params: { granteeId } }: Props) => ( - -); +const Page = async ({ params: { grantId, granteeId } }: Props) => { + const { data: grantee } = await getGranteeDetails(granteeId); + + // Default to the first project + const project = await getGranteeProject(grantee.id); + const stats = project.data.tabs[0].stats; + + return ( + + ); +}; + export default Page; diff --git a/src/app/grants/[grantId]/grantees/[granteeId]/projects/page.tsx b/src/app/grants/[grantId]/grantees/[granteeId]/projects/page.tsx index fdaf811..029a575 100644 --- a/src/app/grants/[grantId]/grantees/[granteeId]/projects/page.tsx +++ b/src/app/grants/[grantId]/grantees/[granteeId]/projects/page.tsx @@ -1,10 +1,22 @@ -import { GranteeProjectDefaultPage } from '@/grants/pages/grantee-project-default-page'; +import { getGranteeDetails } from '@/grants/data/get-grantee-details'; +import { getGranteeProject } from '@/grants/data/get-grantee-project'; + +import { GranteeDefaultSection } from '@/grants/pages/grantee-default-section'; interface Props { - params: { granteeId: string }; + params: { grantId: string; granteeId: string }; } -const Page = ({ params: { granteeId } }: Props) => ( - -); +const Page = async ({ params: { grantId, granteeId } }: Props) => { + const { data: grantee } = await getGranteeDetails(granteeId); + + // Default to the first project + const project = await getGranteeProject(grantee.id); + const stats = project.data.tabs[0].stats; + + return ( + + ); +}; + export default Page; diff --git a/src/app/grants/[grantId]/grantees/page.tsx b/src/app/grants/[grantId]/grantees/page.tsx index e602041..fc866b8 100644 --- a/src/app/grants/[grantId]/grantees/page.tsx +++ b/src/app/grants/[grantId]/grantees/page.tsx @@ -1,10 +1,23 @@ -import { GranteeDefaultPage } from '@/grants/pages/grantee-default-page'; +import { getDefaultGrantee } from '@/grants/data/get-default-grantee'; +import { getGranteeProject } from '@/grants/data/get-grantee-project'; + +import { GranteeDefaultSection } from '@/grants/pages/grantee-default-section'; interface Props { params: { grantId: string }; } -const Page = ({ params: { grantId } }: Props) => ( - -); +const Page = async ({ params: { grantId } }: Props) => { + const grantee = await getDefaultGrantee(grantId); + + if (!grantee) return

TODO: Empty Default Grantee UI

; + + // Default to the first project + const project = await getGranteeProject(grantee.id); + const stats = project.data.tabs[0].stats; + + return ( + + ); +}; export default Page; diff --git a/src/app/grants/[grantId]/page.tsx b/src/app/grants/[grantId]/page.tsx index e602041..fc866b8 100644 --- a/src/app/grants/[grantId]/page.tsx +++ b/src/app/grants/[grantId]/page.tsx @@ -1,10 +1,23 @@ -import { GranteeDefaultPage } from '@/grants/pages/grantee-default-page'; +import { getDefaultGrantee } from '@/grants/data/get-default-grantee'; +import { getGranteeProject } from '@/grants/data/get-grantee-project'; + +import { GranteeDefaultSection } from '@/grants/pages/grantee-default-section'; interface Props { params: { grantId: string }; } -const Page = ({ params: { grantId } }: Props) => ( - -); +const Page = async ({ params: { grantId } }: Props) => { + const grantee = await getDefaultGrantee(grantId); + + if (!grantee) return

TODO: Empty Default Grantee UI

; + + // Default to the first project + const project = await getGranteeProject(grantee.id); + const stats = project.data.tabs[0].stats; + + return ( + + ); +}; export default Page; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5445f39..22badd5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,7 @@ import 'swiper/css/pagination'; import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; -import { lato, roboto } from '@/shared/core/fonts'; +import { grotesk, interTight } from '@/shared/core/fonts'; import { InitPathSyncer } from '@/shared/components/init-path-syncer'; import { NavLayout } from '@/shared/components/nav-space-layout'; import { PageScrollDisabler } from '@/shared/components/page-scroll-disabler'; @@ -29,7 +29,7 @@ interface RootLayoutProps { const RootLayout: React.FC = ({ children }) => ( diff --git a/src/grants/components/grantee-project/project-tab-selection.tsx b/src/grants/components/grantee-project/project-tab-selection.tsx index adfecdf..d862831 100644 --- a/src/grants/components/grantee-project/project-tab-selection.tsx +++ b/src/grants/components/grantee-project/project-tab-selection.tsx @@ -21,7 +21,7 @@ export const ProjectTabSelection = ({ defaultId, baseHref }: Props) => { const { data } = useGranteeProject(projectId); if (!data) return ; - const activeTab = params.tab; + const activeTab = params.tab || data.data.tabs[0].tab; if (!activeTab) return null; const tabs = data.data.tabs.map((t) => ({ diff --git a/src/grants/data/get-default-grantee.ts b/src/grants/data/get-default-grantee.ts new file mode 100644 index 0000000..e10fa4b --- /dev/null +++ b/src/grants/data/get-default-grantee.ts @@ -0,0 +1,16 @@ +import { getGrantDetails } from '@/grants/data/get-grant-details'; +import { getGranteeDetails } from '@/grants/data/get-grantee-details'; +import { getGranteesList } from '@/grants/data/get-grantees-list'; + +export const getDefaultGrantee = async (grantId: string) => { + const { data: grant } = await getGrantDetails(grantId); + const { data: grantees } = await getGranteesList({ + page: 1, + grantId: grant.id, + }); + + if (grantees.length === 0) return null; + const { data: firstGrantee } = await getGranteeDetails(grantees[0].id); + + return firstGrantee; +}; diff --git a/src/grants/pages/grantee-default-page.tsx b/src/grants/pages/grantee-default-page.tsx deleted file mode 100644 index 7f8ed47..0000000 --- a/src/grants/pages/grantee-default-page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface Props { - grantId: string; -} - -/** - * Display first grantee by default for the following routes: - * - /grants/:grantId - * - /grants/:grantId/grantees - * */ -export const GranteeDefaultPage = ({ grantId }: Props) => { - // TODO: JOB-683 - return ( -
- {`Default Grantee Page for grant#${grantId}`} -
- ); -}; diff --git a/src/grants/pages/grantee-default-section.tsx b/src/grants/pages/grantee-default-section.tsx new file mode 100644 index 0000000..62be0e3 --- /dev/null +++ b/src/grants/pages/grantee-default-section.tsx @@ -0,0 +1,30 @@ +import { Grantee, GranteeProjectStat } from '@/grants/core/schemas'; +import { GranteeProjectStats } from '@/grants/components/grantee-project/project-stats'; + +import { GranteePageLayout } from '@/grants/pages/grantee-page-layout'; + +interface Props { + grantId: string; + grantee: Grantee; + stats: GranteeProjectStat[]; +} + +/** + * Display first grantee details by default for the following routes: + * - /grants/:grantId + * - /grants/:grantId/grantees + * - /grants/:grantId/grantees/:granteeId + * - /grants/:grantId/grantees/:granteeId/projects + * */ +export const GranteeDefaultSection = ({ grantId, grantee, stats }: Props) => { + // TODO: JOB-683 + // TODO: JOB-684 + + const baseHref = `/grants/${grantId}/grantees/${grantee.id}/projects`; + + return ( + + + + ); +}; diff --git a/src/grants/pages/grantee-page-layout.tsx b/src/grants/pages/grantee-page-layout.tsx index eec896b..9893180 100644 --- a/src/grants/pages/grantee-page-layout.tsx +++ b/src/grants/pages/grantee-page-layout.tsx @@ -6,12 +6,12 @@ import { ProjectTabSelection } from '@/grants/components/grantee-project/project interface Props { baseHref: string; grantee: Grantee; + // TODO: Project children: React.ReactNode; } export const GranteePageLayout = ({ baseHref, grantee, children }: Props) => { const projects = grantee.projects; - const hasProject = projects.length > 0; return (
@@ -19,9 +19,7 @@ export const GranteePageLayout = ({ baseHref, grantee, children }: Props) => { - {hasProject && ( - - )} + {children}
diff --git a/src/grants/pages/grantee-project-default-page.tsx b/src/grants/pages/grantee-project-default-page.tsx deleted file mode 100644 index c119043..0000000 --- a/src/grants/pages/grantee-project-default-page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface Props { - granteeId: string; -} - -/** - * Display first grantee-project by default for the following routes: - * - /grants/:grantId/grantees/:granteeId - * - /grants/:grantId/grantees/:granteeId/projects - * */ -export const GranteeProjectDefaultPage = ({ granteeId }: Props) => { - // TODO: JOB-684 - return ( -
- {`TODO: Fetch Summary Page for project 1 of grantee#${granteeId}`} -
- ); -}; diff --git a/src/grants/pages/route-stories/[grantId].stories.tsx b/src/grants/pages/route-stories/[grantId].stories.tsx index 0bc1aa0..36f8286 100644 --- a/src/grants/pages/route-stories/[grantId].stories.tsx +++ b/src/grants/pages/route-stories/[grantId].stories.tsx @@ -1,35 +1,50 @@ import { Meta, StoryObj } from '@storybook/react'; +import { faker } from '@faker-js/faker'; + import { NavLayout } from '@/shared/components/nav-space-layout'; -import { MockInfiniteQueryResult } from '@/shared/testutils/misc'; +import { + MockInfiniteQueryResult, + MockQueryResult, +} from '@/shared/testutils/misc'; import { GranteeList } from '@/grants/components/grantee-list'; import { fakeGrant } from '@/grants/testutils/fake-grant'; import { fakeGrantee, fakeGrantees } from '@/grants/testutils/fake-grantee'; +import { fakeGranteeProject } from '@/grants/testutils/fake-grantee-project'; import { mockGranteeListQuery } from '@/grants/testutils/mock-grantee-list-query'; +import { mockGranteeProjectQuery } from '@/grants/testutils/mock-grantee-project-query'; import { GrantPageLayout } from '@/grants/pages/grant-page-layout'; -import { GranteeDefaultPage } from '@/grants/pages/grantee-default-page'; +import { GranteeDefaultSection } from '@/grants/pages/grantee-default-section'; + +faker.seed(69); const grantProgram = fakeGrant(); const grantee = fakeGrantee(); const grantees = [grantee, ...fakeGrantees().slice(1)]; +const granteeProject = fakeGranteeProject({ id: grantee.projects[0] }); -const Component = () => { - return ( - - }> - - - - ); +const Component = ({ content }: { content: React.ReactNode }) => { + return {content}; }; const meta: Meta = { title: 'grants/routes/[grantId]', component: Component, + args: { + content: ( + }> + + + ), + }, parameters: { nextjs: { navigation: { @@ -42,6 +57,9 @@ const meta: Meta = { mockGranteeListQuery(MockInfiniteQueryResult.SUCCESS, { data: grantees, }), + mockGranteeProjectQuery(MockQueryResult.SUCCESS, { + data: granteeProject, + }), ], }, }, @@ -50,4 +68,4 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Default: Story = {}; +export const Success: Story = {}; diff --git a/src/grants/testutils/fake-grant.ts b/src/grants/testutils/fake-grant.ts index 9189740..a72ba99 100644 --- a/src/grants/testutils/fake-grant.ts +++ b/src/grants/testutils/fake-grant.ts @@ -6,7 +6,7 @@ import { fakeNullable } from '@/shared/testutils/fake-nullable'; import { Grant } from '@/grants/core/schemas'; -export const fakeGrant = (): Grant => ({ +export const fakeGrant = (partial: Partial = {}): Grant => ({ id: faker.string.uuid(), name: faker.company.name(), networks: Array.from({ length: faker.number.int({ min: 0, max: 4 }) }).map( @@ -38,4 +38,5 @@ export const fakeGrant = (): Grant => ({ `https://discord.com/${faker.internet.userName().toLocaleLowerCase()}`, ), granteesCount: faker.number.int({ min: 1, max: 100 }), + ...partial, }); diff --git a/src/grants/testutils/fake-grantee.ts b/src/grants/testutils/fake-grantee.ts index d5caf61..bd81ca1 100644 --- a/src/grants/testutils/fake-grantee.ts +++ b/src/grants/testutils/fake-grantee.ts @@ -4,7 +4,7 @@ import { fakeNullable } from '@/shared/testutils/fake-nullable'; import { Grantee } from '@/grants/core/schemas'; -export const fakeGrantee = (): Grantee => ({ +export const fakeGrantee = (partial: Partial = {}): Grantee => ({ id: faker.string.uuid(), name: faker.company.name(), logo: fakeNullable(faker.image.url()), @@ -18,6 +18,7 @@ export const fakeGrantee = (): Grantee => ({ projects: Array.from({ length: faker.number.int({ min: 1, max: 3 }) }).map( () => faker.internet.domainWord(), ), + ...partial, }); export const fakeGrantees = ({ diff --git a/src/grants/testutils/mock-grant-list-query.ts b/src/grants/testutils/mock-grant-list-query.ts index 3692a87..3fe7ed4 100644 --- a/src/grants/testutils/mock-grant-list-query.ts +++ b/src/grants/testutils/mock-grant-list-query.ts @@ -1,5 +1,5 @@ import { MockInfiniteQueryResult, MswOptions } from '@/shared/testutils/misc'; -import { mockInfiniteListQuery } from '@/shared/testutils/mockIfniniteListQuery'; +import { mockInfiniteListQuery } from '@/shared/testutils/mock-infinite-list-query'; import { GRANT_QUERY_URLS } from '@/grants/core/constants'; diff --git a/src/grants/testutils/mock-grant-query.ts b/src/grants/testutils/mock-grant-query.ts new file mode 100644 index 0000000..658b615 --- /dev/null +++ b/src/grants/testutils/mock-grant-query.ts @@ -0,0 +1,57 @@ +import { delay, http, HttpResponse } from 'msw'; + +import { errMsg } from '@/shared/core/errors'; + +import { + DEFAULT_MSW_OPTIONS, + MockQueryResult, + MswOptions, +} from '@/shared/testutils/misc'; + +import { GRANT_QUERY_URLS } from '@/grants/core/constants'; + +import { fakeGrant } from '@/grants/testutils/fake-grant'; + +export const mockGrantQuery = (result: MockQueryResult, options?: MswOptions) => + http.get(`${GRANT_QUERY_URLS.GRANT_DETAILS}/:grantId`, async ({ params }) => { + const { networkDelay } = options || DEFAULT_MSW_OPTIONS; + await delay(networkDelay); + + const grantId = params.grantId as string; + + const successResponse = HttpResponse.json({ + success: true, + message: 'Grant retrieved successfully', + data: fakeGrant({ id: grantId }), + }); + + const internalErrorResponse = HttpResponse.json( + { message: errMsg.INTERNAL }, + { status: 500 }, + ); + + switch (result) { + case MockQueryResult.SUCCESS: { + return successResponse; + } + + case MockQueryResult.NOT_FOUND: { + return HttpResponse.json( + { message: errMsg.NOT_FOUND }, + { status: 404 }, + ); + } + + case MockQueryResult.FETCH_ERROR: { + return internalErrorResponse; + } + + case MockQueryResult.NETWORK_ERROR: { + return HttpResponse.error(); + } + + default: { + throw new Error(`Unhandled mock query result: ${result}`); + } + } + }); diff --git a/src/grants/testutils/mock-grantee-list-query.ts b/src/grants/testutils/mock-grantee-list-query.ts index f6bbff1..0be8044 100644 --- a/src/grants/testutils/mock-grantee-list-query.ts +++ b/src/grants/testutils/mock-grantee-list-query.ts @@ -1,5 +1,5 @@ import { MockInfiniteQueryResult, MswOptions } from '@/shared/testutils/misc'; -import { mockInfiniteListQuery } from '@/shared/testutils/mockIfniniteListQuery'; +import { mockInfiniteListQuery } from '@/shared/testutils/mock-infinite-list-query'; import { GRANT_QUERY_URLS } from '@/grants/core/constants'; import { Grantee } from '@/grants/core/schemas'; diff --git a/src/grants/testutils/mock-grantee-project-query.ts b/src/grants/testutils/mock-grantee-project-query.ts index 2dcc2bc..445fcaa 100644 --- a/src/grants/testutils/mock-grantee-project-query.ts +++ b/src/grants/testutils/mock-grantee-project-query.ts @@ -9,12 +9,15 @@ import { } from '@/shared/testutils/misc'; import { GRANT_QUERY_URLS } from '@/grants/core/constants'; +import { GranteeProject } from '@/grants/core/schemas'; import { fakeGranteeProject } from '@/grants/testutils/fake-grantee-project'; export const mockGranteeProjectQuery = ( result: MockQueryResult, - options?: MswOptions, + options?: MswOptions & { + data?: GranteeProject; + }, ) => http.get( `${GRANT_QUERY_URLS.GRANTEE_PROJECT}/:projectId`, @@ -28,8 +31,9 @@ export const mockGranteeProjectQuery = ( const successResponse = HttpResponse.json({ success: true, message: 'Grantee project retrieved successfully', - data: fakeGranteeProject({ id: projectId }), + data: options?.data || fakeGranteeProject({ id: projectId }), }); + const internalErrorResponse = HttpResponse.json( { message: errMsg.INTERNAL }, { status: 500 }, diff --git a/src/grants/testutils/mock-grantee-query.ts b/src/grants/testutils/mock-grantee-query.ts new file mode 100644 index 0000000..e19077c --- /dev/null +++ b/src/grants/testutils/mock-grantee-query.ts @@ -0,0 +1,63 @@ +import { delay, http, HttpResponse } from 'msw'; + +import { errMsg } from '@/shared/core/errors'; + +import { + DEFAULT_MSW_OPTIONS, + MockQueryResult, + MswOptions, +} from '@/shared/testutils/misc'; + +import { GRANT_QUERY_URLS } from '@/grants/core/constants'; + +import { fakeGrantee } from '@/grants/testutils/fake-grantee'; + +export const mockGranteeQuery = ( + result: MockQueryResult, + options?: MswOptions, +) => + http.get( + `${GRANT_QUERY_URLS.GRANTEE_DETAILS}/:granteeId`, + async ({ params }) => { + const { networkDelay } = options || DEFAULT_MSW_OPTIONS; + await delay(networkDelay); + + const granteeId = params.granteeId as string; + + const successResponse = HttpResponse.json({ + success: true, + message: 'Grantee retrieved successfully', + data: fakeGrantee({ id: granteeId }), + }); + + const internalErrorResponse = HttpResponse.json( + { message: errMsg.INTERNAL }, + { status: 500 }, + ); + + switch (result) { + case MockQueryResult.SUCCESS: { + return successResponse; + } + + case MockQueryResult.NOT_FOUND: { + return HttpResponse.json( + { message: errMsg.NOT_FOUND }, + { status: 404 }, + ); + } + + case MockQueryResult.FETCH_ERROR: { + return internalErrorResponse; + } + + case MockQueryResult.NETWORK_ERROR: { + return HttpResponse.error(); + } + + default: { + throw new Error(`Unhandled mock query result: ${result}`); + } + } + }, + ); diff --git a/src/orgs/components/org-card/index.tsx b/src/orgs/components/org-card/index.tsx index d96b884..a77a4b4 100644 --- a/src/orgs/components/org-card/index.tsx +++ b/src/orgs/components/org-card/index.tsx @@ -41,8 +41,8 @@ export const OrgCard = (props: Props) => { >
-

{name}

-

{location}

+

{name}

+

{location}

diff --git a/src/shared/components/chains-info-tag.tsx b/src/shared/components/chains-info-tag.tsx index 07cddff..b45455a 100644 --- a/src/shared/components/chains-info-tag.tsx +++ b/src/shared/components/chains-info-tag.tsx @@ -22,7 +22,7 @@ const CHAIN_COUNTS = { const AvatarGroupCount = ({ count }: { count: number }) => (
- +
); diff --git a/src/shared/components/details-panel/tab.tsx b/src/shared/components/details-panel/tab.tsx index cca8800..a46bfe7 100644 --- a/src/shared/components/details-panel/tab.tsx +++ b/src/shared/components/details-panel/tab.tsx @@ -17,12 +17,9 @@ export const DetailsPanelTab = ({ href, isActive, text }: TabProps) => { const wrapperClassName = 'flex h-10 shrink-0 items-center justify-center rounded-lg border border-white/10 px-4 py-2 sm:h-12 md:h-8'; - const contentClassName = cn( - `rounded-lg border border-transparent font-lato text-sm`, - { - 'border-0': isActive, // Prevent active border layout shift - }, - ); + const contentClassName = cn(`rounded-lg border border-transparent text-sm`, { + 'border-0': isActive, // Prevent active border layout shift + }); return (