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 === '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 }) => }
+
+
+
+
+
+ );
+}
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/contact-details/primary-office-state.tsx b/client/src/containers/my-projects/form/steps/contact-details/primary-office-state.tsx
new file mode 100644
index 00000000..10d68b1d
--- /dev/null
+++ b/client/src/containers/my-projects/form/steps/contact-details/primary-office-state.tsx
@@ -0,0 +1,74 @@
+'use client';
+
+import { ComponentProps, useEffect } from 'react';
+
+import { Field as FieldRFF, useForm, useFormState } 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 PrimaryOfficeStateSelector() {
+ const {
+ values: { country_id: countryId },
+ } = useFormState();
+
+ const { change, getFieldState } = useForm();
+
+ const stateFieldState = getFieldState('state_id');
+ const dirty = stateFieldState?.dirty;
+
+ const { data: countries } = useSubGeographics(
+ {
+ filters: { geographic: 'countries' },
+ },
+ {
+ select: ({ data }) => data,
+ }
+ );
+
+ const isUSA = countries.find(({ id }) => id === countryId)?.code === 'USA';
+
+ const {
+ data: states,
+ isFetching: statesFetching,
+ isFetched: statesFetched,
+ } = useSubGeographics(
+ {
+ filters: { geographic: 'states' },
+ },
+ {
+ select: ({ data }) => data,
+ enabled: isUSA,
+ }
+ );
+
+ const statesOptions: ComponentProps['options'] =
+ states?.map(({ id, name }) => ({ value: id, label: name })) || [];
+
+ useEffect(() => {
+ if (dirty && !isUSA) {
+ change('state_id', undefined);
+ }
+ }, [dirty, isUSA, change]);
+
+ return (
+ name="state_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 (
);
}
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