diff --git a/components/directory/CensusView.tsx b/components/directory/CensusView.tsx index d1d1355a..078861d8 100644 --- a/components/directory/CensusView.tsx +++ b/components/directory/CensusView.tsx @@ -23,7 +23,7 @@ import { useProfile } from '../auth/useProfile' import { List } from '../core/List' import { useBackend } from '@/components/hooks/useBackend' import { - ProfileListParams, + ProfileListParamsType, ProfileListResponse, ProfileSort, ProfileListFragment, @@ -48,7 +48,7 @@ export const CensusView = () => { const { user } = useProfile({ redirectTo: '/' }) const { useGetPaginated } = useBackend() - const input = useMemo(() => { + const input = useMemo(() => { // Only search if there are at least 2 characters const searchQuery = searchValue.length >= 2 ? searchValue : '' diff --git a/components/landing/NeighborhoodShowcase.tsx b/components/landing/NeighborhoodShowcase.tsx index 4757b5e0..30f74763 100644 --- a/components/landing/NeighborhoodShowcase.tsx +++ b/components/landing/NeighborhoodShowcase.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import Image from 'next/image' import { useBackend } from '@/components/hooks/useBackend' import { - LocationListParams, + LocationListParamsType, LocationListResponse, } from '@/utils/types/location' import { getImageUrlByIpfsHash } from '@/lib/image' @@ -15,7 +15,7 @@ export const NeighborhoodShowcase = () => { const { useGet } = useBackend() const { data } = useGet('LOCATION_LIST', { sort: 'votesDesc', - } as LocationListParams) + } as LocationListParamsType) const locations = !data || 'error' in data ? [] : data.locations.slice(0, 4) diff --git a/components/landing/NeighborhoodsTop6List.tsx b/components/landing/NeighborhoodsTop6List.tsx index 21cca3a7..820b7bed 100644 --- a/components/landing/NeighborhoodsTop6List.tsx +++ b/components/landing/NeighborhoodsTop6List.tsx @@ -1,7 +1,7 @@ import Link from 'next/link' import { useBackend } from '@/components/hooks/useBackend' import { - LocationListParams, + LocationListParamsType, LocationListResponse, LocationType, } from '@/utils/types/location' @@ -17,7 +17,7 @@ export const NeighborhoodsTop6List = () => { 'LOCATION_LIST', { locationType: LocationType.Neighborhood, - } as LocationListParams + } as LocationListParamsType ) const { voteForLocation } = useLocationVote(refetchLocations) diff --git a/components/neighborhoods/LocationsByVoteCount.tsx b/components/neighborhoods/LocationsByVoteCount.tsx index 0ebbb76e..3e786277 100644 --- a/components/neighborhoods/LocationsByVoteCount.tsx +++ b/components/neighborhoods/LocationsByVoteCount.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react' import InfiniteScroll from 'react-infinite-scroll-component' import { LocationFragment, - LocationListParams, + LocationListParamsType, LocationListResponse, } from '@/utils/types/location' import { useLocationVote } from '../hooks/useLocationVote' @@ -21,7 +21,7 @@ export const LocationsByVoteCount = () => { mutate: refetchLocations, } = useGetPaginated('LOCATION_LIST', { sort: 'votesDesc', - } as LocationListParams) + } as LocationListParamsType) const locations = data ? data.reduce( diff --git a/components/neighborhoods/edit-location/BasicDetailStep.tsx b/components/neighborhoods/edit-location/BasicDetailStep.tsx index b7c1c3ef..eebad9d8 100644 --- a/components/neighborhoods/edit-location/BasicDetailStep.tsx +++ b/components/neighborhoods/edit-location/BasicDetailStep.tsx @@ -1,6 +1,9 @@ import React, { useState } from 'react' import { useUpdateLocation } from '../useUpdateLocation' -import { AddressFragmentType, LocationEditParams } from '@/utils/types/location' +import { + AddressFragmentType, + LocationEditParamsType, +} from '@/utils/types/location' import { useError } from '@/components/hooks/useError' import { MAX_LOCATION_BIO_LENGTH, @@ -43,7 +46,7 @@ export const BasicDetailStep = ({ const { showError } = useError() - const [locationInput, setLocationInput] = useState({ + const [locationInput, setLocationInput] = useState({ name: location.name, caretakerEmail: location.caretakerEmail, tagline: location.tagline, @@ -57,7 +60,7 @@ export const BasicDetailStep = ({ const handleOnChange = ( e: React.ChangeEvent, - field: keyof LocationEditParams + field: keyof LocationEditParamsType ) => { const intFields = ['sleepCapacity', 'internetSpeedMbps'] diff --git a/components/neighborhoods/edit-location/DescriptionStep.tsx b/components/neighborhoods/edit-location/DescriptionStep.tsx index 01ca2853..2fcd1d4d 100644 --- a/components/neighborhoods/edit-location/DescriptionStep.tsx +++ b/components/neighborhoods/edit-location/DescriptionStep.tsx @@ -12,7 +12,7 @@ import { REQUIRED_FIELD_ERROR, } from '@/utils/validate' import { useError } from '@/components/hooks/useError' -import { LocationEditParams } from '@/utils/types/location' +import { LocationEditParamsType } from '@/utils/types/location' export const DescriptionStep = ({ name, @@ -22,7 +22,7 @@ export const DescriptionStep = ({ steps, }: StepProps) => { const { updateLocation } = useUpdateLocation(location.externId) - const [locationInput, setLocationInput] = useState({ + const [locationInput, setLocationInput] = useState({ description: location.description, }) diff --git a/components/neighborhoods/edit-location/OffersStep.tsx b/components/neighborhoods/edit-location/OffersStep.tsx index aeb03e43..7de117fc 100644 --- a/components/neighborhoods/edit-location/OffersStep.tsx +++ b/components/neighborhoods/edit-location/OffersStep.tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/router' import { useBackend } from '@/components/hooks/useBackend' import { OfferType, - OfferListParams, + OfferListParamsType, OfferListResponse, OfferNewParams, OfferNewResponse, @@ -36,7 +36,7 @@ export const OffersStep = ({ location ? 'OFFER_LIST' : null, { locationId: location.externId, - } as OfferListParams + } as OfferListParamsType ) const offerList = !offersData || 'error' in offersData ? [] : offersData.offers diff --git a/components/neighborhoods/edit-location/PhotoGalleryStep.tsx b/components/neighborhoods/edit-location/PhotoGalleryStep.tsx index b467033d..2179758e 100644 --- a/components/neighborhoods/edit-location/PhotoGalleryStep.tsx +++ b/components/neighborhoods/edit-location/PhotoGalleryStep.tsx @@ -3,7 +3,7 @@ import { useError } from '@/components/hooks/useError' import { useUpdateLocation } from '../useUpdateLocation' import { FileNameIpfsHashMap } from '@/lib/file-storage/types' import { - LocationEditParams, + LocationEditParamsType, LocationMediaCategory, } from '@/utils/types/location' import { @@ -35,7 +35,7 @@ export const PhotoGalleryStep = ({ [LocationMediaCategory.Features]: false, }) - const [locationInput, setLocationInput] = useState({ + const [locationInput, setLocationInput] = useState({ bannerImageIpfsHash: location.bannerImageIpfsHash, mediaItems: location.mediaItems?.map((mediaItem) => ({ ipfsHash: mediaItem?.ipfsHash, diff --git a/components/neighborhoods/useUpdateLocation.ts b/components/neighborhoods/useUpdateLocation.ts index c63a01ad..977c69ff 100644 --- a/components/neighborhoods/useUpdateLocation.ts +++ b/components/neighborhoods/useUpdateLocation.ts @@ -1,6 +1,6 @@ import { useBackend } from '@/components/hooks/useBackend' import { - LocationEditParams, + LocationEditParamsType, LocationEditResponse, } from '@/utils/types/location' @@ -10,7 +10,7 @@ export function useUpdateLocation(locationId: string | undefined) { locationId ? ['LOCATION', { externId: locationId }] : null ) - const updateLocation = async (inputData: LocationEditParams = {}) => { + const updateLocation = async (inputData: LocationEditParamsType = {}) => { if (locationId) { const data = await mutateLocation(inputData) diff --git a/components/neighborhoods/validations.ts b/components/neighborhoods/validations.ts index b3ea2013..98568469 100644 --- a/components/neighborhoods/validations.ts +++ b/components/neighborhoods/validations.ts @@ -7,8 +7,8 @@ import { truthyString, } from '@/utils/validate' import { emptyEditorValue } from '../core/slate/slate-utils' -import { OfferEditParams, OfferType } from '@/utils/types/offer' -import { LocationEditParams } from '@/utils/types/location' +import { OfferEditParamsType, OfferType } from '@/utils/types/offer' +import { LocationEditParamsType } from '@/utils/types/location' export type ValidationType = 'missing' | 'invalid' | 'valid' @@ -17,7 +17,7 @@ type ValidationResult = { valid: boolean } -export const validateLocationInput = (values: LocationEditParams) => { +export const validateLocationInput = (values: LocationEditParamsType) => { const { name, tagline, @@ -45,7 +45,7 @@ export const validateLocationInput = (values: LocationEditParams) => { export const validateOfferInput = ( type: OfferType, - newValues: OfferEditParams + newValues: OfferEditParamsType ) => { const invalid = !validateTitle(newValues.title).valid || diff --git a/components/offers/EditOfferPageView.tsx b/components/offers/EditOfferPageView.tsx index 5dd1f7ab..2597ae4a 100644 --- a/components/offers/EditOfferPageView.tsx +++ b/components/offers/EditOfferPageView.tsx @@ -5,7 +5,7 @@ import { useModal } from '../hooks/useModal' import { useNavigation } from '../hooks/useNavigation' import { useGetOffer } from './useGetOffer' import { useBackend } from '@/components/hooks/useBackend' -import { OfferEditParams, OfferType } from '@/utils/types/offer' +import { OfferEditParamsType, OfferType } from '@/utils/types/offer' import { SingleColumnLayout } from '../layouts/SingleColumnLayout' import { EditOfferView } from './EditOfferView' import { DiscardChangesModal } from '../core/DiscardChangesModal' @@ -34,7 +34,7 @@ export const EditOfferPageView = () => { const [highlightErrors, setHighlightErrors] = useState(false) const [unsavedChanges, setUnsavedChanges] = useState(false) - const [newValues, setNewValues] = useState({}) + const [newValues, setNewValues] = useState({}) useEffect(() => { if (offer) { @@ -78,7 +78,7 @@ export const EditOfferPageView = () => { } } - const handleOnEdit = (updateOfferInput: OfferEditParams) => { + const handleOnEdit = (updateOfferInput: OfferEditParamsType) => { setUnsavedChanges(true) setNewValues((prev) => ({ diff --git a/components/offers/EditOfferView.tsx b/components/offers/EditOfferView.tsx index 3b36a7de..a2b095ff 100644 --- a/components/offers/EditOfferView.tsx +++ b/components/offers/EditOfferView.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useRouter } from 'next/router' import { useModal } from '../hooks/useModal' -import { OfferEditParams, OfferFragment } from '@/utils/types/offer' +import { OfferEditParamsType, OfferFragment } from '@/utils/types/offer' import styled from 'styled-components' import { ContentCard } from '../core/ContentCard' import { TitleCard } from '../core/TitleCard' @@ -10,8 +10,8 @@ import { DiscardChangesModal } from '../core/DiscardChangesModal' interface EditOfferViewProps { offer: OfferFragment - updateOfferInput: OfferEditParams - onEdit: (updateOfferInput: OfferEditParams) => void + updateOfferInput: OfferEditParamsType + onEdit: (updateOfferInput: OfferEditParamsType) => void highlightErrors?: boolean } @@ -25,7 +25,7 @@ export const EditOfferView = ({ const { showModal } = useModal() const router = useRouter() - const handleEdit = (updateOfferInput: OfferEditParams) => { + const handleEdit = (updateOfferInput: OfferEditParamsType) => { setUnsavedChanges(true) onEdit(updateOfferInput) } diff --git a/components/offers/OfferTabList.tsx b/components/offers/OfferTabList.tsx index 0ff82d46..cec14fb4 100644 --- a/components/offers/OfferTabList.tsx +++ b/components/offers/OfferTabList.tsx @@ -3,7 +3,7 @@ import { useProfile } from '../auth/useProfile' import { useBackend } from '@/components/hooks/useBackend' import { OfferFragment, - OfferListParams, + OfferListParamsType, OfferListResponse, OfferType, } from '@/utils/types/offer' @@ -19,7 +19,7 @@ export const OfferTabList = ({ offerType }: { offerType?: OfferType }) => { useGetPaginated('OFFER_LIST', { offerType: offerType ?? undefined, publiclyVisibleOnly: 'true', - } as OfferListParams) + } as OfferListParamsType) const offers = data ? data.reduce( diff --git a/components/offers/edit-offer/EditOfferForm.tsx b/components/offers/edit-offer/EditOfferForm.tsx index 5e8ed552..ecc7b218 100644 --- a/components/offers/edit-offer/EditOfferForm.tsx +++ b/components/offers/edit-offer/EditOfferForm.tsx @@ -1,7 +1,11 @@ import React, { useState } from 'react' import { useRouter } from 'next/router' import { useModal } from '@/components/hooks/useModal' -import { OfferEditParams, OfferFragment, OfferType } from '@/utils/types/offer' +import { + OfferEditParamsType, + OfferFragment, + OfferType, +} from '@/utils/types/offer' import { MAX_OFFER_TITLE_LENGTH, PHOTO_UPLOAD_INSTRUCTIONS, @@ -32,8 +36,8 @@ import { useBackend } from '@/components/hooks/useBackend' interface EditOfferFormProps { offer: OfferFragment - updateOfferInput: OfferEditParams - onEdit: (updateOfferInput: OfferEditParams) => void + updateOfferInput: OfferEditParamsType + onEdit: (updateOfferInput: OfferEditParamsType) => void highlightErrors?: boolean } diff --git a/components/offers/edit-offer/Pricing.tsx b/components/offers/edit-offer/Pricing.tsx index 4eb95761..47b562cd 100644 --- a/components/offers/edit-offer/Pricing.tsx +++ b/components/offers/edit-offer/Pricing.tsx @@ -1,5 +1,5 @@ import { ChangeEvent } from 'react' -import { OfferEditParams, OfferPriceInterval } from '@/utils/types/offer' +import { OfferEditParamsType, OfferPriceInterval } from '@/utils/types/offer' import { REQUIRED_FIELD_ERROR } from '@/utils/validate' import styled from 'styled-components' import { SelectOption } from '@/components/hooks/useDropdownLogic' @@ -30,11 +30,11 @@ const options = Object.values(OfferPriceInterval).map((interval) => ({ })) interface PricingProps { - price?: OfferEditParams['price'] - priceInterval?: OfferEditParams['priceInterval'] + price?: OfferEditParamsType['price'] + priceInterval?: OfferEditParamsType['priceInterval'] onPriceChange?: ( - price: OfferEditParams['price'], - priceInterval: OfferEditParams['priceInterval'] + price: OfferEditParamsType['price'], + priceInterval: OfferEditParamsType['priceInterval'] ) => void highlightErrors?: boolean } diff --git a/components/profile/AboutInput.tsx b/components/profile/AboutInput.tsx index 72e582e2..7b4e2474 100644 --- a/components/profile/AboutInput.tsx +++ b/components/profile/AboutInput.tsx @@ -8,6 +8,7 @@ import { AvatarFragmentType } from '@/utils/types/profile' import { LocationAutocompleteInput } from '@/components/core/LocationAutocompleteInput' import { ADDRESS_ERROR } from '@/utils/validate' import { AddressFragmentType } from '@/utils/types/location' +import { NeighborhoodSelect } from '@/components/profile/RegistrationForm' interface AboutInputProps { bio: string @@ -16,6 +17,8 @@ interface AboutInputProps { onAddressChange: (location: AddressFragmentType) => void avatar?: AvatarFragmentType onAvatarChange?: (avatar: AvatarFragmentType | undefined) => void + neighborhoodExternId: string | null | undefined + onNeighborhoodChange: (n: string | null | undefined) => void } export const AboutInput = ({ @@ -25,6 +28,8 @@ export const AboutInput = ({ onAddressChange, avatar, onAvatarChange, + neighborhoodExternId, + onNeighborhoodChange, }: AboutInputProps) => { const handleBioChange = (e: React.ChangeEvent) => { onBioChange(e.target.value) @@ -51,6 +56,11 @@ export const AboutInput = ({ error={!isValidAddress(address)} errorMessage={ADDRESS_ERROR} /> + ) } diff --git a/components/profile/AvatarSetup.tsx b/components/profile/AvatarSetup.tsx index 6220ff7e..c77c0198 100644 --- a/components/profile/AvatarSetup.tsx +++ b/components/profile/AvatarSetup.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Network, OwnedNft } from 'alchemy-sdk' -import { ProfileEditParams } from '@/utils/types/profile' +import { ProfileEditParamsType } from '@/utils/types/profile' import { getImageUrlFromNft } from '@/lib/image' import { FileNameIpfsHashMap } from '@/lib/file-storage/types' import { getImageUrlByIpfsHash } from '@/lib/image' @@ -16,8 +16,10 @@ export type ExtendedOwnedNft = OwnedNft & { } interface AvatarSetupProps { - onNftSelected: (nft: ProfileEditParams['data']['avatar'] | undefined) => void - avatar?: ProfileEditParams['data']['avatar'] | undefined | null + onNftSelected: ( + nft: ProfileEditParamsType['data']['avatar'] | undefined + ) => void + avatar?: ProfileEditParamsType['data']['avatar'] | undefined | null } export const AvatarSetup = ({ onNftSelected, avatar }: AvatarSetupProps) => { diff --git a/components/profile/ContactInput.tsx b/components/profile/ContactInput.tsx index 0ac1c868..734eb266 100644 --- a/components/profile/ContactInput.tsx +++ b/components/profile/ContactInput.tsx @@ -1,6 +1,6 @@ import { ContactFieldType, - ContactFragment, + ContactFragmentType, MeFragment, } from '@/utils/types/profile' import { SetStateAction, useEffect } from 'react' @@ -23,8 +23,8 @@ const contactOptions = Object.values(ContactFieldType).map( interface ContactInputProps { profile: MeFragment - contactList: ContactFragment[] - setContactList: (contactList: SetStateAction) => void + contactList: ContactFragmentType[] + setContactList: (contactList: SetStateAction) => void } export const ContactInput = ({ diff --git a/components/profile/EditProfileForm.tsx b/components/profile/EditProfileForm.tsx index ecb8cdec..0a803cdf 100644 --- a/components/profile/EditProfileForm.tsx +++ b/components/profile/EditProfileForm.tsx @@ -1,9 +1,8 @@ import React, { useEffect, useState } from 'react' import { - ProfileEditParams, - RoleType, + ProfileEditParamsType, MeFragment, - ContactFragment, + ContactFragmentType, } from '@/utils/types/profile' import styled from 'styled-components' import { H3 } from '@/components/core/Typography' @@ -14,8 +13,8 @@ import { Identity } from './edit-profile/Identity' export interface UpdateProfileProps { user: MeFragment - profileEditParams: ProfileEditParams['data'] | null - onChange: (input: ProfileEditParams['data']) => void + profileEditParams: ProfileEditParamsType['data'] | null + onChange: (input: ProfileEditParamsType['data']) => void } export const EditProfileForm = ({ @@ -24,9 +23,8 @@ export const EditProfileForm = ({ onChange, }: { user: MeFragment - profileEditParams: ProfileEditParams['data'] | null - onChange: (input: ProfileEditParams['data']) => void - onRolesChange: (roleTypes: RoleType[]) => void + profileEditParams: ProfileEditParamsType['data'] | null + onChange: (input: ProfileEditParamsType['data']) => void }) => { if (!user) { return null @@ -58,6 +56,8 @@ export const EditProfileForm = ({ const About = ({ user, profileEditParams, onChange }: UpdateProfileProps) => { const bio = profileEditParams?.bio ?? user?.bio ?? '' const address = profileEditParams?.address ?? user?.address ?? undefined + const neighborhoodExternId = + profileEditParams?.neighborhoodExternId ?? user?.neighborhoodExternId return ( @@ -68,13 +68,17 @@ const About = ({ user, profileEditParams, onChange }: UpdateProfileProps) => { onAddressChange={(address) => onChange({ ...profileEditParams, address }) } + neighborhoodExternId={neighborhoodExternId} + onNeighborhoodChange={(n) => + onChange({ ...profileEditParams, neighborhoodExternId: n || null }) + } /> ) } const Contact = ({ profileEditParams, onChange, user }: UpdateProfileProps) => { - const [contactList, setContactList] = useState([]) + const [contactList, setContactList] = useState([]) useEffect(() => { if (contactList.length) { diff --git a/components/profile/EditProfileView.tsx b/components/profile/EditProfileView.tsx index a6a60f0a..f350ef4e 100644 --- a/components/profile/EditProfileView.tsx +++ b/components/profile/EditProfileView.tsx @@ -5,9 +5,8 @@ import { useProfile } from '../auth/useProfile' import { useModal } from '../hooks/useModal' import { useBackend } from '@/components/hooks/useBackend' import { - ProfileEditParams, + ProfileEditParamsType, ProfileEditResponse, - RoleType, } from '@/utils/types/profile' import { validateProfileInput } from './validations' import { EditProfileForm } from './EditProfileForm' @@ -26,8 +25,7 @@ export const EditProfileView = () => { user ? ['PROFILE', { externId: user.externId }] : null ) - const [newValues, setNewValues] = useState({}) - const [roleTypes, setRoleTypes] = useState(null) + const [newValues, setNewValues] = useState({}) const { showModal } = useModal() const handleSubmit = async () => { @@ -42,8 +40,7 @@ export const EditProfileView = () => { if (user && validateProfileInput(newValues)) { const res = await updateUser({ data: newValues, - roleTypes, - } as ProfileEditParams) + } as ProfileEditParamsType) if (!res.error) { await router.push(`/profile/${user.externId}`) @@ -59,12 +56,7 @@ export const EditProfileView = () => { } } - const handleRolesChange = (roleTypes: RoleType[]) => { - if (!roleTypes) return - setRoleTypes(roleTypes) - } - - const handleChange = (input: ProfileEditParams['data']) => { + const handleChange = (input: ProfileEditParamsType['data']) => { setNewValues((prev) => ({ ...prev, ...input, @@ -96,7 +88,6 @@ export const EditProfileView = () => { user={user} profileEditParams={newValues} onChange={handleChange} - onRolesChange={handleRolesChange} /> diff --git a/components/profile/RegistrationForm.tsx b/components/profile/RegistrationForm.tsx index 87d2f006..14bdbf59 100644 --- a/components/profile/RegistrationForm.tsx +++ b/components/profile/RegistrationForm.tsx @@ -4,7 +4,6 @@ import { useProfile } from '@/components/auth/useProfile' import { useExternalUser } from '@/components/auth/useExternalUser' import { useBackend } from '@/components/hooks/useBackend' import { - NeighborhoodFragment, NeighborhoodListParamsType, NeighborhoodListResponse, } from '@/utils/types/neighborhood' @@ -43,8 +42,8 @@ export function RegistrationForm({ const [avatar, setAvatar] = useState() const [name, setName] = useState('') const [address, setAddress] = useState() - const [neighborhood, setNeighborhood] = useState< - NeighborhoodFragment | null | undefined + const [neighborhoodExternId, setNeighborhoodExternId] = useState< + string | null | undefined >() const [canShowNameError, setCanShowNameError] = useState(false) @@ -111,7 +110,7 @@ export function RegistrationForm({ name: name.trim(), address, avatar, - neighborhoodExternId: neighborhood?.externId, + neighborhoodExternId: neighborhoodExternId || undefined, }) } else { linkEmail() @@ -154,8 +153,8 @@ export function RegistrationForm({ @@ -171,14 +170,14 @@ export function RegistrationForm({ ) } -function NeighborhoodSelect({ +export function NeighborhoodSelect({ address, - selected, + neighborhoodExternId, onNeighborhoodChange, }: { address: AddressFragmentType | undefined - selected: NeighborhoodFragment | null | undefined - onNeighborhoodChange: (n: NeighborhoodFragment | null | undefined) => void + neighborhoodExternId: string | null | undefined + onNeighborhoodChange: (n: string | null | undefined) => void }) { const label = 'Cabin Neighborhood' const { useGet } = useBackend() @@ -224,28 +223,40 @@ function NeighborhoodSelect({ } const n = o && neighborhoods.find((ne) => ne.externId === o.value) - onNeighborhoodChange(n) + onNeighborhoodChange(n?.externId) } useEffect(() => { + if (isLoading) { + return + } + if (!neighborhoodSelectOptions.length) { selectNeighborhood(undefined) return } - if (selected === undefined) { + if (neighborhoodExternId === undefined) { selectNeighborhood(neighborhoodSelectOptions[0]) return } const selectedInOptions = neighborhoodSelectOptions.find( - (n) => n.value === (selected === null ? '' : selected.externId) + (n) => + n.value === (neighborhoodExternId === null ? '' : neighborhoodExternId) ) if (!selectedInOptions) { selectNeighborhood(neighborhoodSelectOptions[0]) + } else if (!selectedOption) { + setSelectedOption(selectedInOptions) } - }, [neighborhoodSelectOptions, selected]) + }, [ + neighborhoodSelectOptions, + neighborhoodExternId, + selectedOption, + isLoading, + ]) if (!address || !neighborhoods.length) { return ( diff --git a/components/profile/edit-profile/Identity.tsx b/components/profile/edit-profile/Identity.tsx index 535f84df..b0a617f9 100644 --- a/components/profile/edit-profile/Identity.tsx +++ b/components/profile/edit-profile/Identity.tsx @@ -4,7 +4,10 @@ import { useExternalUser } from '@/components/auth/useExternalUser' import { useModal } from '@/components/hooks/useModal' import useEns from '@/components/hooks/useEns' import { useBackend } from '@/components/hooks/useBackend' -import { ProfileEditParams, ProfileEditResponse } from '@/utils/types/profile' +import { + ProfileEditParamsType, + ProfileEditResponse, +} from '@/utils/types/profile' import { shortenedAddress } from '@/utils/display-utils' import styled from 'styled-components' import { MAX_DISPLAY_NAME_LENGTH } from '../constants' @@ -66,7 +69,7 @@ export const Identity = ({ data: { email: externalUser.email.address, }, - } as ProfileEditParams) + } as ProfileEditParamsType) } }, [externalUser?.email?.address, updateProfile, user]) diff --git a/components/profile/setup-profile/AboutStep.tsx b/components/profile/setup-profile/AboutStep.tsx index 1e38d200..e0b9d964 100644 --- a/components/profile/setup-profile/AboutStep.tsx +++ b/components/profile/setup-profile/AboutStep.tsx @@ -1,6 +1,9 @@ import { useState } from 'react' import { useBackend } from '@/components/hooks/useBackend' -import { ProfileEditParams, ProfileEditResponse } from '@/utils/types/profile' +import { + ProfileEditParamsType, + ProfileEditResponse, +} from '@/utils/types/profile' import { useProfile } from '@/components/auth/useProfile' import { StepProps } from './step-configuration' import { SetupStepForm } from './SetupStepForm' @@ -14,6 +17,9 @@ export const AboutStep = ({ name, onBack, onNext }: StepProps) => { const [bio, setBio] = useState(user?.bio ?? '') const [address, setAddress] = useState(user?.address) const [avatar, setAvatar] = useState(user?.avatar) + const [neighborhoodExternId, setNeighborhoodExternId] = useState( + user?.neighborhoodExternId + ) const { useMutate } = useBackend() const { trigger: updateProfile } = useMutate( @@ -41,8 +47,9 @@ export const AboutStep = ({ name, onBack, onNext }: StepProps) => { bio, address, avatar, + neighborhoodExternId: neighborhoodExternId || null, }, - } as ProfileEditParams) + } as ProfileEditParamsType) onNext() } @@ -51,11 +58,13 @@ export const AboutStep = ({ name, onBack, onNext }: StepProps) => { ) diff --git a/components/profile/setup-profile/ContactStep.tsx b/components/profile/setup-profile/ContactStep.tsx index ab6c3ad0..98d6b7c7 100644 --- a/components/profile/setup-profile/ContactStep.tsx +++ b/components/profile/setup-profile/ContactStep.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { SetupStepForm } from './SetupStepForm' -import { ContactFragment, ProfileEditResponse } from '@/utils/types/profile' +import { ContactFragmentType, ProfileEditResponse } from '@/utils/types/profile' import { ContactInput } from '../ContactInput' import { useProfile } from '@/components/auth/useProfile' import { StepProps } from './step-configuration' @@ -8,7 +8,7 @@ import { useBackend } from '@/components/hooks/useBackend' export const ContactStep = ({ name, onBack, onNext }: StepProps) => { const { user } = useProfile() - const [contactList, setContactList] = useState([]) + const [contactList, setContactList] = useState([]) const { useMutate } = useBackend() const { trigger: updateProfile } = useMutate( user ? ['PROFILE', { externId: user.externId }] : null diff --git a/components/profile/validations.ts b/components/profile/validations.ts index 8e32e81d..801d95ef 100644 --- a/components/profile/validations.ts +++ b/components/profile/validations.ts @@ -1,11 +1,11 @@ -import { ProfileEditParams } from '@/utils/types/profile' +import { ProfileEditParamsType } from '@/utils/types/profile' import { MAX_DISPLAY_NAME_LENGTH, MAX_BIO_LENGTH } from './constants' import { EMAIL_VALID_REGEX } from '@/utils/validate' import { isAddress } from 'viem' import { AddressFragmentType } from '@/utils/types/location' export const validateProfileInput = ( - editProfileInput: ProfileEditParams['data'] + editProfileInput: ProfileEditParamsType['data'] ) => { const { name, email, bio, address } = editProfileInput diff --git a/pages/admin.tsx b/pages/admin.tsx index 39e99d20..e64ea2b7 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -13,7 +13,7 @@ import { REQUIRED_FIELD_ERROR } from '@/utils/validate' import { ProfileEditResponse, ProfileListFragment, - ProfileListParams, + ProfileListParamsType, ProfileListResponse, } from '@/utils/types/profile' import { ErrorModal } from '@/components/ErrorModal' @@ -35,7 +35,7 @@ export default function Page({ const { data, page, setPage, isEmpty, isLastPage, mutate } = useGetPaginated('PROFILE_LIST', { withLocation: 'true', - } as ProfileListParams) + } as ProfileListParamsType) const profiles = data ? data.reduce( diff --git a/pages/api/v2/location/[externId].ts b/pages/api/v2/location/[externId].ts index d3d5e8c2..20e8d0e3 100644 --- a/pages/api/v2/location/[externId].ts +++ b/pages/api/v2/location/[externId].ts @@ -1,12 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next' +import { prisma } from '@/lib/prisma' +import { Prisma } from '@prisma/client' +import { toErrorString } from '@/utils/api/error' import { AuthData, ProfileWithWallet, requireProfile, withAuth, } from '@/utils/api/withAuth' -import { prisma } from '@/lib/prisma' -import { Prisma } from '@prisma/client' import { LocationDeleteResponse, LocationEditParams, @@ -87,7 +88,12 @@ async function handlePost( return } - const params: LocationEditParams = req.body + const parsed = LocationEditParams.safeParse(req.body) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) + return + } + const params = parsed.data const mediaItemsToDelete: number[] = [] if (params.mediaItems) { diff --git a/pages/api/v2/location/list.ts b/pages/api/v2/location/list.ts index ded795ba..09b5368b 100644 --- a/pages/api/v2/location/list.ts +++ b/pages/api/v2/location/list.ts @@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { withAuth } from '@/utils/api/withAuth' import { prisma } from '@/lib/prisma' import { Prisma } from '@prisma/client' +import { toErrorString } from '@/utils/api/error' import { PAGE_SIZE } from '@/utils/api/backend' import { LocationFragment, @@ -26,21 +27,12 @@ async function handler( return } - const params: LocationListParams = { - // searchQuery: req.query.searchQuery - // ? (req.query.searchQuery as string) - // : undefined, - offerType: req.query.offerType - ? (req.query.offerType as OfferType) - : undefined, - locationType: req.query.locationType - ? (req.query.locationType as LocationType) - : undefined, - sort: req.query.sort ? (req.query.sort as LocationSort) : undefined, - page: req.query.page ? parseInt(req.query.page as string) : undefined, + const parsed = LocationListParams.safeParse(req.query) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) + return } - - // TODO: data validation + const params = parsed.data const skip = params.page ? PAGE_SIZE * (params.page - 1) : 0 const take = PAGE_SIZE diff --git a/pages/api/v2/neighborhood/list.ts b/pages/api/v2/neighborhood/list.ts index 7aef8de3..d207427a 100644 --- a/pages/api/v2/neighborhood/list.ts +++ b/pages/api/v2/neighborhood/list.ts @@ -4,7 +4,6 @@ import { prisma } from '@/lib/prisma' import { Prisma } from '@prisma/client' import { NeighborhoodFragment, - NeighborhoodListParamsType, NeighborhoodListParams, NeighborhoodListResponse, NeighborhoodQueryInclude, @@ -24,13 +23,12 @@ async function handler( return } - let params: NeighborhoodListParamsType - try { - params = NeighborhoodListParams.parse(req.query) - } catch (e) { - res.status(400).send({ error: toErrorString(e) }) + const parsed = NeighborhoodListParams.safeParse(req.query) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) return } + const params = parsed.data const idsInOrder = await sortByDistancePrequery( params.lat, diff --git a/pages/api/v2/offer/[externId].ts b/pages/api/v2/offer/[externId].ts index 2ac8bcf0..96517fe9 100644 --- a/pages/api/v2/offer/[externId].ts +++ b/pages/api/v2/offer/[externId].ts @@ -1,12 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next' +import { prisma } from '@/lib/prisma' +import { Prisma } from '@prisma/client' +import { toErrorString } from '@/utils/api/error' import { AuthData, ProfileWithWallet, requireProfile, withAuth, } from '@/utils/api/withAuth' -import { prisma } from '@/lib/prisma' -import { Prisma } from '@prisma/client' import { OfferDeleteResponse, OfferEditParams, @@ -65,6 +66,13 @@ async function handlePost( res: NextApiResponse, profile: ProfileWithWallet ) { + const parsed = OfferEditParams.safeParse(req.body) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) + return + } + const params = parsed.data + const externId = req.query.externId as string const offerToEdit = await prisma.offer.findUnique({ @@ -84,8 +92,6 @@ async function handlePost( return } - const params: OfferEditParams = req.body - const mediaItemsToDelete: number[] = [] if (params.mediaItems) { for (const mediaItem of offerToEdit.mediaItems) { diff --git a/pages/api/v2/offer/list.ts b/pages/api/v2/offer/list.ts index 9ef10e18..dae670f2 100644 --- a/pages/api/v2/offer/list.ts +++ b/pages/api/v2/offer/list.ts @@ -13,6 +13,7 @@ import { OfferPriceInterval, } from '@/utils/types/offer' import { LocationType } from '@/utils/types/location' +import { toErrorString } from '@/utils/api/error' async function handler( req: NextApiRequest, @@ -24,25 +25,12 @@ async function handler( return } - const params: OfferListParams = { - // searchQuery: req.query.searchQuery - // ? (req.query.searchQuery as string) - // : undefined, - offerType: req.query.offerType - ? (req.query.offerType as OfferType) - : undefined, - locationId: req.query.locationId - ? (req.query.locationId as string) - : undefined, - // sort: req.query.sort ? (req.query.sort as LocationSort) : undefined, - page: req.query.page ? parseInt(req.query.page as string) : undefined, - publiclyVisibleOnly: - req.query.publiclyVisibleOnly && req.query.publiclyVisibleOnly === 'true' - ? 'true' - : 'false', + const parsed = OfferListParams.safeParse(req.query) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) + return } - - // TODO: data validation + const params = parsed.data const publiclyVisibleOnly = params.publiclyVisibleOnly === 'true' diff --git a/pages/api/v2/profile/[externId].ts b/pages/api/v2/profile/[externId].ts index 6db6c5f0..0787da32 100644 --- a/pages/api/v2/profile/[externId].ts +++ b/pages/api/v2/profile/[externId].ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { prisma } from '@/lib/prisma' import { $Enums, Prisma } from '@prisma/client' +import { toErrorString } from '@/utils/api/error' import { AuthData, requireProfile, @@ -28,6 +29,11 @@ type ProfileWithRelations = Prisma.ProfileGetPayload<{ } } address: true + neighborhood: { + select: { + externId: true + } + } avatar: { select: { url: true @@ -94,6 +100,11 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse) { }, }, address: true, + neighborhood: { + select: { + externId: true, + }, + }, avatar: { select: { url: true, @@ -138,7 +149,13 @@ async function handlePost( res: NextApiResponse, profile: ProfileWithWallet ) { - const params: ProfileEditParams = req.body + const parsed = ProfileEditParams.safeParse(req.body) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) + return + } + const params = parsed.data + const externId = req.query.externId as string if (externId != profile.externId && !profile.isAdmin) { @@ -174,6 +191,11 @@ async function handlePost( }, } : undefined, + neighborhood: params.data.neighborhoodExternId + ? { connect: { externId: params.data.neighborhoodExternId } } + : params.data.neighborhoodExternId === null + ? { disconnect: true } + : undefined, }, where: { id: profileToEdit.id, @@ -277,6 +299,9 @@ const profileToFragment = (profile: ProfileWithRelations): ProfileFragment => { name: profile.name, email: profile.email, bio: profile.bio, + neighborhoodExternId: profile.neighborhood + ? profile.neighborhood.externId + : null, address: profile.address ? { locality: profile.address.locality, diff --git a/pages/api/v2/profile/list.ts b/pages/api/v2/profile/list.ts index 178805a8..02d3134f 100644 --- a/pages/api/v2/profile/list.ts +++ b/pages/api/v2/profile/list.ts @@ -1,7 +1,8 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { AuthData, requireAuth, withAuth } from '@/utils/api/withAuth' import { prisma } from '@/lib/prisma' import { Prisma } from '@prisma/client' +import { toErrorString } from '@/utils/api/error' +import { AuthData, requireAuth, withAuth } from '@/utils/api/withAuth' import { resolveAddressOrName } from '@/lib/ens' import { PAGE_SIZE } from '@/utils/api/backend' import { @@ -14,47 +15,7 @@ import { ProfileListFragment, } from '@/utils/types/profile' -// must match ListedProfileQueryInclude below -type ListedProfileWithRelations = Prisma.ProfileGetPayload<{ - include: { - address: true - avatar: true - wallet: { - include: { - _count: { - select: { - badges: true - } - } - } - } - roles: { - include: { - walletHat: true - } - } - } -}> - -// must match ListedProfileWithRelations above -const ListedProfileQueryInclude = { - address: true, - avatar: true, - wallet: { - include: { - _count: { - select: { - badges: true, - }, - }, - }, - }, - roles: { - include: { - walletHat: true, - }, - }, -} satisfies Prisma.ProfileInclude +export default withAuth(handler) async function handler( req: NextApiRequest, @@ -68,26 +29,17 @@ async function handler( requireAuth(req, res, opts) - const params: ProfileListParams = { - searchQuery: req.query.searchQuery - ? (req.query.searchQuery as string) - : undefined, - roleTypes: toArray(req.query.roleTypes), - levelTypes: toArray(req.query.levelTypes), - citizenshipStatuses: toArray( - req.query.citizenshipStatuses - ), - withLocation: req.query.withLocation === 'true' ? 'true' : undefined, - sort: req.query.sort ? (req.query.sort as ProfileSort) : undefined, - page: req.query.page ? parseInt(req.query.page as string) : undefined, + const parsed = ProfileListParams.safeParse(req.query) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) + return } + const params = parsed.data const resolvedAddress = params.searchQuery ? await resolveAddressOrName(params.searchQuery) : undefined - // TODO: data validation - const profileQuery: Prisma.ProfileFindManyArgs = { where: { name: @@ -205,7 +157,9 @@ const profilesToFragments = ( name: profile.name, email: profile.email, bio: profile.bio, - location: profile.location, + neighborhoodExternId: profile.neighborhood + ? profile.neighborhood.externId + : null, isAdmin: profile.isAdmin, mailingListOptIn: profile.mailingListOptIn, voucherId: profile.voucherId, @@ -246,14 +200,54 @@ const profilesToFragments = ( }) } -const toArray = ( - param: string | string[] | undefined -): T[] => { - if (!param) return [] - if (typeof param === 'string' || param instanceof String) { - return param.split(',') as T[] +// must match ListedProfileQueryInclude below +type ListedProfileWithRelations = Prisma.ProfileGetPayload<{ + include: { + address: true + neighborhood: { + select: { + externId: true + } + } + avatar: true + wallet: { + include: { + _count: { + select: { + badges: true + } + } + } + } + roles: { + include: { + walletHat: true + } + } } - return param as T[] -} +}> -export default withAuth(handler) +// must match ListedProfileWithRelations above +const ListedProfileQueryInclude = { + address: true, + neighborhood: { + select: { + externId: true, + }, + }, + avatar: true, + wallet: { + include: { + _count: { + select: { + badges: true, + }, + }, + }, + }, + roles: { + include: { + walletHat: true, + }, + }, +} satisfies Prisma.ProfileInclude diff --git a/pages/api/v2/profile/me.ts b/pages/api/v2/profile/me.ts index c810cab8..a0e0d7a3 100644 --- a/pages/api/v2/profile/me.ts +++ b/pages/api/v2/profile/me.ts @@ -57,6 +57,9 @@ const profileToFragment = (profile: MyProfileWithRelations): MeFragment => { email: profile.email, bio: profile.bio, address: profile.address || undefined, + neighborhoodExternId: profile.neighborhood + ? profile.neighborhood.externId + : null, inviteCode: profile.inviteCode ?? '', citizenshipStatus: profile.citizenshipStatus as CitizenshipStatus, citizenshipTokenId: profile.citizenshipTokenId, @@ -111,6 +114,11 @@ type MyProfileWithRelations = Prisma.ProfileGetPayload<{ } } address: true + neighborhood: { + select: { + externId: true + } + } avatar: { select: { url: true @@ -152,6 +160,11 @@ const MyProfileQueryInclude = { }, }, address: true, + neighborhood: { + select: { + externId: true, + }, + }, avatar: { select: { url: true, diff --git a/pages/api/v2/profile/new.ts b/pages/api/v2/profile/new.ts index e3b6e5f9..0551c33d 100644 --- a/pages/api/v2/profile/new.ts +++ b/pages/api/v2/profile/new.ts @@ -2,11 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { AuthData, requireAuth, withAuth } from '@/utils/api/withAuth' import { prisma } from '@/lib/prisma' import { Prisma } from '@prisma/client' -import { - ProfileNewParams, - ProfileNewParamsType, - ProfileNewResponse, -} from '@/utils/types/profile' +import { ProfileNewParams, ProfileNewResponse } from '@/utils/types/profile' import { createProfile } from '@/utils/profile' import { toErrorString } from '@/utils/api/error' @@ -25,18 +21,17 @@ async function handler( const privyDID = requireAuth(req, res, opts) - let body: ProfileNewParamsType - try { - body = ProfileNewParams.parse(req.body) - } catch (e) { - res.status(400).send({ error: toErrorString(e) }) + const parsed = ProfileNewParams.safeParse(req.body) + if (!parsed.success) { + res.status(400).send({ error: toErrorString(parsed.error) }) return } + const params = parsed.data let invite: Prisma.InviteGetPayload | null = null - if (body.inviteExternId) { + if (params.inviteExternId) { invite = await prisma.invite.findUnique({ - where: { externId: body.inviteExternId }, + where: { externId: params.inviteExternId }, }) if (!invite) { @@ -46,7 +41,7 @@ async function handler( } const existingWallet = await prisma.wallet.findUnique({ - where: { address: body.walletAddress }, + where: { address: params.walletAddress }, include: { profile: true }, }) @@ -59,12 +54,12 @@ async function handler( const profile = await createProfile({ privyDID, - walletAddress: body.walletAddress, - name: body.name, - email: body.email, - neighborhoodExternId: body.neighborhoodExternId, - address: body.address, - avatar: body.avatar, + walletAddress: params.walletAddress, + name: params.name, + email: params.email, + neighborhoodExternId: params.neighborhoodExternId, + address: params.address, + avatar: params.avatar, invite, }) diff --git a/pages/profile/[id]/setup.tsx b/pages/profile/[id]/setup.tsx index d6cac687..fca4d3d8 100644 --- a/pages/profile/[id]/setup.tsx +++ b/pages/profile/[id]/setup.tsx @@ -1,7 +1,7 @@ import { SetupProfileView } from '@/components/profile/SetupProfileView' const SetupPage = () => { - return + return } export default SetupPage diff --git a/utils/types/location.ts b/utils/types/location.ts index 45a41682..31fc6af9 100644 --- a/utils/types/location.ts +++ b/utils/types/location.ts @@ -28,20 +28,22 @@ export enum LocationSort { votesDesc = 'votesDesc', } -export const AddressFragment = z.object({ - lat: z.number().nullable(), - lng: z.number().nullable(), - formattedAddress: z.string().nullable(), - streetNumber: z.string().nullable(), - route: z.string().nullable(), - routeShort: z.string().nullable(), - locality: z.string().nullable(), - admininstrativeAreaLevel1: z.string().nullable(), - admininstrativeAreaLevel1Short: z.string().nullable(), - country: z.string().nullable(), - countryShort: z.string().nullable(), - postalCode: z.string().nullable(), -}) +export const AddressFragment = z + .object({ + lat: z.number().nullable(), + lng: z.number().nullable(), + formattedAddress: z.string().nullable(), + streetNumber: z.string().nullable(), + route: z.string().nullable(), + routeShort: z.string().nullable(), + locality: z.string().nullable(), + admininstrativeAreaLevel1: z.string().nullable(), + admininstrativeAreaLevel1Short: z.string().nullable(), + country: z.string().nullable(), + countryShort: z.string().nullable(), + postalCode: z.string().nullable(), + }) + .strict() export type AddressFragmentType = z.infer @@ -79,13 +81,15 @@ export type LocationFragment = { voteCount: number } -export type LocationListParams = { - // searchQuery?: string - offerType?: OfferType - locationType?: LocationType - sort?: LocationSort - page?: number -} +export const LocationListParams = z + .object({ + offerType: z.nativeEnum(OfferType).optional(), + locationType: z.nativeEnum(LocationType).optional(), + sort: z.nativeEnum(LocationSort).optional(), + page: z.number().optional(), + }) + .strict() +export type LocationListParamsType = z.infer export type LocationListResponse = | ({ @@ -109,20 +113,25 @@ export type LocationNewResponse = { error?: string } -export type LocationEditParams = { - name?: string - tagline?: string - description?: string - address?: AddressFragmentType | null - sleepCapacity?: number - internetSpeedMbps?: number - caretakerEmail?: string | null - bannerImageIpfsHash?: string - mediaItems?: { - category: LocationMediaCategory - ipfsHash: string - }[] -} +export const LocationEditParams = z.object({ + name: z.string().optional(), + tagline: z.string().optional(), + description: z.string().optional(), + address: AddressFragment.nullable().optional(), + sleepCapacity: z.number().optional(), + internetSpeedMbps: z.number().optional(), + caretakerEmail: z.string().nullable().optional(), + bannerImageIpfsHash: z.string().optional(), + mediaItems: z + .array( + z.object({ + category: z.nativeEnum(LocationMediaCategory), + ipfsHash: z.string(), + }) + ) + .optional(), +}) +export type LocationEditParamsType = z.infer export type LocationEditResponse = { location?: LocationFragment | null diff --git a/utils/types/neighborhood.ts b/utils/types/neighborhood.ts index 26efae63..1793ba30 100644 --- a/utils/types/neighborhood.ts +++ b/utils/types/neighborhood.ts @@ -12,14 +12,16 @@ export type NeighborhoodFragment = { lng: number } -export const NeighborhoodListParams = z.object({ - lat: z.string().transform((x) => parseFloat(x)), - lng: z.string().transform((x) => parseFloat(x)), - maxDistance: z - .string({ description: 'max distance in km' }) - .optional() - .transform((x) => (x ? parseInt(x) : undefined)), -}) +export const NeighborhoodListParams = z + .object({ + lat: z.string().transform((x) => parseFloat(x)), + lng: z.string().transform((x) => parseFloat(x)), + maxDistance: z + .string({ description: 'max distance in km' }) + .optional() + .transform((x) => (x ? parseInt(x) : undefined)), + }) + .strict() export type NeighborhoodListParamsType = z.infer diff --git a/utils/types/offer.ts b/utils/types/offer.ts index 97c14c43..65556615 100644 --- a/utils/types/offer.ts +++ b/utils/types/offer.ts @@ -1,7 +1,8 @@ // need these types in a separate file because prisma cant be imported in the frontend -import { LocationType, ShortAddressFragmentType } from '@/utils/types/location' import { Prisma } from '@prisma/client' +import { z } from 'zod' +import { LocationType, ShortAddressFragmentType } from '@/utils/types/location' import { APIError, Paginated } from '@/utils/types/shared' // must match prisma's $Enums.OfferType @@ -55,12 +56,17 @@ export type OfferFragment = { } } -export type OfferListParams = { - locationId?: string - offerType?: OfferType - publiclyVisibleOnly?: 'true' | 'false' - page?: number -} +export const OfferListParams = z + .object({ + locationId: z.string().optional(), + offerType: z.nativeEnum(OfferType).optional(), + publiclyVisibleOnly: z + .union([z.literal('true'), z.literal('false')]) + .optional(), + page: z.number().optional(), + }) + .strict() +export type OfferListParamsType = z.infer export type OfferListResponse = | ({ @@ -85,19 +91,26 @@ export type OfferGetResponse = } | APIError -export type OfferEditParams = { - title?: string - description?: string - startDate?: string - endDate?: string - price?: number - priceInterval?: OfferPriceInterval - applicationUrl?: string - imageIpfsHash?: string - mediaItems?: { - ipfsHash: string - }[] -} +export const OfferEditParams = z + .object({ + title: z.string().optional(), + description: z.string().optional(), + startDate: z.string().optional(), + endDate: z.string().optional(), + price: z.number().optional(), + priceInterval: z.nativeEnum(OfferPriceInterval).optional(), + applicationUrl: z.string().optional(), + imageIpfsHash: z.string().optional(), + mediaItems: z + .array( + z.object({ + ipfsHash: z.string(), + }) + ) + .optional(), + }) + .strict() +export type OfferEditParamsType = z.infer export type OfferEditResponse = | { diff --git a/utils/types/profile.ts b/utils/types/profile.ts index 1dd4bf3b..e9c6dab8 100644 --- a/utils/types/profile.ts +++ b/utils/types/profile.ts @@ -59,8 +59,8 @@ export type ProfileListFragment = { name: string email: string bio: string - location: string address: ShortAddressFragmentType | null + neighborhoodExternId: string | null isAdmin: boolean mailingListOptIn: boolean | null voucherId: number | null @@ -73,15 +73,18 @@ export type ProfileListFragment = { cabinTokenBalanceInt: number } -export type ProfileListParams = { - searchQuery?: string - roleTypes?: RoleType[] - levelTypes?: RoleLevel[] - citizenshipStatuses?: CitizenshipStatus[] - withLocation?: 'true' | 'false' - sort?: ProfileSort - page?: number -} +export const ProfileListParams = z + .object({ + searchQuery: z.string().optional(), + roleTypes: z.array(z.nativeEnum(RoleType)).optional(), + levelTypes: z.array(z.nativeEnum(RoleLevel)).optional(), + citizenshipStatuses: z.array(z.nativeEnum(CitizenshipStatus)).optional(), + withLocation: z.union([z.literal('true'), z.literal('false')]).optional(), + sort: z.nativeEnum(ProfileSort).optional(), + page: z.number().optional(), + }) + .strict() +export type ProfileListParamsType = z.infer export type ProfileListResponse = | ({ @@ -134,6 +137,7 @@ export type ProfileBasicFragment = { export type ProfileFragment = ProfileBasicFragment & { privyDID: string address: ShortAddressFragmentType | undefined + neighborhoodExternId: string | null citizenshipTokenId: number | null citizenshipMintedAt: string | null wallet: { @@ -144,13 +148,16 @@ export type ProfileFragment = ProfileBasicFragment & { externId: string name: string } | null - contactFields: ContactFragment[] + contactFields: ContactFragmentType[] } -export type ContactFragment = { - type: ContactFieldType - value: string -} +export const ContactFragment = z + .object({ + type: z.nativeEnum(ContactFieldType), + value: z.string(), + }) + .strict() +export type ContactFragmentType = z.infer export type RoleFragment = { hatId: number | null @@ -168,15 +175,16 @@ export type BadgeFragment = { } } -export const AvatarFragment = z.object({ - url: z.string(), - contractAddress: z.string().nullish(), - title: z.string().nullish(), - tokenId: z.string().nullish(), - tokenUri: z.string().nullish(), - network: z.string().nullish(), -}) - +export const AvatarFragment = z + .object({ + url: z.string(), + contractAddress: z.string().nullable().optional(), + title: z.string().nullable().optional(), + tokenId: z.string().nullable().optional(), + tokenUri: z.string().nullable().optional(), + network: z.string().nullable().optional(), + }) + .strict() export type AvatarFragmentType = z.infer export type ProfileMeResponse = { @@ -194,6 +202,7 @@ export type MeFragment = { email: string bio: string address: AddressFragmentType | undefined + neighborhoodExternId: string | null inviteCode: string citizenshipStatus: CitizenshipStatus citizenshipTokenId: number | null @@ -210,34 +219,39 @@ export type MeFragment = { name: string } | null - contactFields: ContactFragment[] + contactFields: ContactFragmentType[] roles: RoleFragment[] locationCount: number } -export const ProfileNewParams = z.object({ - walletAddress: z.string(), - name: z.string(), - email: z.string(), - address: AddressFragment, - avatar: AvatarFragment.optional(), - inviteExternId: z.string().optional(), - neighborhoodExternId: z.string().optional(), -}) - +export const ProfileNewParams = z + .object({ + walletAddress: z.string(), + name: z.string(), + email: z.string(), + address: AddressFragment, + avatar: AvatarFragment.optional(), + inviteExternId: z.string().optional(), + neighborhoodExternId: z.string().optional(), + }) + .strict() export type ProfileNewParamsType = z.infer -export type ProfileEditParams = { - data: { - name?: string - email?: string - bio?: string - address?: AddressFragmentType - contactFields?: ContactFragment[] - avatar?: AvatarFragmentType - } - roleTypes?: RoleType[] -} +export const ProfileEditParams = z + .object({ + data: z.object({ + name: z.string().optional(), + email: z.string().optional(), + bio: z.string().optional(), + address: AddressFragment.optional(), + contactFields: z.array(ContactFragment).optional(), + avatar: AvatarFragment.optional(), + neighborhoodExternId: z.string().nullable().optional(), + }), + roleTypes: z.array(z.nativeEnum(RoleType)).optional(), + }) + .strict() +export type ProfileEditParamsType = z.infer export type ProfileEditResponse = { success: boolean