diff --git a/client/src/module/student/opensource/GSoCReposPage.tsx b/client/src/module/student/opensource/GSoCReposPage.tsx index 830362f0c..e9edb9e77 100644 --- a/client/src/module/student/opensource/GSoCReposPage.tsx +++ b/client/src/module/student/opensource/GSoCReposPage.tsx @@ -19,6 +19,7 @@ import { Lightbulb, BookOpen, ArrowUpRight, + Heart, } from "lucide-react"; import api from "../../../lib/axios"; import { queryKeys } from "../../../lib/query-keys"; @@ -28,6 +29,32 @@ import { SEO } from "../../../components/SEO"; import { canonicalUrl } from "../../../lib/seo.utils"; import type { GSoCOrganization, GSoCStats } from "../../../lib/types"; +const WISHLIST_KEY = "gsoc_wishlist"; + +function useWishlist() { + const [wishlist, setWishlist] = useState(() => { + try { + const parsed = JSON.parse(localStorage.getItem(WISHLIST_KEY) ?? "[]"); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + }); + + const toggle = (id: number) => { + setWishlist((prev) => { + const next = prev.includes(id) + ? prev.filter((x) => x !== id) + : [...prev, id]; + localStorage.setItem(WISHLIST_KEY, JSON.stringify(next)); + return next; + }); + }; + + const has = (id: number) => wishlist.includes(id); + return { wishlist, toggle, has }; +} + const cardBase = "group relative flex h-full w-full flex-col rounded-md border border-stone-200 bg-white p-5 text-left transition-colors hover:border-stone-400 dark:border-white/10 dark:bg-stone-900 dark:hover:border-white/30"; @@ -58,7 +85,13 @@ function OrgMark({ org }: { org: GSoCOrganization }) { ); } -function MetaChip({ icon, children }: { icon: ReactNode; children: ReactNode }) { +function MetaChip({ + icon, + children, +}: { + icon: ReactNode; + children: ReactNode; +}) { return ( {icon} @@ -67,7 +100,13 @@ function MetaChip({ icon, children }: { icon: ReactNode; children: ReactNode }) ); } -function PlainChip({ children, accent = false }: { children: ReactNode; accent?: boolean }) { +function PlainChip({ + children, + accent = false, +}: { + children: ReactNode; + accent?: boolean; +}) { return ( -

No organizations found

+

+ No organizations found +

try different search criteria

@@ -146,6 +187,17 @@ function FilterDropdown({ ); } +function GSoCOrgCard({ + org, + onClick, + wishlisted, + onWishlistToggle, +}: { + org: GSoCOrganization; + onClick: () => void; + wishlisted: boolean; + onWishlistToggle: (e: React.MouseEvent) => void; +}) { const ParticipationBar = ({ participatedYears }: { participatedYears: number[] }) => { // Show participation from 2016 to current year const currentYear = new Date().getFullYear(); @@ -175,7 +227,7 @@ function GSoCOrgCard({ org, onClick }: { org: GSoCOrganization; onClick: () => v const years = [...org.yearsParticipated].sort((a, b) => b - a); return ( -
@@ -220,7 +293,7 @@ function GSoCOrgCard({ org, onClick }: { org: GSoCOrganization; onClick: () => v
- + ); } @@ -230,13 +303,23 @@ interface GSoCOrgModalProps { githubRepos: { title: string; url: string }[]; gsocPageUrl: string | null; reposLoading: boolean; + wishlisted: boolean; + onWishlistToggle: () => void; } - -function GSoCOrgModal({ org, onClose, githubRepos, gsocPageUrl, reposLoading }: GSoCOrgModalProps) { +function GSoCOrgModal({ + org, + onClose, + githubRepos, + gsocPageUrl, + reposLoading, + wishlisted, + onWishlistToggle, +}: GSoCOrgModalProps) { const [selectedYear, setSelectedYear] = useState(null); const years = [...org.yearsParticipated].sort((a, b) => b - a); const activeYear = selectedYear || (years[0] ? String(years[0]) : null); - const yearData = activeYear && org.projectsData ? org.projectsData[activeYear] : null; + const yearData = + activeYear && org.projectsData ? org.projectsData[activeYear] : null; return ( + ); })} @@ -352,7 +455,10 @@ function GSoCOrgModal({ org, onClose, githubRepos, gsocPageUrl, reposLoading }: {yearData?.projects && yearData.projects.length > 0 && (
{yearData.projects.map((project, index) => ( -
+
@@ -389,25 +495,43 @@ function GSoCOrgModal({ org, onClose, githubRepos, gsocPageUrl, reposLoading }:
{org.contactEmail && ( - + Contact )} {org.mailingList && ( - + Mailing List )} {org.ideasUrl && ( - + Project Ideas )} {org.guideUrl && ( - + Contributor Guide @@ -494,6 +618,12 @@ export default function GSoCReposPage() { const [page, setPage] = useState(1); const [selectedOrg, setSelectedOrg] = useState(null); + const [timer, setTimer] = useState | null>( + null, + ); + const { wishlist, toggle, has } = useWishlist(); + const [showWishlist, setShowWishlist] = useState(false); + // ---> CHANGED TO useRef TO FIX STALE CLOSURES <--- const timerRef = useRef | null>(null); @@ -555,32 +685,52 @@ export default function GSoCReposPage() { const { data, isLoading } = useQuery({ queryKey: queryKeys.gsoc.list(params), - queryFn: () => api.get("/gsoc/organizations", { params }).then((res) => res.data), + queryFn: () => + api.get("/gsoc/organizations", { params }).then((res) => res.data), }); const organizations: GSoCOrganization[] = data?.organizations ?? []; + const filteredOrganizations = showWishlist + ? organizations.filter((org) => has(org.id)) + : organizations; const pagination = data?.pagination ?? { page: 1, total: 0, totalPages: 1 }; const { data: detailData } = useQuery({ queryKey: queryKeys.gsoc.detail(selectedOrg?.slug ?? ""), - queryFn: () => api.get(`/gsoc/organizations/${selectedOrg!.slug}`).then((res) => res.data), + queryFn: () => + api + .get(`/gsoc/organizations/${selectedOrg!.slug}`) + .then((res) => res.data), enabled: !!selectedOrg, }); - const detailOrg: GSoCOrganization | null = detailData?.organization ?? selectedOrg; + const detailOrg: GSoCOrganization | null = + detailData?.organization ?? selectedOrg; const { data: reposData, isLoading: reposLoading } = useQuery({ queryKey: queryKeys.gsoc.repos(selectedOrg?.slug ?? ""), - queryFn: () => api.get(`/gsoc/organizations/${selectedOrg!.slug}/repos`).then((res) => res.data), + queryFn: () => + api + .get(`/gsoc/organizations/${selectedOrg!.slug}/repos`) + .then((res) => res.data), enabled: !!selectedOrg, staleTime: 1000 * 60 * 60, }); const githubRepos: { title: string; url: string }[] = reposData?.githubRepos ?? []; const gsocPageUrl: string | null = reposData?.gsocPageUrl ?? null; - const categoryOptions = ["All", ...(stats?.categories.map((category) => category.name) ?? [])]; - const yearOptions = ["All", ...(stats?.years.map((year) => String(year.year)) ?? [])]; - const techOptions = ["All", ...(stats?.technologies.slice(0, 30).map((tech) => tech.name) ?? [])]; + const categoryOptions = [ + "All", + ...(stats?.categories.map((category) => category.name) ?? []), + ]; + const yearOptions = [ + "All", + ...(stats?.years.map((year) => String(year.year)) ?? []), + ]; + const techOptions = [ + "All", + ...(stats?.technologies.slice(0, 30).map((tech) => tech.name) ?? []), + ]; const hasFilters = Boolean(initialQ) || selectedCategory !== "All" || selectedTech !== "All" || selectedYear !== "All"; @@ -613,16 +763,23 @@ export default function GSoCReposPage() {

- Search accepted organizations, study their past projects, and find a community where your stack fits. + Search accepted organizations, study their past projects, and + find a community where your stack fits.

- {stats?.total ?? "-"} orgs + + {stats?.total ?? "-"} + {" "} + orgs - {stats?.categories.length ?? "-"} categories + + {stats?.categories.length ?? "-"} + {" "} + categories @@ -674,17 +831,43 @@ export default function GSoCReposPage() { clear )} +

- {pagination.total} organizations + + {pagination.total} + {" "} + organizations {hasFilters && ( <> - {" "} / filtered + {" "} + /{" "} + + filtered + + + )} + {pagination.totalPages > 1 && ( + <> + {" "} + / page {pagination.page} of {pagination.totalPages} )} - {pagination.totalPages > 1 && <> / page {pagination.page} of {pagination.totalPages}}

@@ -693,9 +876,12 @@ export default function GSoCReposPage() { ) : organizations.length === 0 ? ( ) : ( - + - {organizations.map((org, index) => ( + {filteredOrganizations.map((org, index) => ( - setSelectedOrg(org)} /> + setSelectedOrg(org)} + wishlisted={has(org.id)} + onWishlistToggle={(e) => { + e.stopPropagation(); + toggle(org.id); + }} + /> ))} @@ -717,7 +911,6 @@ export default function GSoCReposPage() { onPageChange={setPage} showingInfo={{ total: pagination.total, limit }} /> -
@@ -728,6 +921,8 @@ export default function GSoCReposPage() { githubRepos={githubRepos} gsocPageUrl={gsocPageUrl} reposLoading={reposLoading} + wishlisted={has(detailOrg.id)} + onWishlistToggle={() => toggle(detailOrg.id)} /> )}