-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Content manager & featured listings (#526)
* Content manager & featured listings * content manager role and feature post * Frontend lint * Updates * Linting * Unit test * Additional code updates
- Loading branch information
Showing
16 changed files
with
758 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import React, { createContext, useContext, useEffect, useState } from "react"; | ||
|
||
import { useAuthentication } from "@/hooks/useAuth"; | ||
|
||
type FeaturedListing = { | ||
id: string; | ||
username: string; | ||
slug: string | null; | ||
name: string; | ||
}; | ||
|
||
type FeaturedListingsContextType = { | ||
featuredListings: FeaturedListing[]; | ||
refreshFeaturedListings: () => Promise<void>; | ||
}; | ||
|
||
const FeaturedListingsContext = | ||
createContext<FeaturedListingsContextType | null>(null); | ||
|
||
export const FeaturedListingsProvider = ({ | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) => { | ||
const [featuredListings, setFeaturedListings] = useState<FeaturedListing[]>( | ||
[], | ||
); | ||
const auth = useAuthentication(); | ||
|
||
const refreshFeaturedListings = async () => { | ||
try { | ||
const { data: featuredData } = | ||
await auth.client.GET("/listings/featured"); | ||
|
||
if (!featuredData?.listing_ids?.length) { | ||
setFeaturedListings([]); | ||
return; | ||
} | ||
|
||
const { data: batchData } = await auth.client.GET("/listings/batch", { | ||
params: { | ||
query: { ids: featuredData.listing_ids }, | ||
}, | ||
}); | ||
|
||
if (batchData?.listings) { | ||
const orderedListings = featuredData.listing_ids | ||
.map((id) => batchData.listings.find((listing) => listing.id === id)) | ||
.filter( | ||
(listing): listing is NonNullable<typeof listing> => | ||
listing !== undefined, | ||
) | ||
.map((listing) => ({ | ||
id: listing.id, | ||
username: listing.username ?? "", | ||
slug: listing.slug, | ||
name: listing.name, | ||
})); | ||
|
||
setFeaturedListings(orderedListings); | ||
} | ||
} catch (error) { | ||
console.error("Error refreshing featured listings:", error); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
refreshFeaturedListings(); | ||
|
||
const handleFeaturedChange = () => { | ||
refreshFeaturedListings(); | ||
}; | ||
|
||
window.addEventListener("featuredListingsChanged", handleFeaturedChange); | ||
return () => { | ||
window.removeEventListener( | ||
"featuredListingsChanged", | ||
handleFeaturedChange, | ||
); | ||
}; | ||
}, []); | ||
|
||
return ( | ||
<FeaturedListingsContext.Provider | ||
value={{ featuredListings, refreshFeaturedListings }} | ||
> | ||
{children} | ||
</FeaturedListingsContext.Provider> | ||
); | ||
}; | ||
|
||
export const useFeaturedListings = () => { | ||
const context = useContext(FeaturedListingsContext); | ||
if (!context) { | ||
throw new Error( | ||
"useFeaturedListings must be used within a FeaturedListingsProvider", | ||
); | ||
} | ||
return context; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { useEffect, useState } from "react"; | ||
import { FaStar } from "react-icons/fa"; | ||
|
||
import { useFeaturedListings } from "@/components/listing/FeaturedListings"; | ||
import { Button } from "@/components/ui/button"; | ||
import { useAlertQueue } from "@/hooks/useAlertQueue"; | ||
import { useAuthentication } from "@/hooks/useAuth"; | ||
|
||
interface Props { | ||
listingId: string; | ||
initialFeatured: boolean; | ||
currentFeaturedCount?: number; | ||
} | ||
|
||
const ListingFeatureButton = (props: Props) => { | ||
const { listingId, initialFeatured, currentFeaturedCount = 0 } = props; | ||
const [isFeatured, setIsFeatured] = useState(initialFeatured); | ||
const [isUpdating, setIsUpdating] = useState(false); | ||
|
||
const { addAlert, addErrorAlert } = useAlertQueue(); | ||
const auth = useAuthentication(); | ||
const { refreshFeaturedListings } = useFeaturedListings(); | ||
|
||
const hasPermission = auth.currentUser?.permissions?.some( | ||
(permission) => | ||
permission === "is_content_manager" || permission === "is_admin", | ||
); | ||
|
||
if (!hasPermission) { | ||
return null; | ||
} | ||
|
||
useEffect(() => { | ||
setIsFeatured(initialFeatured); | ||
}, [initialFeatured]); | ||
|
||
const handleFeatureToggle = async (e: React.MouseEvent) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
|
||
setIsUpdating(true); | ||
|
||
try { | ||
const response = await auth.client.PUT( | ||
"/listings/featured/{listing_id}", | ||
{ | ||
params: { | ||
path: { listing_id: listingId }, | ||
query: { featured: !isFeatured }, | ||
}, | ||
}, | ||
); | ||
|
||
if (response.error) { | ||
addErrorAlert(response.error); | ||
} else { | ||
const newFeaturedState = !isFeatured; | ||
setIsFeatured(newFeaturedState); | ||
addAlert( | ||
`Listing ${newFeaturedState ? "featured" : "unfeatured"} successfully`, | ||
"success", | ||
); | ||
|
||
refreshFeaturedListings(); | ||
} | ||
} catch { | ||
addErrorAlert("Failed to update featured status"); | ||
} finally { | ||
setIsUpdating(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="flex items-center gap-2 mt-2"> | ||
<Button | ||
onClick={handleFeatureToggle} | ||
variant="primary" | ||
disabled={isUpdating} | ||
title={ | ||
currentFeaturedCount >= 3 && !isFeatured | ||
? "Maximum of 3 featured listings allowed. Unfeature another listing first." | ||
: isFeatured | ||
? "Remove from featured" | ||
: "Add to featured" | ||
} | ||
className="flex items-center" | ||
> | ||
<FaStar className="mr-2 h-4 w-4" /> | ||
<span className="mr-2"> | ||
{isUpdating ? "Updating..." : isFeatured ? "Unfeature" : "Feature"} | ||
</span> | ||
</Button> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ListingFeatureButton; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.