diff --git a/TODO.md b/TODO.md index 2eed767..9d16a36 100644 --- a/TODO.md +++ b/TODO.md @@ -6,18 +6,17 @@ > https://next-s3-upload.codingvalue.com/setup > API changes -- [] purpose of passing location details in the listing create api? - [] add a new field `hash` to image entity and have max length of 100(even though actually needed is less than 25) - [] always sort images to make the preview image as the first image (specially in the detail screen) - [] vehicle description needs to be at least 1000 characters? // new! -- [] make postal code into alpha numeric (need to be able to save 00500 as a postal code) -- [] make trim as optional - [] only return name for city api call, only return name and state code for state api call, remove id from country list & detail api call - [] is lease field really necessary? - [] vehicle brands have duplicate values - [] return empty array instead of 404 if no cities or states found - [] state city sql query should also use like instead of == +- [] featured listing needs a country path param +- [] user phone and address null in listing details response > Web app todo list @@ -31,8 +30,6 @@ - [] show safety tips similar to ikman under item details description - [] add web3 forms or similar to contact us - [] go through all the loading screens and make sure that the parents have animate pulse class -- [] add country code as inputPrefix for phone numbers -- [] show currency as inputPrefix for price inputs - [] avoid session?.user?.sub! - [] verify how emtpy, unauthorized and error component redirects & links work - [] refer create t3 structure and eslint @@ -41,6 +38,7 @@ - [] having debouncer in search inputs in search screen causes issues when page load during typing - [] show expiry date in listings throughout. specially when renewing listings - [] handle when visting posted listing from different country +- [] review all dynamic usage and try to use it at higher levels that in lower level > Need to verify - [] loading bug when changing query (while loading type something) (Added a possible fix) diff --git a/package.json b/package.json index f6e7b65..c5e32ab 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "@aws-sdk/client-s3": "^3.421.0", "@aws-sdk/s3-request-presigner": "^3.421.0", "@formkit/auto-animate": "^0.8.0", - "@headlessui/react": "^1.7.17", "@hookform/resolvers": "^3.1.1", + "@mui/base": "5.0.0-beta.21", "@t3-oss/env-nextjs": "^0.6.1", "@tanstack/react-query": "^4.35.3", "@types/node": "20.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86d36c8..03b6129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,8 @@ specifiers: '@aws-sdk/client-s3': ^3.421.0 '@aws-sdk/s3-request-presigner': ^3.421.0 '@formkit/auto-animate': ^0.8.0 - '@headlessui/react': ^1.7.17 '@hookform/resolvers': ^3.1.1 + '@mui/base': 5.0.0-beta.21 '@t3-oss/env-nextjs': ^0.6.1 '@tanstack/react-query': ^4.35.3 '@types/fslightbox-react': ^1.7.4 @@ -59,8 +59,8 @@ dependencies: '@aws-sdk/client-s3': 3.421.0 '@aws-sdk/s3-request-presigner': 3.421.0 '@formkit/auto-animate': 0.8.0 - '@headlessui/react': 1.7.17_biqbaboplfbrettd7655fr4n2y '@hookform/resolvers': 3.1.1_react-hook-form@7.45.2 + '@mui/base': 5.0.0-beta.21_ew3x2u2ugx4vldoypxrzsnu5mm '@t3-oss/env-nextjs': 0.6.1_4vjarjeahs5r4i4dbld6hjohfi '@tanstack/react-query': 4.35.3_biqbaboplfbrettd7655fr4n2y '@types/node': 20.1.3 @@ -730,18 +730,11 @@ packages: tslib: 2.6.2 dev: false - /@babel/runtime/7.21.5: - resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 - /@babel/runtime/7.23.2: resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 - dev: true /@eslint-community/eslint-utils/4.4.0_eslint@8.50.0: resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} @@ -780,6 +773,34 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@floating-ui/core/1.5.0: + resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} + dependencies: + '@floating-ui/utils': 0.1.6 + dev: false + + /@floating-ui/dom/1.5.3: + resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/utils': 0.1.6 + dev: false + + /@floating-ui/react-dom/2.0.2_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.5.3 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + + /@floating-ui/utils/0.1.6: + resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} + dev: false + /@formkit/auto-animate/0.8.0: resolution: {integrity: sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw==} dev: false @@ -794,18 +815,6 @@ packages: '@hapi/hoek': 9.3.0 dev: false - /@headlessui/react/1.7.17_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==} - engines: {node: '>=10'} - peerDependencies: - react: ^16 || ^17 || ^18 - react-dom: ^16 || ^17 || ^18 - dependencies: - client-only: 0.0.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - /@hookform/resolvers/3.1.1_react-hook-form@7.45.2: resolution: {integrity: sha512-tS16bAUkqjITNSvbJuO1x7MXbn7Oe8ZziDTJdA9mMvsoYthnOOiznOTGBYwbdlYBgU+tgpI/BtTU3paRbCuSlg==} peerDependencies: @@ -862,6 +871,58 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@mui/base/5.0.0-beta.21_ew3x2u2ugx4vldoypxrzsnu5mm: + resolution: {integrity: sha512-eTKWx3WV/nwmRUK4z4K1MzlMyWCsi3WJ3RtV4DiXZeRh4qd4JCyp1Zzzi8Wv9xM4dEBmqQntFoei716PzwmFfA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@floating-ui/react-dom': 2.0.2_biqbaboplfbrettd7655fr4n2y + '@mui/types': 7.2.7_@types+react@18.2.33 + '@mui/utils': 5.14.15_o5ysialsahsznabqayk7pree4e + '@popperjs/core': 2.11.8 + '@types/react': 18.2.33 + clsx: 2.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + + /@mui/types/7.2.7_@types+react@18.2.33: + resolution: {integrity: sha512-sofpWmcBqOlTzRbr1cLQuUDKaUYVZTw8ENQrtL39TECRNENEzwgnNPh6WMfqMZlMvf1Aj9DLg74XPjnLr0izUQ==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.33 + dev: false + + /@mui/utils/5.14.15_o5ysialsahsznabqayk7pree4e: + resolution: {integrity: sha512-QBfHovAvTa0J1jXuYDaXGk+Yyp7+Fm8GSqx6nK2JbezGqzCFfirNdop/+bL9Flh/OQ/64PeXcW4HGDdOge+n3A==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@types/prop-types': 15.7.9 + '@types/react': 18.2.33 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + /@next/env/14.0.0: resolution: {integrity: sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA==} dev: false @@ -1516,7 +1577,6 @@ packages: /@types/prop-types/15.7.9: resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==} - dev: true /@types/react-datepicker/4.15.0_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-kr10s8ex4+MmCJmzdhA9kfmoMQBmsW5uDYDlH+8f/PgStrp7rRaz23Y/cvTiMgvESVq8ujDh4SOo6jlSwEw13g==} @@ -1542,11 +1602,9 @@ packages: '@types/prop-types': 15.7.9 '@types/scheduler': 0.16.5 csstype: 3.1.2 - dev: true /@types/scheduler/0.16.5: resolution: {integrity: sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==} - dev: true /@types/semver/7.5.3: resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} @@ -2108,7 +2166,7 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.23.2 /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -4224,6 +4282,10 @@ packages: /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-is/18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false + /react-number-format/5.3.1_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-qpYcQLauIeEhCZUZY9jXZnnroOtdy3jYaS1zQ3M1Sr6r/KMOBEIGNIb7eKT19g2N1wbYgFgvDzs19hw5TrB8XQ==} peerDependencies: @@ -4287,12 +4349,8 @@ packages: which-builtin-type: 1.1.3 dev: true - /regenerator-runtime/0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - /regenerator-runtime/0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - dev: true /regexp.prototype.flags/1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} diff --git a/src/app/[locale]/dashboard/listings/(list)/page.tsx b/src/app/[locale]/dashboard/listings/(list)/page.tsx index a29fb33..e277c03 100644 --- a/src/app/[locale]/dashboard/listings/(list)/page.tsx +++ b/src/app/[locale]/dashboard/listings/(list)/page.tsx @@ -13,9 +13,10 @@ import { LocalePathParam, SearchParams } from "@/utils/types"; export default async function Page({ searchParams, params }: SearchParams & LocalePathParam) { const page = searchParams["PageNumber"] ?? "1"; const parsedSearchParams = DashboardListingFilterSchema.parse(searchParams); - const [session, listings] = await Promise.all([ + const [session, listings, brands] = await Promise.all([ getSession(), transformListingsListResponse(await api.getListings({ PageNumber: Number(page), ...parsedSearchParams })), + api.getVehicleBrands() ]); if (listings.items?.length === 0 && page !== "1") { @@ -26,7 +27,7 @@ export default async function Page({ searchParams, params }: SearchParams & Loca } + filter={} itemCount={listings.totalCount} /> diff --git a/src/components/DashboardListHeader/DashboardAllListFilter.tsx b/src/components/DashboardListHeader/DashboardAllListFilter.tsx index 0753912..197da94 100644 --- a/src/components/DashboardListHeader/DashboardAllListFilter.tsx +++ b/src/components/DashboardListHeader/DashboardAllListFilter.tsx @@ -1,13 +1,17 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { clsx } from "clsx"; import { FC } from "react"; import ClickAwayListener from "react-click-away-listener"; import { useForm } from "react-hook-form"; +import { getCitiesOfState, getStatesOfCountry } from "@/actions/localtionActions"; import { useDashboardListingsContext } from "@/providers/dashboard-listings-provider"; import { FuelTypeList, ListingTypeList, VehicleTypeList } from "@/utils/constants"; +import { COUNTRIES } from "@/utils/countries"; import { DashboardListingFilterSchema } from "@/utils/schemas"; -import { DashboardListFilterReq } from "@/utils/types"; +import { DashboardListFilterReq, LabelValue, UpdateProfileReq, VehicleBrand } from "@/utils/types"; +import { FilterAutoComplete as AutocompleteController } from "./DashboardFilterAutoComplete"; import { FilterInput as InputController } from "./DashboardFilterInput"; import { FilterSelect as SelectController } from "./DashboardFilterSelect"; import { FilterButton } from "./FilterButton"; @@ -16,6 +20,8 @@ import { useDashboardFilter } from "./FilterHooks"; const defaultFilter: DashboardListFilterReq = { Brand: "", City: "", + Country: "", + State: "", Condition: "", FuelType: "", ListingStatus: "", @@ -29,10 +35,10 @@ const defaultFilter: DashboardListFilterReq = { StartCreatedDate: "", }; -export const DashboardAllListFilter: FC = () => { +export const DashboardAllListFilter: FC<{ vehicleBrands?: VehicleBrand[] }> = ({ vehicleBrands = [] }) => { const { hasSearchParams, searchParamsObj, isLoading, newSearchQuery, setNewSearchQuery } = useDashboardListingsContext(); - const { handleSubmit, reset, control } = useForm({ + const { handleSubmit, reset, control, watch, setValue } = useForm({ resolver: zodResolver(DashboardListingFilterSchema), defaultValues: searchParamsObj, mode: "all", @@ -47,16 +53,63 @@ export const DashboardAllListFilter: FC = () => { searchParamsObj, }); + const country = watch("Country"); + const state = watch("State"); + const city = watch("City"); + + const countryCode = Object.keys(COUNTRIES).find((item) => COUNTRIES[item]?.[0] === country); + + const countryList: LabelValue[] = Object.keys(COUNTRIES).map((key) => ({ + label: COUNTRIES[key]?.[0]!, + value: COUNTRIES[key]?.[0]!, + })); + + const { + data: states = [], + isFetching: isLoadingStates, + isError: stateFetchError, + } = useQuery({ + queryFn: () => getStatesOfCountry(countryCode!), + enabled: !!countryCode, + queryKey: ["country-states", { locale: countryCode }], + onSettled: (data, _) => { + if (!!data?.length && !data?.some((item) => item.name === state)) { + setValue("State", ""); + setValue("City", ""); + } + }, + }); + + const stateList = states?.map((item) => ({ label: item.name, value: item.name }) as LabelValue); + + const stateCode = states.find((item) => item.name === state)?.stateCode; + + const { + data: cityList = [], + isFetching: isLoadingCities, + isError: cityFetchError, + } = useQuery({ + queryFn: () => getCitiesOfState(countryCode!, stateCode!), + enabled: !!countryCode && !!stateCode, + queryKey: ["country-state-cities", { locale: countryCode, stateCode }], + select: (data) => data.map((item) => ({ label: item.name, value: item.name }) as LabelValue), + onSettled: (data, _) => { + if (!!data?.length && !data?.some((item) => item.label === city)) { + setValue("City", ""); + } + }, + }); + return ( setDropdownOpen(false)}> -
    +
    Filters
    {hasSearchParams && ( - )} @@ -65,13 +118,57 @@ export const DashboardAllListFilter: FC = () => {
    - + + {stateList?.length > 0 && !stateFetchError ? ( + + ) : ( + + )} + + {cityList.length > 0 && !cityFetchError ? ( + + ) : ( + + )} + + ({ label: item.name, value: item.name }))} + placeholder="Toyota, Nissan, Honda, etc" + /> + + + { options={ListingTypeList} placeholder="All status types" /> - - + + + { placeholder="Created after date" type="date" /> + { placeholder="Created before date" type="date" /> - + + + { />
    -
+
); diff --git a/src/components/DashboardListHeader/DashboardFilterAutoComplete.tsx b/src/components/DashboardListHeader/DashboardFilterAutoComplete.tsx new file mode 100644 index 0000000..dd1fc5b --- /dev/null +++ b/src/components/DashboardListHeader/DashboardFilterAutoComplete.tsx @@ -0,0 +1,7 @@ +import { forwardRef } from "react"; +import { AutocompleteController, ControllerProps } from "@/components/FormElements/AutoComplete"; + +export const FilterAutoComplete = forwardRef((props, ref) => { + return ; +}); +FilterAutoComplete.displayName = "AutoComplete"; diff --git a/src/components/DashboardProfile/ProfileDetails.tsx b/src/components/DashboardProfile/ProfileDetails.tsx index f40c2fc..45438ed 100644 --- a/src/components/DashboardProfile/ProfileDetails.tsx +++ b/src/components/DashboardProfile/ProfileDetails.tsx @@ -81,7 +81,7 @@ export const ProfileDetails: FC = ({ profile, session, loading }) => { title="Phone Number" value={ <> - {countryPhoneCode ? `${countryPhoneCode} ` : ""} + {countryPhoneCode ? `(${countryPhoneCode}) ` : ""} {profile?.phone ?? "-"} } diff --git a/src/components/FormElements/AutoComplete/AutoCompleteController.tsx b/src/components/FormElements/AutoComplete/AutoCompleteController.tsx index 17c188b..9bc72e4 100644 --- a/src/components/FormElements/AutoComplete/AutoCompleteController.tsx +++ b/src/components/FormElements/AutoComplete/AutoCompleteController.tsx @@ -62,7 +62,7 @@ export const AutocompleteController: FC = ({ selectClassNames={selectClassNames} setFieldValue={(value: string | number) => field.onChange(value)} showSelectedTick={showSelectedTick} - value={field.value} + value={options?.find((item) => item.value === field.value)} /> )} diff --git a/src/components/FormElements/AutoComplete/Autocomplete.tsx b/src/components/FormElements/AutoComplete/Autocomplete.tsx index 7a42114..6ee54f1 100644 --- a/src/components/FormElements/AutoComplete/Autocomplete.tsx +++ b/src/components/FormElements/AutoComplete/Autocomplete.tsx @@ -1,12 +1,13 @@ "use client"; -import { Combobox } from "@headlessui/react"; +import { useAutocomplete } from "@mui/base/useAutocomplete"; import { clsx } from "clsx"; -import React, { forwardRef, useState } from "react"; +import React, { forwardRef } from "react"; import { Control } from "react-hook-form"; import { FormFieldControllerProps } from "@/components/FormElements/Common"; import { CheckIcon } from "@/icons"; import { unCamelCase } from "@/utils/helpers"; import { LabelValue } from "@/utils/types"; +import useForkRef from "./useForkRef"; export interface ControllerProps extends FormFieldControllerProps { control: Control; @@ -24,7 +25,7 @@ export interface Props extends Pick; setFieldValue?: (value: any) => void; - value?: string; + value?: LabelValue; } export const Autocomplete = forwardRef((props, ref) => { @@ -41,103 +42,89 @@ export const Autocomplete = forwardRef((props, ref) => onBlur, } = props; - const [query, setQuery] = useState(""); + const { getRootProps, getInputProps, getListboxProps, getOptionProps, anchorEl, setAnchorEl, groupedOptions, expanded, popupOpen } = + useAutocomplete({ + options, + onChange: (_, value) => { + if (setFieldValue) { + setFieldValue(value?.value || ""); + } + }, + onClose: (event) => { + if (onBlur) { + onBlur(event as React.FocusEvent); + } + }, + autoComplete: true, + value, + }); - const filteredOptions = - query === "" - ? options - : options.filter((option) => option.label.toLowerCase().replace(/\s+/g, "").includes(query.toLowerCase().replace(/\s+/g, ""))); + const rootRef = useForkRef(setAnchorEl); - const resetQueryIfNoMatch = () => { - if (query?.length > 0 && filteredOptions?.length === 0) { - setQuery(""); - } - }; - - const setSelectedValue = (newValue: string | number) => { - if (setFieldValue) { - setFieldValue(newValue); - } - }; + const inputRef = useForkRef(ref, getInputProps().ref); return ( - - {({ open }) => ( -
- - (option && option !== "" ? unCamelCase(option as string) : query)} - onBlur={(event) => { - setTimeout(() => { - if (onBlur) { - onBlur(event); - } - }, 200); - }} - onChange={(event) => setQuery(event.target.value)} - placeholder={placeholder} - ref={ref} - /> - - -
-
- {filteredOptions.length === 0 && query !== "" ? ( -
Nothing found.
- ) : ( - filteredOptions.map((option) => ( - - clsx({ - "hover:bg-base-200 w-full truncate line-clamp-1 overflow-hidden": true, - "font-bold": selected, - "bg-base-300 hover:bg-base-300": active, - }) - } - onClick={(event) => { - event.stopPropagation(); - setSelectedValue(option.value); - }} - value={option.value} +
+
+ +
+ {anchorEl && expanded && ( +
+
    + {groupedOptions?.length > 0 ? ( + groupedOptions.map((option, index) => { + const selected = value?.value === (option as LabelValue).value; + return ( +
  • - {({ selected }) => ( -
    - - {unCamelCase(option.label)} +
    + + {unCamelCase((option as LabelValue).label)} + + {showSelectedTick && selected ? ( + + - {showSelectedTick && selected ? ( - - - - ) : null} -
    - )} - - )) - )} -
    -
+ ) : null} +
+ + ); + }) + ) : ( +
Nothing found.
+ )} +
)} - +
); }); Autocomplete.displayName = "Autocomplete"; diff --git a/src/components/FormElements/AutoComplete/index.ts b/src/components/FormElements/AutoComplete/index.ts index 50a8f36..8dd903a 100644 --- a/src/components/FormElements/AutoComplete/index.ts +++ b/src/components/FormElements/AutoComplete/index.ts @@ -1,2 +1,3 @@ export { AutocompleteController } from "./AutoCompleteController"; export { Autocomplete } from "./Autocomplete"; +export type { ControllerProps } from "./Autocomplete"; diff --git a/src/components/FormElements/AutoComplete/useForkRef.ts b/src/components/FormElements/AutoComplete/useForkRef.ts new file mode 100644 index 0000000..00af69e --- /dev/null +++ b/src/components/FormElements/AutoComplete/useForkRef.ts @@ -0,0 +1,30 @@ +"use client"; +import * as React from "react"; + +export default function useForkRef(...refs: Array | undefined>): React.RefCallback | null { + /** + * This will create a new function if the refs passed to this hook change and are all defined. + * This means react will call the old forkRef with `null` and the new forkRef + * with the ref. Cleanup naturally emerges from this behavior. + */ + return React.useMemo(() => { + if (refs.every((ref) => ref == null)) { + return null; + } + + return (instance) => { + refs.forEach((ref) => { + setRef(ref, instance); + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, refs); +} + +function setRef(ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, value: T | null): void { + if (typeof ref === "function") { + ref(value); + } else if (ref) { + ref.current = value; + } +} diff --git a/src/components/FormElements/Input/Input.tsx b/src/components/FormElements/Input/Input.tsx index 20c4b2f..60183b6 100644 --- a/src/components/FormElements/Input/Input.tsx +++ b/src/components/FormElements/Input/Input.tsx @@ -1,10 +1,11 @@ "use client"; import { clsx } from "clsx"; import dynamic from "next/dynamic"; -import { ComponentProps, createContext, forwardRef, ReactElement, ReactNode, useContext } from "react"; +import { ComponentProps, createContext, forwardRef, ReactNode, useContext } from "react"; import { Control } from "react-hook-form"; import { FormFieldControllerProps } from "@/components/FormElements/Common"; +// todo: check if this context is worth it const NumericFormatLoadingContext = createContext<{ inputClassNames?: string }>({}); const NumericFormat = dynamic(() => import("react-number-format").then((mod) => mod.NumericFormat), { diff --git a/src/components/Forms/Listings/ListingForm.tsx b/src/components/Forms/Listings/ListingForm.tsx index a68bc00..99b046c 100644 --- a/src/components/Forms/Listings/ListingForm.tsx +++ b/src/components/Forms/Listings/ListingForm.tsx @@ -30,6 +30,12 @@ interface Props { form?: UseFormReturn; isLoading?: boolean; isMutating?: boolean; + isUpdateProfileEnabled?: boolean; + listingUser?: { + email?: string; + phoneCountryCode?: string; + phoneNumber?: string; + }; onMutate?: (values: CreateListingReq) => void; profile?: ListingUser; submitButton?: { @@ -39,12 +45,6 @@ interface Props { }; title?: string; vehicleBrands?: VehicleBrand[]; - isUpdateProfileEnabled?: boolean; - listingUser?: { - email?: string; - phoneNumber?: string; - phoneCountryCode?: string; - } } const DetailsItem = ({ title, value, loading }: { loading?: boolean; title: string; value: ReactNode }) => ( @@ -285,7 +285,9 @@ export const ListingForm: FC = (props) => { title="Phone Number" value={ <> - {listingUser?.phoneCountryCode ? `${listingUser?.phoneCountryCode} ` : ""} + + {listingUser?.phoneCountryCode ? `(${listingUser?.phoneCountryCode}) ` : ""} + {listingUser?.phoneNumber ?? "-"} } diff --git a/src/components/Forms/Profile/ProfileForm.tsx b/src/components/Forms/Profile/ProfileForm.tsx index 3818fd4..efa22f5 100644 --- a/src/components/Forms/Profile/ProfileForm.tsx +++ b/src/components/Forms/Profile/ProfileForm.tsx @@ -59,8 +59,8 @@ export const ProfileForm: FC = (props) => { queryFn: () => getStatesOfCountry(countryCode!), enabled: !!countryCode, queryKey: ["country-states", { locale: countryCode }], - onSettled: (data, err) => { - if (err || !data?.some((item) => item.name === state)) { + onSettled: (data, _) => { + if (!!data?.length && !data?.some((item) => item.name === state)) { (form as UseFormReturn).setValue("address.state", ""); (form as UseFormReturn).setValue("address.city", ""); } @@ -80,8 +80,8 @@ export const ProfileForm: FC = (props) => { enabled: !!countryCode && !!stateCode, queryKey: ["country-state-cities", { locale: countryCode, stateCode }], select: (data) => data.map((item) => ({ label: item.name, value: item.name }) as LabelValue), - onSettled: (data, err) => { - if (err || !data?.some((item) => item.label === city)) { + onSettled: (data, _) => { + if (!!data?.length && !data?.some((item) => item.label === city)) { (form as UseFormReturn).setValue("address.city", ""); } }, diff --git a/src/components/ListingDetails/ListingKeySpecifications/ListingKeySpecifications.tsx b/src/components/ListingDetails/ListingKeySpecifications/ListingKeySpecifications.tsx index a68d055..c95e8c0 100644 --- a/src/components/ListingDetails/ListingKeySpecifications/ListingKeySpecifications.tsx +++ b/src/components/ListingDetails/ListingKeySpecifications/ListingKeySpecifications.tsx @@ -6,7 +6,7 @@ import { Vehicle } from "@/utils/types"; interface Props { loading?: boolean; vehicle?: Vehicle; - countryCode?: string + countryCode?: string; } export const ListingKeySpecifications: FC = ({ vehicle, loading, countryCode }) => { @@ -19,7 +19,7 @@ export const ListingKeySpecifications: FC = ({ vehicle, loading, countryC if (vehicle?.model) { items.push({ label: "Modal", value: vehicle.model }); } - items.push({ label: "Trim / Edition", value: vehicle?.trim || '-' }); + items.push({ label: "Trim / Edition", value: vehicle?.trim || "-" }); if (vehicle?.yearOfManufacture) { items.push({ label: "Manufactured Year", value: getYearFromDateString(vehicle.yearOfManufacture) }); } @@ -30,7 +30,7 @@ export const ListingKeySpecifications: FC = ({ vehicle, loading, countryC items.push({ label: "Condition", value: unCamelCase(vehicle?.condition) }); } if (vehicle?.millage) { - items.push({ label: "Mileage", value: getFormattedDistance(vehicle?.millage?.distance,countryCode) }); + items.push({ label: "Mileage", value: getFormattedDistance(vehicle?.millage?.distance, countryCode) }); } if (vehicle?.transmission) { items.push({ label: "Transmission", value: unCamelCase(vehicle?.transmission) }); @@ -50,7 +50,7 @@ export const ListingKeySpecifications: FC = ({ vehicle, loading, countryC key={i} className={clsx( "flex animate-pulse flex-col items-center gap-0.5 lg:md:gap-0.5", - i % 2 === 0 ? "lg:items-start" : "lg:items-end", + i % 2 === 0 ? "lg:items-start" : "lg:items-end" )} >
diff --git a/src/components/ListingDetails/ListingSellerDetails/ListingSellerDetails.tsx b/src/components/ListingDetails/ListingSellerDetails/ListingSellerDetails.tsx index 248b277..7b8b7eb 100644 --- a/src/components/ListingDetails/ListingSellerDetails/ListingSellerDetails.tsx +++ b/src/components/ListingDetails/ListingSellerDetails/ListingSellerDetails.tsx @@ -1,14 +1,17 @@ import { clsx } from "clsx"; import { FC } from "react"; import { Avatar } from "@/components/Common/Avatar"; +import { COUNTRIES } from "@/utils/countries"; import { ListingUser } from "@/utils/types"; interface Props { loading?: boolean; user?: ListingUser; + // todo: add country code to phone } -// change color of email & phone on hover and also underline +// todo: change color of email & phone on hover and also underline export const ListingSellerDetails: FC = ({ user, loading }) => { + const countryPhoneCode = COUNTRIES[user?.address?.country || ""]?.[3]; return ( <>
@@ -40,7 +43,12 @@ export const ListingSellerDetails: FC = ({ user, loading }) => { {user?.phone && ( Contact Number: - {user.phone} + + <> + {countryPhoneCode ? `(${countryPhoneCode}) ` : ""} + {user?.phone ?? "-"} + + )}
Car Dealer
diff --git a/src/utils/api.ts b/src/utils/api.ts index 00d673e..315fadd 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -116,6 +116,7 @@ const fetchApi = { protectedDelete: (endpoint: string, config: RequestInit = {}) => fetchRequest(endpoint, { method: "DELETE", ...config }, true), }; +// todo: review all cache clearing logics(mainly ones specific to country) export const api = { getFeaturesList: () => @@ -128,7 +129,6 @@ export const api = { fetchApi.get(`/v1/Listings/posted/${locale}?${qs.stringify(req ?? {}, { skipEmptyString: true })}`, { next: { revalidate: revalidationTime.thirtyMins, tags: [apiTags.getPostedListings()] }, }), - // todo: remove locale param and handle this in the UI getPostedListingItem: (id: ListingIdType) => fetchApi.get(`/v1/Listings/posted/${id}`, { next: { tags: [apiTags.getPostedListingItem(id)], revalidate: revalidationTime.oneDay }, diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 1441e6a..ec74d88 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -199,6 +199,8 @@ export const DashboardListingFilterSchema = MyListingsFilterSchema.extend({ MinPrice: z.union([getNumericSchema('',0), z.literal("")]).optional(), MaxPrice: z.union([getNumericSchema('',0), z.literal("")]).optional(), City: z.string().optional(), + State: z.string().optional(), + Country: z.string().optional(), Brand: z.string().optional(), Model: z.string().optional(), VehicleType: z.union([z.nativeEnum(VehicleTypes), z.literal("")]).optional(),