diff --git a/src/pages/Admin/Charity/CreateProgram/useSubmit.ts b/src/pages/Admin/Charity/CreateProgram/useSubmit.ts index b7c775d70d..22b457b10c 100644 --- a/src/pages/Admin/Charity/CreateProgram/useSubmit.ts +++ b/src/pages/Admin/Charity/CreateProgram/useSubmit.ts @@ -1,7 +1,7 @@ import { SubmitHandler, useFormContext } from "react-hook-form"; import { FV } from "./types"; -import { EndowmentProfileUpdate, Program } from "types/aws"; -import { SemiPartial } from "types/utils"; +import { ProfileUpdateMsg } from "services/types"; +import { Program } from "types/aws"; import { useModalContext } from "contexts/ModalContext"; import { ImgLink } from "components/ImgEditor"; import { TxPrompt } from "components/Prompt"; @@ -55,7 +55,7 @@ export default function useSubmit() { } } - const updates: SemiPartial = { + const updates: ProfileUpdateMsg = { id, owner, program: [program], diff --git a/src/pages/Admin/Charity/EditProfile/Form.tsx b/src/pages/Admin/Charity/EditProfile/Form.tsx index a356eef013..8baaaedad1 100644 --- a/src/pages/Admin/Charity/EditProfile/Form.tsx +++ b/src/pages/Admin/Charity/EditProfile/Form.tsx @@ -1,6 +1,6 @@ import { FormHTMLAttributes } from "react"; import { Link } from "react-router-dom"; -import { FormValues as FV } from "./types"; +import { FV } from "./types"; import { UNSDG_NUMS } from "types/lists"; import ActivityCountries from "components/ActivityCountries"; import CountrySelector from "components/CountrySelector"; @@ -114,9 +114,9 @@ export default function Form({ - + multiple - name="categories_sdgs" + name="sdgs" options={sdgOptions} classes={{ button: "field-input-admin" }} /> @@ -161,43 +161,43 @@ export default function Form({ classes="field-admin" - name="social_media_url_facebook" + name="social_media_urls.facebook" label="Facebook" placeholder="https://facebook.com/" /> classes="field-admin" - name="social_media_url_linkedin" + name="social_media_urls.linkedin" label="Linkedin" placeholder="https://linkedin.com/" /> classes="field-admin" - name="social_media_url_twitter" + name="social_media_urls.twitter" label="Twitter" placeholder="https://twitter.com/" /> classes="field-admin" - name="social_media_url_discord" + name="social_media_urls.discord" label="Discord" placeholder="https://discord.com/" /> classes="field-admin" - name="social_media_url_instagram" + name="social_media_urls.instagram" label="Instagram" placeholder="https://instagram.com/" /> classes="field-admin" - name="social_media_url_youtube" + name="social_media_urls.youtube" label="YouTube" placeholder="https://youtube.com/" /> classes="field-admin" - name="social_media_url_tiktok" + name="social_media_urls.tiktok" label="Tiktok" placeholder="https://tiktok.com/" /> diff --git a/src/pages/Admin/Charity/EditProfile/index.tsx b/src/pages/Admin/Charity/EditProfile/index.tsx index 8211e6cc2f..2d1eb22c59 100644 --- a/src/pages/Admin/Charity/EditProfile/index.tsx +++ b/src/pages/Admin/Charity/EditProfile/index.tsx @@ -1,7 +1,8 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { FormProvider, useForm } from "react-hook-form"; -import { FlatFormValues, FormValues } from "./types"; -import { Profile as TProfile, endow } from "services/types"; +import { FV } from "./types"; +import { Profile as TProfile } from "services/types"; +import { EndowDesignation, EndowmentProfileUpdate } from "types/aws"; import { useProfileQuery } from "services/aws/aws"; import { FormError, FormSkeleton } from "components/admin"; import { adminRoutes } from "constants/routes"; @@ -13,6 +14,7 @@ import { getEndowDesignationLabelValuePair } from "./getEndowDesignationLabelVal import { getSDGLabelValuePair } from "./getSDGLabelValuePair"; import { ops } from "./ops"; import { schema } from "./schema"; +import { toProfileUpdate } from "./update"; export default function EditProfile() { const { id } = useAdminContext(ops); @@ -36,60 +38,39 @@ export default function EditProfile() { } function FormWithContext(props: TProfile) { - const { txResource } = useAdminContext(ops); + const { txResource, id, owner } = useAdminContext<"charity">(ops); - const { active_in_countries = [] } = props; - const designation = endow(props) ? props.endow_designation : ""; - const sdgs = endow(props) ? props.sdgs : []; - // could just add to useForm.defaultValue - but not Partial here - const flatInitial: FlatFormValues = { - name: props.name, - categories_sdgs: sdgs, - endow_designation: designation, - hq_country: props.hq_country ?? "", - active_in_countries: active_in_countries, - image: props.image || "", - logo: props.logo || "", - kyc_donors_only: props.kyc_donors_only || false, - overview: props.overview ?? "", - url: props.url || "", - published: props.published || false, - registration_number: props.registration_number || "", - social_media_url_facebook: props.social_media_urls?.facebook || "", - social_media_url_linkedin: props.social_media_urls?.linkedin || "", - social_media_url_twitter: props.social_media_urls?.twitter || "", - social_media_url_discord: props.social_media_urls?.discord || "", - social_media_url_instagram: props.social_media_urls?.instagram || "", - social_media_url_youtube: props.social_media_urls?.youtube || "", - social_media_url_tiktok: props.social_media_urls?.tiktok || "", - street_address: props.street_address || "", - tagline: props.tagline, - }; + const init: EndowmentProfileUpdate = toProfileUpdate({ + type: "initial", + data: { ...props, id, owner }, + }); - const defaults: FormValues = { - ...flatInitial, + const defaults: FV = { + ...init, image: { name: "", publicUrl: props.image ?? "", preview: props.image ?? "", }, logo: { name: "", publicUrl: props.logo ?? "", preview: props.logo ?? "" }, - endow_designation: designation - ? getEndowDesignationLabelValuePair(designation) + endow_designation: init.endow_designation + ? getEndowDesignationLabelValuePair( + init.endow_designation as EndowDesignation + ) : { label: "", value: "" }, hq_country: { flag: "", name: props.hq_country ?? "", code: "" }, - categories_sdgs: sdgs.map((x) => getSDGLabelValuePair(x, unsdgs[x].title)), - active_in_countries: active_in_countries.map((x) => ({ + sdgs: init.sdgs.map((x) => getSDGLabelValuePair(x, unsdgs[x].title)), + active_in_countries: init.active_in_countries.map((x) => ({ label: x, value: x, })), //meta type: props.type, - initial: flatInitial, + initial: init, }; - const methods = useForm({ + const methods = useForm({ defaultValues: defaults, resolver: yupResolver(schema), context: { isEndow: props.type === "endowment" }, diff --git a/src/pages/Admin/Charity/EditProfile/schema.ts b/src/pages/Admin/Charity/EditProfile/schema.ts index f62bc1c8e9..35f1ee20c0 100644 --- a/src/pages/Admin/Charity/EditProfile/schema.ts +++ b/src/pages/Admin/Charity/EditProfile/schema.ts @@ -1,5 +1,5 @@ import { array, object, string } from "yup"; -import { FormValues } from "./types"; +import { FV } from "./types"; import { SchemaShape } from "schemas/types"; import { ImgLink } from "components/ImgEditor"; import { OptionType } from "components/Selector"; @@ -23,9 +23,9 @@ const fileObj = object().shape>({ }); //construct strict shape to avoid hardcoding shape keys -const shape: SchemaShape = { +const shape: SchemaShape = { //not required for ASTs - categories_sdgs: array() + sdgs: array() .max(MAX_SDGS, `maximum ${MAX_SDGS} selections allowed`) .when("$isEndow", { is: true, @@ -48,13 +48,15 @@ const shape: SchemaShape = { }), name: requiredString, active_in_countries: array(), - social_media_url_facebook: url, - social_media_url_twitter: url, - social_media_url_linkedin: url, - social_media_url_discord: url, - social_media_url_instagram: url, - social_media_url_youtube: url, - social_media_url_tiktok: url, + social_media_urls: object().shape>({ + facebook: url, + twitter: url, + linkedin: url, + discord: url, + instagram: url, + youtube: url, + tiktok: url, + }), }; export const schema = object().shape(shape); diff --git a/src/pages/Admin/Charity/EditProfile/types.ts b/src/pages/Admin/Charity/EditProfile/types.ts index 294aa33f29..9009510912 100644 --- a/src/pages/Admin/Charity/EditProfile/types.ts +++ b/src/pages/Admin/Charity/EditProfile/types.ts @@ -1,4 +1,4 @@ -import { Except } from "type-fest"; +import { OverrideProperties } from "type-fest"; import { Profile } from "services/types"; import { EndowmentProfileUpdate } from "types/aws"; import { Country } from "types/countries"; @@ -6,51 +6,14 @@ import { UNSDG_NUMS } from "types/lists"; import { ImgLink } from "components/ImgEditor"; import { OptionType } from "components/Selector"; -type K = keyof EndowmentProfileUpdate; -const _logo: K = "logo"; -const _img: K = "image"; -const _country: K = "hq_country"; -const _activity_countries: K = "active_in_countries"; -const _sdgs: K = "categories_sdgs"; -const _general: K = "categories_general"; -const _id: K = "id"; -const _tier: K = "tier"; -const _owner: K = "owner"; -const _npo_type: K = "endow_designation"; -const _contributor_verification_required: K = - "contributor_verification_required"; - -export type FlatFormValues = Except< +export type FV = OverrideProperties< EndowmentProfileUpdate, - /** to flatten */ - /** don't include for now */ - | typeof _general - /** not editable fields*/ - | typeof _id - | typeof _tier - | typeof _owner - | typeof _contributor_verification_required - | "program" - | "program_id" ->; - -export type FormValues = Omit< - FlatFormValues, - | typeof _logo - | typeof _img - | typeof _country - | typeof _sdgs - | typeof _activity_countries - | typeof _npo_type -> & { - [_npo_type]: OptionType; - [_logo]: ImgLink; - [_img]: ImgLink; - [_country]: Country; - [_sdgs]: OptionType[]; - [_activity_countries]: OptionType[]; - - //meta - type: Profile["type"]; - initial: FlatFormValues; -}; + { + endow_designation: OptionType; + logo: ImgLink; + image: ImgLink; + hq_country: Country; + sdgs: OptionType[]; + active_in_countries: OptionType[]; + } +> & { type: Profile["type"]; initial: EndowmentProfileUpdate }; diff --git a/src/pages/Admin/Charity/EditProfile/update.ts b/src/pages/Admin/Charity/EditProfile/update.ts new file mode 100644 index 0000000000..d00d6b5dcd --- /dev/null +++ b/src/pages/Admin/Charity/EditProfile/update.ts @@ -0,0 +1,69 @@ +import { Except } from "type-fest"; +import { FV } from "./types"; +import { Profile, endow } from "services/types"; +import { EndowmentProfileUpdate } from "types/aws"; + +type RequiredFields = Pick; +type Arg = + | { + type: "initial"; + data: Profile & RequiredFields; + } + | { + type: "final"; + data: Except & RequiredFields; + urls: { image: string; logo: string }; + }; + +export function toProfileUpdate(arg: Arg): EndowmentProfileUpdate { + if (arg.type === "initial") { + const { data: d } = arg; + return { + id: d.id, + owner: d.owner, + active_in_countries: d.active_in_countries ?? [], + categories: { sdgs: d.sdgs ?? [], general: [] }, + charity_navigator_rating: "", + contact_email: d.contact_email ?? "", + contributor_verification_required: + d.contributor_verification_required ?? false, + endow_designation: endow(d) ? d.endow_designation : "", + hq_country: d.hq_country ?? "", + image: d.image ?? "", + kyc_donors_only: d.kyc_donors_only ?? false, + logo: d.logo ?? "", + name: d.name ?? "", + overview: d.overview ?? "", + program: [], //program is updated in /create-program + program_id: "", + published: d.published ?? false, + registration_number: d.registration_number ?? "", + sdgs: d.sdgs ?? [], + social_media_urls: { + facebook: d.social_media_urls?.facebook ?? "", + instagram: d.social_media_urls?.instagram ?? "", + linkedin: d.social_media_urls?.linkedin ?? "", + twitter: d.social_media_urls?.twitter ?? "", + discord: d.social_media_urls?.discord ?? "", + youtube: d.social_media_urls?.youtube ?? "", + tiktok: d.social_media_urls?.tiktok ?? "", + }, + street_address: d.street_address ?? "", + tagline: d.tagline ?? "", + tier: 1, + url: d.url ?? "", + }; + } + + const { data: fv, urls } = arg; + return { + ...fv, + program: [], //program is updated in /create-program + image: urls.image, + logo: urls.logo, + hq_country: fv.hq_country.name, + endow_designation: fv.endow_designation.value, + sdgs: fv.sdgs.map((opt) => opt.value), + active_in_countries: fv.active_in_countries.map((opt) => opt.value), + }; +} diff --git a/src/pages/Admin/Charity/EditProfile/useEditProfile.ts b/src/pages/Admin/Charity/EditProfile/useEditProfile.ts index 3ad72cc69c..7397a14ce4 100644 --- a/src/pages/Admin/Charity/EditProfile/useEditProfile.ts +++ b/src/pages/Admin/Charity/EditProfile/useEditProfile.ts @@ -1,7 +1,6 @@ import { SubmitHandler, useFormContext } from "react-hook-form"; -import { FormValues as FV, FlatFormValues } from "./types"; -import { EndowmentProfileUpdate } from "types/aws"; -import { SemiPartial } from "types/utils"; +import { FV } from "./types"; +import { ProfileUpdateMsg } from "services/types"; import { useModalContext } from "contexts/ModalContext"; import { ImgLink } from "components/ImgEditor"; import { TxPrompt } from "components/Prompt"; @@ -11,6 +10,7 @@ import { getFullURL, uploadFiles } from "helpers/uploadFiles"; import { useAdminContext } from "../../Context"; import useUpdateEndowmentProfile from "../common/useUpdateEndowmentProfile"; import { ops } from "./ops"; +import { toProfileUpdate } from "./update"; export default function useEditProfile() { const { id, owner } = useAdminContext<"charity">(ops); @@ -24,23 +24,13 @@ export default function useEditProfile() { const { showModal } = useModalContext(); const updateProfile = useUpdateEndowmentProfile(); - const editProfile: SubmitHandler = async ({ - initial, - image, - logo, - hq_country, - categories_sdgs, - active_in_countries, - endow_designation, - type, - ...newData - }) => { + const editProfile: SubmitHandler = async ({ initial, type, ...fv }) => { try { /** special case for edit profile: since upload happens prior * to tx submission. Other users of useTxSender */ - const [bannerUrl, logoUrl] = await uploadImgs([image, logo], () => { + const [bannerUrl, logoUrl] = await uploadImgs([fv.image, fv.logo], () => { showModal( TxPrompt, { loading: "Uploading images.." }, @@ -48,32 +38,26 @@ export default function useEditProfile() { ); }); - const changes: FlatFormValues = { - image: bannerUrl, - logo: logoUrl, - hq_country: hq_country.name, - endow_designation: endow_designation.value, - categories_sdgs: categories_sdgs.map((opt) => opt.value), - active_in_countries: active_in_countries.map((opt) => opt.value), - ...newData, - }; + const update = toProfileUpdate({ + type: "final", + data: { ...fv, id, owner }, + urls: { image: bannerUrl, logo: logoUrl }, + }); - const diff = getPayloadDiff(initial, changes); + const diffs = getPayloadDiff(initial, update); - if (Object.entries(diff).length <= 0) { + if (Object.entries(diffs).length <= 0) { return showModal(TxPrompt, { error: "No changes detected" }); } - /** already clean - no need to futher clean "": to unset values { field: val }, field must have a value - like ""; unlike contracts where if fields is not present, val is set to null. - */ - const updates: SemiPartial = { - ...changes, - id, - owner, - }; + //only include top level keys that appeared on diff + const cleanUpdate: ProfileUpdateMsg = { id, owner }; + for (const [path] of diffs) { + const key = path.split(".")[0] as keyof ProfileUpdateMsg; + (cleanUpdate as any)[key] = update[key]; + } - await updateProfile(updates); + await updateProfile(cleanUpdate); } catch (err) { showModal(TxPrompt, { error: err instanceof Error ? err.message : "Unknown error occured", diff --git a/src/pages/Admin/Charity/Programs/List.tsx b/src/pages/Admin/Charity/Programs/List.tsx index 0c29e8a7c4..0c3635fea9 100644 --- a/src/pages/Admin/Charity/Programs/List.tsx +++ b/src/pages/Admin/Charity/Programs/List.tsx @@ -1,12 +1,9 @@ -import { Link } from "react-router-dom"; -import { Program as TProgram } from "types/aws"; import { useAdminContext } from "pages/Admin/Context"; import { useProfileQuery } from "services/aws/aws"; import ContentLoader from "components/ContentLoader"; -import Image from "components/Image"; import QueryLoader from "components/QueryLoader"; import { ErrorStatus, Info } from "components/Status"; -import { adminRoutes } from "constants/routes"; +import { Program } from "./Program"; export default function List() { const { id } = useAdminContext(); @@ -32,38 +29,6 @@ export default function List() { ); } -function Program(props: TProgram) { - return ( -
-
- program banner -

{props.program_title}

-
- -
- - - edit - -
-
- ); -} - function Skeleton() { return (
diff --git a/src/pages/Admin/Charity/Programs/Program.tsx b/src/pages/Admin/Charity/Programs/Program.tsx new file mode 100644 index 0000000000..01a6941ca6 --- /dev/null +++ b/src/pages/Admin/Charity/Programs/Program.tsx @@ -0,0 +1,59 @@ +import { Link } from "react-router-dom"; +import { ProgramDeleteMsg } from "services/types"; +import { Program as TProgram } from "types/aws"; +import { useAdminContext } from "pages/Admin/Context"; +import { useModalContext } from "contexts/ModalContext"; +import Image from "components/Image"; +import { TxPrompt } from "components/Prompt"; +import { adminRoutes } from "constants/routes"; +import useUpdateEndowmentProfile from "../common/useUpdateEndowmentProfile"; +import { ops } from "./ops"; + +export function Program(props: TProgram) { + const { id, owner } = useAdminContext<"charity">(ops); + const { showModal } = useModalContext(); + const updateProfile = useUpdateEndowmentProfile(); + + const deleteProgram = async (msg: ProgramDeleteMsg) => { + try { + await updateProfile(msg); + } catch (err) { + console.log(err); + showModal(TxPrompt, { + error: err instanceof Error ? err.message : "Unknown error occured", + }); + } + }; + + return ( +
+
+ program banner +

{props.program_title}

+
+ +
+ + + edit + +
+
+ ); +} diff --git a/src/pages/Admin/Charity/Programs/ops.ts b/src/pages/Admin/Charity/Programs/ops.ts new file mode 100644 index 0000000000..59c327797b --- /dev/null +++ b/src/pages/Admin/Charity/Programs/ops.ts @@ -0,0 +1,3 @@ +import { Operation } from "../../Context"; + +export const ops: Operation[] = ["name", "image", "logo", "sdgs"]; diff --git a/src/pages/Admin/Charity/common/useUpdateEndowmentProfile.ts b/src/pages/Admin/Charity/common/useUpdateEndowmentProfile.ts index 025716def9..0faecafc05 100644 --- a/src/pages/Admin/Charity/common/useUpdateEndowmentProfile.ts +++ b/src/pages/Admin/Charity/common/useUpdateEndowmentProfile.ts @@ -1,7 +1,6 @@ import { toUtf8 } from "@cosmjs/encoding"; import { hexlify } from "@ethersproject/bytes"; -import { EndowmentProfileUpdate } from "types/aws"; -import { SemiPartial } from "types/utils"; +import { ProfileUpdateMsg, ProgramDeleteMsg } from "services/types"; import { useEditProfileMutation } from "services/aws/aws"; import { useModalContext } from "contexts/ModalContext"; import { TxPrompt } from "components/Prompt"; @@ -23,13 +22,11 @@ export default function useUpdateEndowmentProfile() { const { showModal } = useModalContext(); const [submit] = useEditProfileMutation(); - const updateProfile = async ( - endowProfileUpdate: SemiPartial - ) => { + const updateProfile = async (msg: ProfileUpdateMsg | ProgramDeleteMsg) => { try { if (isTooltip(txResource)) throw new Error(txResource); - const cleanUpdates = cleanObject(endowProfileUpdate); + const cleanUpdates = cleanObject(msg); showModal( TxPrompt, diff --git a/src/pages/Profile/Body/Body.tsx b/src/pages/Profile/Body/Body.tsx index 13bf2f2bf9..d4363f8b5f 100644 --- a/src/pages/Profile/Body/Body.tsx +++ b/src/pages/Profile/Body/Body.tsx @@ -1,26 +1,20 @@ +import { Route, Routes } from "react-router-dom"; import BookmarkBtn from "components/BookmarkBtn"; import Breadcrumbs from "components/Breadcrumbs"; import ExtLink from "components/ExtLink"; import Icon from "components/Icon"; -import Seo from "components/Seo"; -import { APP_NAME, DAPP_URL, IS_AST } from "constants/env"; +import { IS_AST } from "constants/env"; import { appRoutes } from "constants/routes"; import { useProfileContext } from "../ProfileContext"; import DonateButton from "./DonateButton"; import GeneralInfo from "./GeneralInfo"; +import Program from "./Program"; export default function Body() { const p = useProfileContext(); return (
-
- + + + } + /> + + } + /> +
); diff --git a/src/pages/Profile/Body/GeneralInfo/Content/Container.tsx b/src/pages/Profile/Body/GeneralInfo/Content/Container.tsx deleted file mode 100644 index 2521c75787..0000000000 --- a/src/pages/Profile/Body/GeneralInfo/Content/Container.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { PropsWithChildren, useState } from "react"; -import Icon from "components/Icon"; - -type Props = PropsWithChildren<{ - title: string; -}>; - -export default function Container(props: Props) { - const [isOpen, setOpen] = useState(true); - - return ( -
-
setOpen((prev) => !prev)} - /> - - {isOpen && props.children} -
- ); -} - -function Header(props: { - isOpen: boolean; - title: string; - onClick: () => void; -}) { - return ( -
- {props.title} - -
- ); -} diff --git a/src/pages/Profile/Body/GeneralInfo/Content/Content.tsx b/src/pages/Profile/Body/GeneralInfo/Content/Content.tsx deleted file mode 100644 index b8e55e41eb..0000000000 --- a/src/pages/Profile/Body/GeneralInfo/Content/Content.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useProfileContext } from "pages/Profile/ProfileContext"; -import RichText from "components/RichText"; -import Container from "./Container"; - -export default function Content() { - const profile = useProfileContext(); - - return ( -
- - - -
- ); -} diff --git a/src/pages/Profile/Body/GeneralInfo/Content/index.ts b/src/pages/Profile/Body/GeneralInfo/Content/index.ts deleted file mode 100644 index 1a94541e5f..0000000000 --- a/src/pages/Profile/Body/GeneralInfo/Content/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Content"; diff --git a/src/pages/Profile/Body/GeneralInfo/DetailsColumn/Details.tsx b/src/pages/Profile/Body/GeneralInfo/DetailsColumn/Details.tsx deleted file mode 100644 index 6cad9ed900..0000000000 --- a/src/pages/Profile/Body/GeneralInfo/DetailsColumn/Details.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { PropsWithChildren } from "react"; -import { Profile } from "services/types"; -import { isEmpty } from "helpers"; - -// import { chainIds } from "constants/chainIds"; -// import Copier from "components/Copier"; -// import ExtLink from "components/ExtLink"; -// import Icon from "components/Icon"; -// import { getAddressUrl, maskAddress } from "helpers"; - -export default function Details(props: Profile) { - const { active_in_countries = [] } = props; - - return ( - <> - {!!props.registration_number && ( - {props.registration_number} - )} - {!!props.street_address && ( - {props.street_address} - )} - - {isEmpty(active_in_countries) - ? props.hq_country - : active_in_countries.join(", ")} - - {/* - - - {maskAddress(profile.owner, 14)} - - {maskAddress(profile.owner, 10)} - - - - - - - - */} - - ); -} - -function Detail(props: PropsWithChildren<{ title: string }>) { - return ( -
-

- {props.title} -

- - {props.children || "-"} - -
- ); -} diff --git a/src/pages/Profile/Body/GeneralInfo/DetailsColumn/DetailsColumn.tsx b/src/pages/Profile/Body/GeneralInfo/DetailsColumn/DetailsColumn.tsx index bed92738b7..fb53c5af25 100644 --- a/src/pages/Profile/Body/GeneralInfo/DetailsColumn/DetailsColumn.tsx +++ b/src/pages/Profile/Body/GeneralInfo/DetailsColumn/DetailsColumn.tsx @@ -1,13 +1,15 @@ +import { PropsWithChildren } from "react"; import { endow } from "services/types"; -import { useProfileContext } from "pages/Profile/ProfileContext"; +import { isEmpty } from "helpers"; +import { useProfileContext } from "../../../ProfileContext"; import DonateButton from "../../DonateButton"; import Balances from "./Balances"; -import Details from "./Details"; import Socials from "./Socials"; import Tags from "./Tags"; -export default function DetailsColumn({ className }: { className: string }) { - const profile = useProfileContext(); +export default function DetailsColumn({ className = "" }) { + const p = useProfileContext(); + const { active_in_countries = [] } = p; return (
@@ -15,13 +17,36 @@ export default function DetailsColumn({ className }: { className: string }) {
-
- {endow(profile) && } - {profile.social_media_urls && ( - + {!!p.registration_number && ( + {p.registration_number} + )} + {!!p.street_address && ( + {p.street_address} + )} + + {isEmpty(active_in_countries) + ? p.hq_country + : active_in_countries.join(", ")} + + {endow(p) && } + {p.social_media_urls && ( + )}
); } + +function Detail(props: PropsWithChildren<{ title: string }>) { + return ( +
+

+ {props.title} +

+ + {props.children || "-"} + +
+ ); +} diff --git a/src/pages/Profile/Body/GeneralInfo/GeneralInfo.tsx b/src/pages/Profile/Body/GeneralInfo/GeneralInfo.tsx index 326d91dd5e..844bb48411 100644 --- a/src/pages/Profile/Body/GeneralInfo/GeneralInfo.tsx +++ b/src/pages/Profile/Body/GeneralInfo/GeneralInfo.tsx @@ -1,14 +1,29 @@ -import Content from "./Content"; +import RichText from "components/RichText"; +import { useProfileContext } from "../../ProfileContext"; +import Container from "../common/Container"; import DetailsColumn from "./DetailsColumn"; +import Programs from "./Programs"; -type Props = { className: string }; - -export default function GeneralInfo({ className }: Props) { +export default function GeneralInfo({ className = "" }) { + const profile = useProfileContext(); return (
- +
+ + + + {profile.program.length > 0 && ( + + + + )} +
); diff --git a/src/pages/Profile/Body/GeneralInfo/Programs.tsx b/src/pages/Profile/Body/GeneralInfo/Programs.tsx new file mode 100644 index 0000000000..89b05062f5 --- /dev/null +++ b/src/pages/Profile/Body/GeneralInfo/Programs.tsx @@ -0,0 +1,36 @@ +import { Link } from "react-router-dom"; +import { Program as TProgram } from "types/aws"; +import Image from "components/Image"; +import RichText from "components/RichText"; +import { useProfileContext } from "../../ProfileContext"; + +export default function Programs() { + const { program } = useProfileContext(); + return ( +
+ {program.map((p) => ( + + ))} +
+ ); +} + +function Program(props: TProgram) { + return ( +
+ + +
+

{props.program_title}

+ +
+
+ ); +} diff --git a/src/pages/Profile/Body/Program/Milestones.tsx b/src/pages/Profile/Body/Program/Milestones.tsx new file mode 100644 index 0000000000..70636fbf9c --- /dev/null +++ b/src/pages/Profile/Body/Program/Milestones.tsx @@ -0,0 +1,24 @@ +import { MileStone } from "types/aws"; +import { Info } from "components/Status"; +import Container from "../common/Container"; + +type Props = { + classes?: string; + milestones: MileStone[]; +}; + +export default function Milestones({ classes = "", milestones }: Props) { + return ( + + {milestones.length > 0 ? ( + <> + ) : ( + No milestones found + )} + + ); +} diff --git a/src/pages/Profile/Body/Program/Program.tsx b/src/pages/Profile/Body/Program/Program.tsx new file mode 100644 index 0000000000..cb1fb2d48e --- /dev/null +++ b/src/pages/Profile/Body/Program/Program.tsx @@ -0,0 +1,56 @@ +import { useParams } from "react-router-dom"; +import { useProfileContext } from "pages/Profile/ProfileContext"; +import { useProgramQuery } from "services/aws/aws"; +import ContentLoader from "components/ContentLoader"; +import QueryLoader from "components/QueryLoader"; +import RichText from "components/RichText"; +import Container from "../common/Container"; +import Milestones from "./Milestones"; + +export default function Program({ className = "" }) { + const { id: endowId } = useProfileContext(); + const { id: programId = "" } = useParams(); + const query = useProgramQuery({ endowId, programId }, { skip: !programId }); + + return ( + }} + > + {(p) => ( +
+ + + + +
+ )} +
+ ); +} + +function Skeleton({ className = "" }) { + return ( +
+ +
+ + + + +
+
+ ); +} diff --git a/src/pages/Profile/Body/Program/index.ts b/src/pages/Profile/Body/Program/index.ts new file mode 100644 index 0000000000..73a045f0bb --- /dev/null +++ b/src/pages/Profile/Body/Program/index.ts @@ -0,0 +1 @@ +export { default } from "./Program"; diff --git a/src/pages/Profile/Body/common/Container.tsx b/src/pages/Profile/Body/common/Container.tsx new file mode 100644 index 0000000000..441462bcbb --- /dev/null +++ b/src/pages/Profile/Body/common/Container.tsx @@ -0,0 +1,69 @@ +import React, { PropsWithChildren, useState } from "react"; +import Icon from "components/Icon"; + +type Props = PropsWithChildren<{ + title: string; + expanded?: true; + classes?: string; +}>; + +export default function Container({ + expanded, + title, + children, + classes = "", +}: Props) { + const [isOpen, setOpen] = useState(true); + + return ( +
+ {expanded ? ( + + ) : ( +
setOpen((prev) => !prev)} + /> + )} + {isOpen && children} +
+ ); +} + +type HeaderProps = { + classes?: string; + title: string; + children?: React.ReactNode; +}; + +function StaticHeader({ title, classes = "", children }: HeaderProps) { + return ( +
+ {title} + {children} +
+ ); +} + +function Header(props: { + title: string; + isOpen: boolean; + onClick: () => void; +}) { + return ( + + + + ); +} diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index a6d4c1752f..f6ee0f504e 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -1,7 +1,9 @@ import { useParams } from "react-router-dom"; import { useProfileQuery } from "services/aws/aws"; import Image from "components/Image"; +import Seo from "components/Seo"; import { idParamToNum } from "helpers"; +import { APP_NAME, DAPP_URL } from "constants/env"; import Body from "./Body"; import PageError from "./PageError"; import ProfileContext, { useProfileContext } from "./ProfileContext"; @@ -15,20 +17,19 @@ export default function Profile() { skip: numId === 0, }); - if (isLoading) { - return ; - } - - if (isError || !data) { - return ; - } - - if (!data.published) { - return ; - } + if (isLoading) return ; + if (isError || !data) return ; + if (!data.published) return ; return ( +
diff --git a/src/services/aws/aws.ts b/src/services/aws/aws.ts index 75e43795ef..dc76086951 100644 --- a/src/services/aws/aws.ts +++ b/src/services/aws/aws.ts @@ -1,5 +1,5 @@ import { createApi, fetchBaseQuery, retry } from "@reduxjs/toolkit/query/react"; -import { Profile } from "../types"; +import { Profile, ProfileUpdatePayload, isDeleteMsg } from "../types"; import { EndowListPaginatedAWSQueryRes, EndowmentCard, @@ -7,11 +7,11 @@ import { EndowmentProfileUpdate, EndowmentsQueryParams, NewAST, + Program, TStrategy, WalletProfile, } from "types/aws"; import { NetworkType } from "types/lists"; -import { SemiPartial } from "types/utils"; import { createAuthToken } from "helpers"; import { chainIds } from "constants/chainIds"; import { IS_AST, IS_TEST } from "constants/env"; @@ -120,19 +120,18 @@ export const aws = createApi({ }; }, }), - editProfile: builder.mutation< - EndowmentProfile, - { - unsignedMsg: SemiPartial; - rawSignature: string; - } - >({ + program: builder.query({ + providesTags: ["profile"], + query: ({ endowId, programId }) => + `/${v(1)}/profile/${network}/program/${endowId}/${programId}`, + }), + editProfile: builder.mutation({ invalidatesTags: (result, error) => error ? [] : ["endowments", "profile", "walletProfile"], query: (payload) => { return { url: `/${v(2)}/profile/${network}/endowment`, - method: "PUT", + method: isDeleteMsg(payload.unsignedMsg) ? "DELETE" : "PUT", body: payload, }; }, @@ -161,6 +160,7 @@ export const { useStrategyCardsQuery, useEndowmentIdNamesQuery, useProfileQuery, + useProgramQuery, useEditProfileMutation, endpoints: { diff --git a/src/services/types.ts b/src/services/types.ts index 197d5a87c4..19a6be4c25 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,9 +1,14 @@ -import { ASTProfile, EndowmentProfile } from "types/aws"; +import { + ASTProfile, + EndowmentProfile, + EndowmentProfileUpdate, +} from "types/aws"; import { AxelarBridgeFees } from "types/aws"; import { EndowmentDetails } from "types/contracts"; import { ApplicationProposal } from "types/contracts/multisig"; import { AccountType } from "types/lists"; import { Transaction } from "types/tx"; +import { SemiPartial } from "types/utils"; export type MultisigConfig = { threshold: number; @@ -71,3 +76,27 @@ export type CharityApplication = ApplicationProposal & { confirmations: number; userConfirmed: boolean; }; + +export type ProfileUpdateMsg = SemiPartial< + EndowmentProfileUpdate, + "id" | "owner" +>; + +export type ProgramDeleteMsg = Pick< + EndowmentProfileUpdate, + "id" | "owner" | "program_id" +>; + +export type ProfileUpdatePayload = { + unsignedMsg: ProfileUpdateMsg | ProgramDeleteMsg; + rawSignature: string; +}; + +export function isDeleteMsg( + msg: ProfileUpdateMsg | ProgramDeleteMsg +): msg is ProgramDeleteMsg { + return ( + //for edits, program_id is accompanied by program:[] + Object.keys(msg).length === 3 && !!(msg as ProgramDeleteMsg).program_id + ); +} diff --git a/src/types/aws/ap/index.ts b/src/types/aws/ap/index.ts index 2770f7ffeb..bf6c9ce4e4 100644 --- a/src/types/aws/ap/index.ts +++ b/src/types/aws/ap/index.ts @@ -13,7 +13,7 @@ type EndowmentBalances = { on_hand_overall: number; }; -type MileStone = { +export type MileStone = { milestone_date: string; //isoDate milestone_description: string; milestone_media: string; @@ -28,6 +28,13 @@ export type Program = { program_milestones: MileStone[]; }; +export type EndowDesignation = + | "Charity" + | "Religious Organization" + | "University" + | "Hospital" + | "Other"; + type EndowmentBase = { hq_country?: string; endow_designation: EndowDesignation; @@ -69,6 +76,7 @@ export type ASTProfile = Pick & Partial