diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 4dba5faa..be78ff87 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -5086,10 +5086,10 @@ packages: peerDependencies: react: '>=16.8.0' - use-sync-external-store@1.2.2: - resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 use@3.1.1: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} @@ -6000,7 +6000,7 @@ snapshots: '@tanstack/query-core': 4.36.1 '@types/use-sync-external-store': 0.0.3 react: 18.3.1 - use-sync-external-store: 1.2.2(react@18.3.1) + use-sync-external-store: 1.4.0(react@18.3.1) optionalDependencies: react-dom: 18.3.1(react@18.3.1) @@ -10896,7 +10896,7 @@ snapshots: dependencies: react: 18.3.1 - use-sync-external-store@1.2.2(react@18.3.1): + use-sync-external-store@1.4.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/client/src/containers/my-projects/constants.ts b/client/src/containers/my-projects/constants.ts index cc054f7b..c3b2d2b6 100644 --- a/client/src/containers/my-projects/constants.ts +++ b/client/src/containers/my-projects/constants.ts @@ -2,13 +2,16 @@ export const FORM_STEPS = [ { label: 'Project Details', value: 'project-details', + disabled: false, }, { label: 'Contact Details', value: 'contact-details', + disabled: false, }, { label: 'Funding', value: 'funding', + disabled: false, }, -]; +] as const; diff --git a/client/src/containers/my-projects/form/index.tsx b/client/src/containers/my-projects/form/index.tsx index 04926483..f13842e8 100644 --- a/client/src/containers/my-projects/form/index.tsx +++ b/client/src/containers/my-projects/form/index.tsx @@ -2,8 +2,9 @@ import { FormRenderProps } from 'react-final-form'; import { useSearchParams } from 'next/navigation'; +import FundingStep from '../new/funding'; + import ContactDetailsStep from './steps/contact-details'; -import FundingStep from './steps/funding'; import ProjectDetailsStep from './steps/project-details'; type STEP = 'project-details' | 'contact-details' | 'funding'; @@ -13,10 +14,14 @@ export default function Form({ handleSubmit }: { handleSubmit: FormRenderProps[' const currentStep = (searchParams.get('step') as STEP) || 'project-details'; return ( -
- {currentStep === 'project-details' && } - {currentStep === 'contact-details' && } + <> + {['project-details', 'contact-details'].includes(currentStep) && ( + + {currentStep === 'project-details' && } + {currentStep === 'contact-details' && } + + )} {currentStep === 'funding' && } - + ); } diff --git a/client/src/containers/my-projects/form/steps/contact-details.tsx b/client/src/containers/my-projects/form/steps/contact-details.tsx deleted file mode 100644 index 8027557e..00000000 --- a/client/src/containers/my-projects/form/steps/contact-details.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ContactDetailsStep() { - return
ContactDetailsStep
; -} diff --git a/client/src/containers/my-projects/form/steps/contact-details/index.tsx b/client/src/containers/my-projects/form/steps/contact-details/index.tsx new file mode 100644 index 00000000..69ea945f --- /dev/null +++ b/client/src/containers/my-projects/form/steps/contact-details/index.tsx @@ -0,0 +1,93 @@ +import { Field as FieldRFF } from 'react-final-form'; + +import { usePathname } from 'next/navigation'; + +import { HiOutlineArrowLeft } from 'react-icons/hi'; + +import LinkButton from 'components/button'; +import { Input } from 'components/forms'; + +import VisibilityLabel from '../../label'; +import FormLegend from '../../legend'; +import { ProjectSchema } from '../../validations'; + +import PrimaryOfficeCountrySelector from './primary-office-country'; +import PrimaryOfficeStateSelector from './primary-office-state'; + +export default function ContactDetailsStep() { + const pathname = usePathname(); + + return ( +
+
+

Contact Details

+ {/*@todo: update text*/} +

+ Lorem ipsum dolor sit amet consectetur et fringilla pellentesque in ut congue at ultrices + nulla nibh dolor sit amet pellentesque consectetur. +

+
+ +
+ +

+ * + All fields marked with a red asterisk are mandatory to fill +

+
+ +
+
+
+ + Primary Office Country + + +
+
+
+
+ + Primary Office State + + +
+
+
+ +
+ + Primary Office City + + name="city" type="text"> + {({ input }) => } + +
+ +
+ + + Project Details + +
+
+ ); +} diff --git a/client/src/containers/my-projects/form/steps/contact-details/primary-office-country.tsx b/client/src/containers/my-projects/form/steps/contact-details/primary-office-country.tsx new file mode 100644 index 00000000..a62e65a0 --- /dev/null +++ b/client/src/containers/my-projects/form/steps/contact-details/primary-office-country.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { ComponentProps } from 'react'; + +import { Field as FieldRFF } from 'react-final-form'; + +import { useSubGeographics } from 'hooks/geographics'; + +import { ProjectSchema } from 'containers/my-projects/form/validations'; + +import { Select } from 'components/forms'; + +export default function PrimaryOfficeCountrySelector() { + const { + data: countries, + isFetching: countriesFetching, + isFetched: countriesFetched, + } = useSubGeographics( + { + filters: { geographic: 'countries' }, + }, + { + select: ({ data }) => data, + } + ); + + const countriesOptions: ComponentProps['options'] = + countries?.map(({ id, name }) => ({ value: id, label: name })) || []; + + return ( + name="country_id"> + {({ input }) => ( + + )} + + ); +} diff --git a/client/src/containers/my-projects/form/steps/funding.tsx b/client/src/containers/my-projects/form/steps/funding.tsx deleted file mode 100644 index fbc37bbb..00000000 --- a/client/src/containers/my-projects/form/steps/funding.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function FundingStep() { - return
FundingStep
; -} diff --git a/client/src/containers/my-projects/form/steps/project-details/demographics.tsx b/client/src/containers/my-projects/form/steps/project-details/demographics.tsx index a3c0f37f..02ce8ad7 100644 --- a/client/src/containers/my-projects/form/steps/project-details/demographics.tsx +++ b/client/src/containers/my-projects/form/steps/project-details/demographics.tsx @@ -21,7 +21,7 @@ export default function DemographicsSelector() { demographics?.map(({ id, name }) => ({ value: id, label: name })) || []; return ( - name="demographics"> + name="leadership_demographics"> {({ input }) => ( (); const logoField = useField('logo'); const [imageSrc, setImageSrc] = useState(null); @@ -197,7 +196,7 @@ export default function ProjectDetailsStep() {
Other demographics - name="demographics_other" type="text"> + + name="leadership_demographics_other" + type="text" + > {({ input }) => }
@@ -219,7 +221,7 @@ export default function ProjectDetailsStep() { className="flex items-center gap-1" > Contact Details - + diff --git a/client/src/containers/my-projects/form/validations.ts b/client/src/containers/my-projects/form/validations.ts index 8aaf038e..4bf0bf88 100644 --- a/client/src/containers/my-projects/form/validations.ts +++ b/client/src/containers/my-projects/form/validations.ts @@ -4,9 +4,13 @@ const ZodProjectSchema = z.object({ name: z.string().nonempty().min(3), website: z.string().url().optional(), description: z.string().nonempty(), + recipient_legal_status: z.string(), logo: z.instanceof(File).optional(), - demographics: z.array(z.string()).nonempty(), - demographics_other: z.string().optional(), + leadership_demographics: z.array(z.string()).nonempty(), + leadership_demographics_other: z.string().optional(), + country_id: z.string(), + state_id: z.string().optional(), + city: z.string(), }); export type ProjectSchema = z.infer; diff --git a/client/src/containers/my-projects/new/funding.tsx b/client/src/containers/my-projects/new/funding.tsx new file mode 100644 index 00000000..8315e412 --- /dev/null +++ b/client/src/containers/my-projects/new/funding.tsx @@ -0,0 +1,18 @@ +import LinkButton from 'components/button'; + +export default function FundingStep() { + return ( +
+
+

You have no funding reported for this project.

+

+ Lorem ipsum dolor sit amet consectetur. Convallis fusce neque odio nunc elementum habitant + sit sagittis. +

+ + Report Funding + +
+
+ ); +} diff --git a/client/src/containers/my-projects/new/header.tsx b/client/src/containers/my-projects/new/header.tsx index 0dafad08..7d881576 100644 --- a/client/src/containers/my-projects/new/header.tsx +++ b/client/src/containers/my-projects/new/header.tsx @@ -1,17 +1,43 @@ import { useForm, useFormState } from 'react-final-form'; -import { Button } from 'components/button/component'; +import { useSearchParams } from 'next/navigation'; + +import { useIsMutating } from '@tanstack/react-query'; + +import { Button, LinkButton } from 'components/button/component'; + +import { FORM_STEPS } from '../constants'; + +import { NEW_PROJECT_QUERY_KEY } from './index'; export default function NewProjectHeader() { const { invalid } = useFormState(); const { submit } = useForm(); + const queryParams = useSearchParams(); + const currentStep = queryParams.get('step') as typeof FORM_STEPS[number]['value']; + + const isMutatingNewProject = useIsMutating({ mutationKey: NEW_PROJECT_QUERY_KEY }); return (

New project

- +
+ {currentStep === 'funding' && !isMutatingNewProject && ( + + Project Page + + )} + {currentStep !== 'funding' && ( + + )} +
); } diff --git a/client/src/containers/my-projects/new/index.tsx b/client/src/containers/my-projects/new/index.tsx index ec879165..2c482a51 100644 --- a/client/src/containers/my-projects/new/index.tsx +++ b/client/src/containers/my-projects/new/index.tsx @@ -1,26 +1,69 @@ +import { useMemo } from 'react'; + +import { useRouter } from 'next/navigation'; + +import { useMutation } from '@tanstack/react-query'; +import { useSession } from 'next-auth/react'; + import Wrapper from 'containers/wrapper'; +import API from 'services/api'; + import { FORM_STEPS } from '../constants'; import Form from '../form'; +import { ProjectSchema } from '../form/validations'; import FormWrapper from '../form/wrapper'; import MyProjectsSidebar from '../sidebar'; import NewProjectHeader from './header'; +export const NEW_PROJECT_QUERY_KEY = ['newProject']; + export default function NewProject() { + const { data: session } = useSession(); + const { push } = useRouter(); + const mutation = useMutation({ + mutationKey: NEW_PROJECT_QUERY_KEY, + mutationFn: (data: ProjectSchema) => { + return API.request({ + method: 'POST', + url: '/members/projects', + data: data, + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${session.accessToken}`, + }, + }); + }, + onSuccess: () => { + push('/my-projects/new?step=funding'); + }, + }); + + const formSteps = useMemo(() => { + return FORM_STEPS.map((step, index) => { + if (index === 2) { + return { ...step, disabled: mutation.isLoading || mutation.isIdle }; + } + return step; + }); + }, [mutation]); + return ( - + console.log('form wrapper', data)} + onSubmit={(data) => { + mutation.mutate(data); + }} render={({ handleSubmit }) => { return ( -
+
-
+
- +
-
+
diff --git a/client/src/containers/my-projects/sidebar.test.tsx b/client/src/containers/my-projects/sidebar.test.tsx index cf328ac3..f5a7ee59 100644 --- a/client/src/containers/my-projects/sidebar.test.tsx +++ b/client/src/containers/my-projects/sidebar.test.tsx @@ -17,6 +17,7 @@ const TEST_SECTIONS: ComponentProps['sections'] = [ { label: 'Test label 3', value: 'test-3', + disabled: true, }, ]; @@ -64,7 +65,7 @@ describe('sidebar', () => { expect(sections[0].querySelector('a')).toHaveClass(ACTIVE_CLASS); }); - it('expects other sections (not the selected one) to not have the active class applied ', () => { + it('expects other sections (not the selected one) to not have the active class applied', () => { const { container } = render(); const sections = container.querySelectorAll('li'); @@ -80,4 +81,11 @@ describe('sidebar', () => { expect(sections[0].querySelector('a')).toHaveClass(ACTIVE_CLASS); }); + + it('if a section is disabled, the disabled styles should be applied', () => { + const { container } = render(); + const sections = container.querySelectorAll('li'); + + expect(sections[2].querySelector('a')).toHaveClass('opacity-50 pointer-events-none'); + }); }); diff --git a/client/src/containers/my-projects/sidebar.tsx b/client/src/containers/my-projects/sidebar.tsx index 17f61e03..4f4e9315 100644 --- a/client/src/containers/my-projects/sidebar.tsx +++ b/client/src/containers/my-projects/sidebar.tsx @@ -10,6 +10,7 @@ export default function MyProjectsSidebar({ sections: { label: string; value: string; + disabled?: boolean; }[]; }) { const pathname = usePathname(); @@ -28,6 +29,7 @@ export default function MyProjectsSidebar({ queryParams.get('step')?.includes(step.value) ?? sections?.[0].value === step.value, })} + disabled={step.disabled} > {step.label} diff --git a/client/src/hooks/geographics/index.ts b/client/src/hooks/geographics/index.ts index 3aad0dff..051ddc4e 100644 --- a/client/src/hooks/geographics/index.ts +++ b/client/src/hooks/geographics/index.ts @@ -4,7 +4,6 @@ import { jsonAPIAdapter } from 'lib/adapters/json-api-adapter'; import { ParamsProps } from 'lib/adapters/types'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import type { FeatureCollection } from 'geojson'; import { Geographic, SubGeographic } from 'types/geographics'; @@ -47,84 +46,26 @@ export function useGeographics(queryOptions: UseQueryOptions = {} + queryOptions: UseQueryOptions<{ data: SubGeographic[] }, unknown, SubGeographic[]> = {} ) { const fetchSubgeographics = () => - API.request({ + API.request<{ data: SubGeographic[] }>({ method: 'GET', url: '/subgeographics', params: jsonAPIAdapter(params), }).then((response) => response.data); - const query = useQuery(['subgeographics', JSON.stringify(params)], fetchSubgeographics, { + return useQuery(['subgeographics', JSON.stringify(params)], fetchSubgeographics, { placeholderData: { - data: [], + data: [] as SubGeographic[], }, + select: ({ data }) => + data.map((subgeographic) => { + return { + ...subgeographic, + id: subgeographic.abbreviation, + }; + }), ...queryOptions, }); - - const { data } = query; - - const DATA = useMemo(() => { - if (!data) { - return []; - } - - // Work with abbreviations instead of ids - return data.map((subgeographic) => { - return { - ...subgeographic, - id: subgeographic.abbreviation, - }; - }); - }, [data]); - - return useMemo(() => { - return { - ...query, - data: DATA, - }; - }, [query, DATA]); -} - -export function useSubGeographicsGeojson( - params: ParamsProps = {}, - queryOptions: UseQueryOptions = {} -) { - const fetchSubgeographicsGeojson = () => - API.request({ - method: 'GET', - url: '/subgeographics/geojson', - transformResponse: (data) => JSON.parse(data), - params: jsonAPIAdapter(params), - }).then((response) => response.data); - - const query = useQuery( - ['subgeographics-geojson', JSON.stringify(params)], - fetchSubgeographicsGeojson, - { - placeholderData: { - type: 'FeatureCollection', - features: [], - }, - ...queryOptions, - } - ); - - const { data } = query; - - const DATA = useMemo(() => { - if (!data) { - return {}; - } - - return data; - }, [data]); - - return useMemo(() => { - return { - ...query, - data: DATA, - }; - }, [query, DATA]); } diff --git a/client/src/middleware.ts b/client/src/middleware.ts new file mode 100644 index 00000000..c1791220 --- /dev/null +++ b/client/src/middleware.ts @@ -0,0 +1,3 @@ +export { default } from 'next-auth/middleware'; + +export const config = { matcher: ['/my-details/:path*', '/my-projects/:path*', '/my-fundings'] }; diff --git a/client/src/svgs/ui/arrow-right.svg b/client/src/svgs/ui/arrow-right.svg deleted file mode 100644 index 8ab8ad2f..00000000 --- a/client/src/svgs/ui/arrow-right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file