diff --git a/frontend/src/components/gdpr/gdprbanner.tsx b/frontend/src/components/gdpr/gdprbanner.tsx index 0790d01f..05736898 100644 --- a/frontend/src/components/gdpr/gdprbanner.tsx +++ b/frontend/src/components/gdpr/gdprbanner.tsx @@ -129,7 +129,7 @@ const GDPRBanner: React.FC = () => { Back - - - {/* Creator button */}
); }; diff --git a/frontend/src/components/listing/ListingRenderer.tsx b/frontend/src/components/listing/ListingRenderer.tsx index 46e1b561..c2d6aab2 100644 --- a/frontend/src/components/listing/ListingRenderer.tsx +++ b/frontend/src/components/listing/ListingRenderer.tsx @@ -54,7 +54,12 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => { {/* Right side - Header and details - full width on mobile, half width on desktop */}
{/* Header */} - +
@@ -91,6 +96,14 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
+ + +
+ {/* Build this robot */}
@@ -99,15 +112,6 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => { initialFeatured={isFeatured} />
- -
- - {/* Description */} -
diff --git a/frontend/src/components/listing/ListingVote.tsx b/frontend/src/components/listing/ListingVote.tsx new file mode 100644 index 00000000..571fe1c1 --- /dev/null +++ b/frontend/src/components/listing/ListingVote.tsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { FaArrowDown, FaArrowUp } from "react-icons/fa"; + +import { useAlertQueue } from "@/hooks/useAlertQueue"; +import { useAuthentication } from "@/hooks/useAuth"; + +interface Props { + listingId: string; + userVote: boolean | null; +} + +const ListingVote = ({ listingId, userVote: initialUserVote }: Props) => { + const auth = useAuthentication(); + const [voting, setVoting] = useState(false); + const [userVote, setUserVote] = useState(initialUserVote); + const { addErrorAlert, addAlert } = useAlertQueue(); + + const handleVote = async (vote: boolean) => { + if (!auth.isAuthenticated) { + addErrorAlert("You must be logged in to vote"); + return; + } + + setVoting(true); + try { + if (userVote === vote) { + const response = await auth.client.DELETE("/listings/{id}/vote", { + params: { + path: { + id: listingId, + }, + }, + }); + if (response.error) { + addErrorAlert(response.error); + } else { + addAlert("Vote removed", "success"); + setUserVote(null); + } + } else { + const response = await auth.client.POST("/listings/{id}/vote", { + params: { + path: { + id: listingId, + }, + query: { + upvote: vote, + }, + }, + }); + if (response.error) { + addErrorAlert(response.error); + } else { + addAlert("Vote added", "success"); + setUserVote(vote); + } + } + } catch (error) { + addErrorAlert(error); + } finally { + setVoting(false); + } + }; + + return ( +
+ + +
+ ); +}; + +export default ListingVote; diff --git a/frontend/src/components/nav/Navbar.tsx b/frontend/src/components/nav/Navbar.tsx index 31540600..703e2cd7 100644 --- a/frontend/src/components/nav/Navbar.tsx +++ b/frontend/src/components/nav/Navbar.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { FaExternalLinkAlt } from "react-icons/fa"; import { FaBars } from "react-icons/fa6"; import { Link, useLocation, useNavigate } from "react-router-dom"; @@ -75,8 +76,10 @@ const Navbar = () => { href={item.path} target="_blank" rel="noopener noreferrer" + className="flex items-center gap-2" > {item.name} + ) : ( diff --git a/frontend/src/components/nav/Sidebar.tsx b/frontend/src/components/nav/Sidebar.tsx index bbb9ba67..739b9262 100644 --- a/frontend/src/components/nav/Sidebar.tsx +++ b/frontend/src/components/nav/Sidebar.tsx @@ -1,4 +1,4 @@ -import { FaTimes } from "react-icons/fa"; +import { FaExternalLinkAlt, FaTimes } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; import Logo from "@/components/Logo"; @@ -11,6 +11,7 @@ interface SidebarItemProps { title: string; icon?: JSX.Element; onClick: () => void; + isExternal?: boolean; } interface SidebarProps { @@ -18,14 +19,22 @@ interface SidebarProps { onClose: () => void; } -const SidebarItem = ({ icon, title, onClick }: SidebarItemProps) => ( +const SidebarItem = ({ + icon, + title, + onClick, + isExternal, +}: SidebarItemProps) => (
  • ); @@ -72,7 +81,7 @@ const Sidebar = ({ show, onClose }: SidebarProps) => { title={listing.name} onClick={() => handleItemClick( - `/item/${listing.username}/${listing.slug || listing.id}`, + `/bot/${listing.username}/${listing.slug || listing.id}`, ) } /> @@ -86,7 +95,10 @@ const Sidebar = ({ show, onClose }: SidebarProps) => { key={item.name} title={item.name} icon={item.icon} - onClick={() => handleItemClick(item.path)} + onClick={() => + handleItemClick(item.path, item.isExternal) + } + isExternal={item.isExternal} /> ))}