diff --git a/TODO.md b/TODO.md index 1539e32..430c7d5 100644 --- a/TODO.md +++ b/TODO.md @@ -35,6 +35,14 @@ - [] update manifest https://web.dev/articles/add-manifest - [] minify manifest after changes! - [] fix chrome third party cookies blocked warning +- [] share new s3 credentials with Gayan + +> After deploy +- [] Add sentry +- [] Add monitors + +> for domain register 4,359.42 + > Web app performance improvements diff --git a/src/actions/cacheActions.ts b/src/actions/cacheActions.ts index f10ca3d..678041b 100644 --- a/src/actions/cacheActions.ts +++ b/src/actions/cacheActions.ts @@ -11,14 +11,22 @@ export const revalidateBrandsAction = async () => { revalidateTag(apiTags.getVehicleBrands()); }; -export const revalidatePosedListingsAction = async () => { +export const revalidateAllPosedListingsAction = async () => { revalidateTag(apiTags.getPostedListings()); }; -export const revalidateFeaturedListingsAction = async () => { +export const revalidatePosedListingsByCountryAction = async (countryCode: string) => { + revalidateTag(apiTags.getPostedListingsByCountry(countryCode)); +}; + +export const revalidateAllFeaturedListingsAction = async () => { revalidateTag(apiTags.getFeaturedListings()); }; +export const revalidateFeaturedListingsByCountryAction = async (countryCode: string) => { + revalidateTag(apiTags.getFeatureListingsByCountry(countryCode)); +}; + export const revalidateRelatedListingsAction = async (listingId: ListingIdType) => { revalidateTag(apiTags.getRelatedListings(listingId)); }; @@ -27,4 +35,11 @@ export const revalidateUserNotificationsAction = async (userId: string) => { revalidateTag(apiTags.getMyNotifications(userId)); }; -// todo:revalidate profile get, location details +export const revalidateCityAndStatesAction = async () => { + revalidateTag(apiTags.getStates()); + revalidateTag(apiTags.getCities()); +}; + +export const revalidateUserProfileDetails = async (userId: string) => { + revalidateTag(apiTags.getMyProfileDetails(userId)); +}; diff --git a/src/app/[locale]/(landingPage)/layout.tsx b/src/app/[locale]/(landingPage)/layout.tsx index f95c283..ae18ddc 100644 --- a/src/app/[locale]/(landingPage)/layout.tsx +++ b/src/app/[locale]/(landingPage)/layout.tsx @@ -4,7 +4,6 @@ import React from "react"; import { displayFont } from "@/app/fonts"; import { LandingHeroSearch } from "@/components/LandingSections/LandingHeroSearch"; -// todo: move features and contact us to separate pages export default function Layout({ children }: { children: React.ReactNode }) { return (
diff --git a/src/components/ManageCache/ManageCache.tsx b/src/components/ManageCache/ManageCache.tsx index 16fbfa7..01d6845 100644 --- a/src/components/ManageCache/ManageCache.tsx +++ b/src/components/ManageCache/ManageCache.tsx @@ -1,8 +1,12 @@ -import { RevalidateFeaturedListings } from "./Sections/RevalidateFeaturedListings"; +import { RevalidateAllFeaturedListings } from "./Sections/RevalidateAllFeaturedListings"; +import { RevalidateAllPostedListings } from "./Sections/RevalidateAllPostedListings"; +import { RevalidateCitiesAndStates } from "./Sections/RevalidateCitiesAndStates"; +import { RevalidateFeaturedListingsByCountry } from "./Sections/RevalidateFeaturedListingsByCountry"; import { RevalidateFeatures } from "./Sections/RevalidateFeatures"; -import { RevalidatePostedListings } from "./Sections/RevalidatePostedListings"; +import { RevalidatePostedListingsByCountry } from "./Sections/RevalidatePostedListingsByCountry"; import { RevalidateRelatedListings } from "./Sections/RevalidateRelatedListings"; import { RevalidateUserNotifications } from "./Sections/RevalidateUserNotifications"; +import { RevalidateUserProfileDetails } from "./Sections/RevalidateUserProfileDetails"; import { RevalidateVehicleBrands } from "./Sections/RevalidateVehicleBrands"; export const ManageCache = () => { @@ -10,10 +14,14 @@ export const ManageCache = () => {
- - + + + + + +
); }; diff --git a/src/components/ManageCache/Sections/RevalidateFeaturedListings.tsx b/src/components/ManageCache/Sections/RevalidateAllFeaturedListings.tsx similarity index 75% rename from src/components/ManageCache/Sections/RevalidateFeaturedListings.tsx rename to src/components/ManageCache/Sections/RevalidateAllFeaturedListings.tsx index 94d528b..210b4df 100644 --- a/src/components/ManageCache/Sections/RevalidateFeaturedListings.tsx +++ b/src/components/ManageCache/Sections/RevalidateAllFeaturedListings.tsx @@ -2,10 +2,10 @@ import { useMutation } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; -import { revalidateFeaturedListingsAction } from "@/actions/cacheActions"; +import { revalidateAllFeaturedListingsAction } from "@/actions/cacheActions"; -export const RevalidateFeaturedListings = () => { - const { mutate, isLoading } = useMutation(async () => revalidateFeaturedListingsAction(), { +export const RevalidateAllFeaturedListings = () => { + const { mutate, isLoading } = useMutation(async () => revalidateAllFeaturedListingsAction(), { onSuccess: () => toast.success(`Successfully revalidated featured listings`), onError: () => toast.error(`Failed to revalidate featured listings`), }); diff --git a/src/components/ManageCache/Sections/RevalidateAllPostedListings.tsx b/src/components/ManageCache/Sections/RevalidateAllPostedListings.tsx new file mode 100644 index 0000000..32783cc --- /dev/null +++ b/src/components/ManageCache/Sections/RevalidateAllPostedListings.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { toast } from "react-hot-toast"; +import { revalidateAllPosedListingsAction } from "@/actions/cacheActions"; + +export const RevalidateAllPostedListings = () => { + const { mutate, isLoading } = useMutation(async () => revalidateAllPosedListingsAction(), { + onSuccess: () => toast.success(`Successfully revalidated all posted listings`), + onError: () => toast.error(`Failed to revalidate all posted listings`), + }); + + return ( + + ); +}; diff --git a/src/components/ManageCache/Sections/RevalidatePostedListings.tsx b/src/components/ManageCache/Sections/RevalidateCitiesAndStates.tsx similarity index 57% rename from src/components/ManageCache/Sections/RevalidatePostedListings.tsx rename to src/components/ManageCache/Sections/RevalidateCitiesAndStates.tsx index 96ba7c5..663dd7f 100644 --- a/src/components/ManageCache/Sections/RevalidatePostedListings.tsx +++ b/src/components/ManageCache/Sections/RevalidateCitiesAndStates.tsx @@ -2,17 +2,17 @@ import { useMutation } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; -import { revalidatePosedListingsAction } from "@/actions/cacheActions"; +import { revalidateCityAndStatesAction } from "@/actions/cacheActions"; -export const RevalidatePostedListings = () => { - const { mutate, isLoading } = useMutation(async () => revalidatePosedListingsAction(), { - onSuccess: () => toast.success(`Successfully revalidated posted listings`), - onError: () => toast.error(`Failed to revalidate posted listings`), +export const RevalidateCitiesAndStates = () => { + const { mutate, isLoading } = useMutation(async () => revalidateCityAndStatesAction(), { + onSuccess: () => toast.success(`Successfully revalidated cities and states`), + onError: () => toast.error(`Failed to revalidate cities and states`), }); return ( ); }; diff --git a/src/components/ManageCache/Sections/RevalidateFeaturedListingsByCountry.tsx b/src/components/ManageCache/Sections/RevalidateFeaturedListingsByCountry.tsx new file mode 100644 index 0000000..8741111 --- /dev/null +++ b/src/components/ManageCache/Sections/RevalidateFeaturedListingsByCountry.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; +import { z } from "zod"; +import { revalidateFeaturedListingsByCountryAction } from "@/actions/cacheActions"; +import { Modal, ModalFooter } from "@/components/Common"; +import { AutocompleteController } from "@/components/FormElements/AutoComplete"; +import { COUNTRIES } from "@/utils/countries"; +import { LabelValue } from "@/utils/types"; + +const RevalidateFeaturedListingsByCountrySchema = z.object({ country: z.string() }); + +type RevalidateFeaturedListingsByCountryReq = z.infer; + +export const RevalidateFeaturedListingsByCountry = () => { + const [modalVisible, setModalVisible] = useState(false); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(RevalidateFeaturedListingsByCountrySchema), + mode: "all", + }); + + const { mutate, isLoading } = useMutation( + (req: RevalidateFeaturedListingsByCountryReq) => { + const countryCode = Object.keys(COUNTRIES).find((item) => COUNTRIES[item]?.[0] === req.country); + return revalidateFeaturedListingsByCountryAction(countryCode!); + }, + { + onMutate: () => setModalVisible(false), + onSuccess: () => toast.success(`Successfully revalidated featured listings`), + onError: () => toast.error(`Failed to revalidate featured listings`), + }, + ); + + const countryList: LabelValue[] = Object.keys(COUNTRIES).map((key) => ({ + label: COUNTRIES[key]?.[0]!, + value: COUNTRIES[key]?.[0]!, + })); + + return ( + <> + + +
+ +
+ mutate(values))} + onVisibleChange={setModalVisible} + primaryButton={{ text: "Proceed" }} + /> + + + + ); +}; diff --git a/src/components/ManageCache/Sections/RevalidatePostedListingsByCountry.tsx b/src/components/ManageCache/Sections/RevalidatePostedListingsByCountry.tsx new file mode 100644 index 0000000..36ad3ff --- /dev/null +++ b/src/components/ManageCache/Sections/RevalidatePostedListingsByCountry.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; +import { z } from "zod"; +import { revalidatePosedListingsByCountryAction } from "@/actions/cacheActions"; +import { Modal, ModalFooter } from "@/components/Common"; +import { AutocompleteController } from "@/components/FormElements/AutoComplete"; +import { COUNTRIES } from "@/utils/countries"; +import { LabelValue } from "@/utils/types"; + +const RevalidatePostedListingsByCountrySchema = z.object({ country: z.string() }); + +type RevalidatePostedListingsByCountryReq = z.infer; + +export const RevalidatePostedListingsByCountry = () => { + const [modalVisible, setModalVisible] = useState(false); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(RevalidatePostedListingsByCountrySchema), + mode: "all", + }); + + const { mutate, isLoading } = useMutation( + (req: RevalidatePostedListingsByCountryReq) => { + const countryCode = Object.keys(COUNTRIES).find((item) => COUNTRIES[item]?.[0] === req.country); + return revalidatePosedListingsByCountryAction(countryCode!); + }, + { + onMutate: () => setModalVisible(false), + onSuccess: () => toast.success(`Successfully revalidated posted listings`), + onError: () => toast.error(`Failed to revalidate posted listings`), + }, + ); + + const countryList: LabelValue[] = Object.keys(COUNTRIES).map((key) => ({ + label: COUNTRIES[key]?.[0]!, + value: COUNTRIES[key]?.[0]!, + })); + + return ( + <> + + +
+ +
+ mutate(values))} + onVisibleChange={setModalVisible} + primaryButton={{ text: "Proceed" }} + /> + + + + ); +}; diff --git a/src/components/ManageCache/Sections/RevalidateUserProfileDetails.tsx b/src/components/ManageCache/Sections/RevalidateUserProfileDetails.tsx new file mode 100644 index 0000000..cdd5649 --- /dev/null +++ b/src/components/ManageCache/Sections/RevalidateUserProfileDetails.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; +import { z } from "zod"; +import { revalidateUserProfileDetails } from "@/actions/cacheActions"; +import { Modal, ModalFooter } from "@/components/Common"; +import { InputController } from "@/components/FormElements/Input"; + +const RevalidateUserProfileSchema = z.object({ userId: z.string() }); + +type RevalidateUserProfileSchemaReq = z.infer; + +export const RevalidateUserProfileDetails = () => { + const [modalVisible, setModalVisible] = useState(false); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(RevalidateUserProfileSchema), + mode: "all", + }); + + const { mutate, isLoading } = useMutation( + (req: RevalidateUserProfileSchemaReq) => { + return revalidateUserProfileDetails(req.userId); + }, + { + onMutate: () => setModalVisible(false), + onSuccess: () => toast.success(`Successfully revalidated user profile`), + onError: () => toast.error(`Failed to revalidate user profile`), + }, + ); + + return ( + <> + + +
+ + mutate(values))} + onVisibleChange={setModalVisible} + primaryButton={{ text: "Proceed" }} + /> + +
+ + ); +}; diff --git a/src/utils/api.ts b/src/utils/api.ts index 475fe0c..5368015 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -51,7 +51,7 @@ const extractBadRequestError = (errors: { [key: string]: string[] }): string | v } }; -const fetchRequest = async (endpoint: string, config: RequestInit, withAuth = false): Promise => { +const fetchRequest = async (endpoint: string, config: RequestInit, withAuth = false, firstAttempt = true): Promise => { let response: Response; if (withAuth) { const configWithAuth = await getConfigWithAuth(config); @@ -69,6 +69,9 @@ const fetchRequest = async (endpoint: string, config: RequestInit, wi } else { if (response.status === 401) { console.log("getting 401 for api call", endpoint); + if (firstAttempt) { + return fetchRequest(endpoint, config, withAuth, false); + } const currentPathname = nextHeaders().get("x-pathname"); redirect(`/unauthorized${currentPathname ? `?returnTo=${currentPathname}` : ``}`); } @@ -119,7 +122,6 @@ const fetchApi = { protectedDelete: (endpoint: string, body: TBody, config: RequestInit = {}) => fetchRequest(endpoint, { method: "DELETE", body, ...config }, true), }; -// todo: review all cache clearing logics(mainly ones specific to country) export const api = { getFeaturesList: () => @@ -130,7 +132,7 @@ export const api = { fetchApi.get("/v1/Vehicles/brands", { next: { tags: [apiTags.getVehicleBrands()], revalidate: revalidationTime.oneWeek } }), getPostedListings: (locale: string, req?: PaginatedRequest & PostedListingsFilterReq) => fetchApi.get(`/v1/Listings/posted/${locale}?${qs.stringify(req ?? {}, { skipEmptyString: true })}`, { - next: { revalidate: revalidationTime.thirtyMins, tags: [apiTags.getPostedListings()] }, + next: { revalidate: revalidationTime.thirtyMins, tags: [apiTags.getPostedListings(), apiTags.getPostedListingsByCountry(locale)] }, }), getPostedListingItem: (id: ListingIdType) => fetchApi.get(`/v1/Listings/posted/${id}`, { @@ -148,7 +150,10 @@ export const api = { }), getFeaturedListings: (countryCode: string) => fetchApi.get(`/v1/Listings/featured-listings/${countryCode}`, { - next: { tags: [apiTags.getFeaturedListings()], revalidate: revalidationTime.twelveHours }, + next: { + tags: [apiTags.getFeaturedListings(), apiTags.getFeatureListingsByCountry(countryCode)], + revalidate: revalidationTime.twelveHours, + }, }), getMyListings: (listingUserId: string, req?: PaginatedRequest & MyListingsFilterReq) => fetchApi.protectedGet(`/v1/Users/me/listings?${qs.stringify(req ?? {}, { skipEmptyString: true })}`, { @@ -224,7 +229,9 @@ export const apiTags = { getFeaturesList: () => "get-features-list", getVehicleBrands: () => "get-vehicle-brands", getFeaturedListings: () => "get-featured-listings", + getFeatureListingsByCountry: (countryCode: string) => `get-featured-listings-country-${countryCode}`, getPostedListings: () => "get-posted-listings", + getPostedListingsByCountry: (countryCode: string) => `get-posted-listings-country-${countryCode}`, getPostedListingItem: (id: ListingIdType) => `get-posted-listing-item-${id}`, getRelatedListings: (id: ListingIdType) => `get-related-listing-item-${id}`, getListings: () => `get-admin-listings`, diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index ab34f20..b221ec3 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -64,7 +64,6 @@ export const OptionalMilageSchema = z.object({ unit: z.string().min(1, "Milage unit is required"), }); -// todo: set from user's values as default export const LocationSchema = z.object({ city: z.string().min(1, "City is required"), state: z.string().min(1, "State is required"),