From 1af471cc060e8268153295e9b83e423a29eb665f Mon Sep 17 00:00:00 2001 From: DavidVargas-Ctrl Date: Tue, 19 Aug 2025 10:22:04 -0500 Subject: [PATCH 01/11] Colaboradores del repositorio en el footer --- bun.lockb | Bin 319105 -> 319105 bytes package.json | 2 +- src/components/shared/footer.tsx | 87 ++++++++++---- src/components/shared/ui/cn.tsx | 7 ++ src/components/shared/ui/tooltip.tsx | 167 +++++++++++++++++++++++++++ src/services/github.service.ts | 52 +++++++++ 6 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 src/components/shared/ui/cn.tsx create mode 100644 src/components/shared/ui/tooltip.tsx create mode 100644 src/services/github.service.ts diff --git a/bun.lockb b/bun.lockb index 807422fe7fecdba100004b74b5f3d950e35b52b2..0a282b8ef5dc16f515a2c434b07673631607685d 100755 GIT binary patch delta 325 zcmZqNE8MtOc!Hin=CjEMe)3d@xg0IF|1?KRZdT~|ld%lVDKe{me>wE{@YaofZf=aB zlLh0oCpYMDOg`bpgWzrEa94fez_irH?{nglR1e`pcD}AA^SVB<%It1tyIN zi=U|vJtZr)cJXs;S$DNBbq}|T+NWb(_thty=&@>fllro7dagF3gu|EXI|5bz+$((M z_E2K;e(vbEPemO0ZV5Qcy?eIyk^)0aW$LLn@7!14tAF%qo=W}V1sXnS8OD_hW#wuG zes?F8@U|b-X54;Mn`vgp^rS6JM%yQBVQS%+F0q%XfU$0S>t3d8Q)W{=Vq*LuVRnr6?9NOO;`vjMYfe-+fh@8t3s9T5mddu}(%t z0aTNbIxCZaEC`dxMhpQ{laV?rlYlG;vj|5e4Hxo6Y?%HcK>^XMqDPJWV@O z)~^0NLaH}mvLbPBeww@1c1Wa8HTt3 diff --git a/package.json b/package.json index e2a46f6..d428730 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.53.0", "@tabler/icons-react": "^3.34.1", - "@tanstack/react-query": "^5.84.1", + "@tanstack/react-query": "^5.85.3", "@tsparticles/engine": "^3.8.1", "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.8.1", diff --git a/src/components/shared/footer.tsx b/src/components/shared/footer.tsx index 34f86ce..7344ebd 100644 --- a/src/components/shared/footer.tsx +++ b/src/components/shared/footer.tsx @@ -1,39 +1,86 @@ -'use client'; - +"use client"; +import { useEffect, useState } from "react"; +import AnimatedTooltip from "./ui/tooltip"; import { IconBrandInstagram, IconBrandLinkedin } from "@tabler/icons-react"; +import { getGitHubContributors, GitHubContributor } from "@/services/github.service"; export default function Footer() { + const [contributors, setContributors] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + + useEffect(() => { + const fetchContributors = async () => { + try { + setIsLoading(true); + + // Obtener contribuidores del repositorio + console.log('🚀 Cargando contribuidores del proyecto...'); + const contributorsData = await getGitHubContributors(); + setContributors(contributorsData); + } catch (error) { + console.error('Error loading contributors:', error); + } finally { + setIsLoading(false); + } + }; + + fetchContributors(); + }, []); + + // Convertir contribuidores al formato del tooltip + const contributorItems = contributors.map((contributor) => ({ + id: contributor.id, + name: contributor.login, + designation: "ACM Member", + image: contributor.avatar_url, + className: "border-gray-200 hover:border-blue-400", + })); + + // Fallback al logo ACM si no hay contribuidores o está cargando + const acmLogo = [ + { + id: 1, + name: "ACM Javeriana", + designation: "Capítulo Universitario", + image: "/Logo_Oscuro.svg", + imageDark: "/Logo_Claro.svg", + className: "border-transparent", + }, + ]; + return (
- {/* Título a la izquierda */} + {}
-

Capítulo Javeriano ACM

+

+ Capítulo Javeriano ACM +

- {/* Logo en el centro */} + {/* Contribuidores del proyecto en el centro */}
- Logo ACM Javeriana - Logo ACM Javeriana 0 ? contributorItems : acmLogo} />
- {/* Redes sociales a la derecha - apiladas verticalmente */} + {} @@ -41,4 +88,4 @@ export default function Footer() {
); -} \ No newline at end of file +} diff --git a/src/components/shared/ui/cn.tsx b/src/components/shared/ui/cn.tsx new file mode 100644 index 0000000..0a0f360 --- /dev/null +++ b/src/components/shared/ui/cn.tsx @@ -0,0 +1,7 @@ +import { type ClassValue } from "clsx"; +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/components/shared/ui/tooltip.tsx b/src/components/shared/ui/tooltip.tsx new file mode 100644 index 0000000..c217172 --- /dev/null +++ b/src/components/shared/ui/tooltip.tsx @@ -0,0 +1,167 @@ +"use client"; + +import React, { useRef, useState, useEffect } from "react"; +import { + motion, + useTransform, + AnimatePresence, + useMotionValue, + useSpring, +} from "motion/react"; +import { cn } from "./cn"; +import dynamic from 'next/dynamic'; + +/** Tipos */ +type TooltipItem = { + id: string | number; + name: string; + designation?: string; + image: string; + imageDark?: string; + className?: string; +}; + +type AnimatedTooltipProps = { + items: TooltipItem[]; + className?: string; + /** Posicion del tooltip: 'top' | 'bottom' | 'left' | 'right' */ + position?: 'top' | 'bottom' | 'left' | 'right'; +}; + +const springConfig = { stiffness: 100, damping: 15 }; + +const getTooltipPositionClasses = (position: 'top' | 'bottom' | 'left' | 'right') => { + switch (position) { + case 'top': + return 'absolute -top-12 left-1/2 -translate-x-1/2'; + case 'bottom': + return 'absolute -bottom-12 left-1/2 -translate-x-1/2'; + case 'left': + return 'absolute top-1/2 -left-12 -translate-y-1/2'; + case 'right': + return 'absolute top-1/2 -right-12 -translate-y-1/2'; + default: + return 'absolute -top-12 left-1/2 -translate-x-1/2'; + } +}; + +function AnimatedTooltipComponent({ + items, + className, + position = 'top', +}: AnimatedTooltipProps) { + const [hoveredId, setHoveredId] = useState(null); + const [isClient, setIsClient] = useState(false); + const x = useMotionValue(0); + const frameRef = useRef(null); + + const rotate = useSpring(useTransform(x, [-100, 100], [-45, 45]), springConfig); + const translateX = useSpring( + useTransform(x, [-100, 100], [-50, 50]), + springConfig + ); + + useEffect(() => { + setIsClient(true); + return () => { + if (frameRef.current) { + cancelAnimationFrame(frameRef.current); + } + }; + }, []); + + const handleMouseMove = (event: React.MouseEvent) => { + if (!isClient) return; + if (frameRef.current) cancelAnimationFrame(frameRef.current); + frameRef.current = requestAnimationFrame(() => { + const target = event.currentTarget; + if (!target) return; + + const rect = target.getBoundingClientRect(); + const localX = event.clientX - rect.left; + x.set(localX - rect.width / 2); + }); + }; + + return ( +
+ {items.map((item) => ( +
setHoveredId(item.id)} + onMouseLeave={() => setHoveredId(null)} + onMouseMove={handleMouseMove} + > + + {hoveredId === item.id && isClient && ( + +
+ {item.name} +
+ {item.designation && ( +
{item.designation}
+ )} + +
+
+ + )} + + +
+ {/* Claro */} + {item.name} + {/* Oscuro */} + {item.imageDark && ( + {item.name} + )} +
+
+ ))} +
+ ); +} + + +const AnimatedTooltip = dynamic(() => Promise.resolve(AnimatedTooltipComponent), { + ssr: false, + loading: () => ( +
+ ), +}); + +export default AnimatedTooltip; \ No newline at end of file diff --git a/src/services/github.service.ts b/src/services/github.service.ts new file mode 100644 index 0000000..268497e --- /dev/null +++ b/src/services/github.service.ts @@ -0,0 +1,52 @@ +// Servicio para obtener contribuidores del proyecto ACM-Web-Page-UI +export interface GitHubContributor { + id: number; + login: string; + avatar_url: string; + html_url: string; + type: string; +} + +export async function getGitHubContributors( + owner: string = "CapituloJaverianoACM", + repo: string = "ACM-Web-Page-UI", + limit: number = 6 +): Promise { + try { + console.log(`Obteniendo contribuidores de ${owner}/${repo}...`); + + const url = `https://api.github.com/repos/${owner}/${repo}/contributors?per_page=${limit}&anon=0`; + + const response = await fetch(url, { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "ACM-Web-Page-UI/Contributors (no-auth)", + }, + next: { revalidate: 300 }, + }); + + if (!response.ok) { + console.error(`Error GitHub API: ${response.status} ${response.statusText}`); + return []; + } + + const data = await response.json(); + + const contributors: GitHubContributor[] = data + .filter((c: any) => c && c.type === "User" && typeof c.login === "string" && !c.login.endsWith("[bot]")) + .slice(0, limit) + .map((c: any) => ({ + id: c.id, + login: c.login, + avatar_url: c.avatar_url, + html_url: c.html_url, + type: c.type, + })); + + console.log(`Contribuidores obtenidos: ${contributors.length}`); + return contributors; + } catch (error) { + console.error("Error obteniendo contribuidores:", error); + return []; + } +} From 58b4aa70e2e43a0904dfe36992b2830e75b37c33 Mon Sep 17 00:00:00 2001 From: DavidVargas-Ctrl Date: Tue, 19 Aug 2025 18:38:32 -0500 Subject: [PATCH 02/11] =?UTF-8?q?Colaboradores=20con=20API=20y=20cli=20agr?= =?UTF-8?q?egados,=20no=20hay=20duplicados,=20correci=C3=B3n=20de=20carpet?= =?UTF-8?q?as?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/shared/footer.tsx | 32 ++++++-- src/controllers/github.controller.ts | 106 +++++++++++++++++++++++++++ src/services/github.service.ts | 52 ------------- 3 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 src/controllers/github.controller.ts delete mode 100644 src/services/github.service.ts diff --git a/src/components/shared/footer.tsx b/src/components/shared/footer.tsx index 7344ebd..3279866 100644 --- a/src/components/shared/footer.tsx +++ b/src/components/shared/footer.tsx @@ -2,21 +2,32 @@ import { useEffect, useState } from "react"; import AnimatedTooltip from "./ui/tooltip"; import { IconBrandInstagram, IconBrandLinkedin } from "@tabler/icons-react"; -import { getGitHubContributors, GitHubContributor } from "@/services/github.service"; +import { getGitHubContributorsFromRepos, GitHubContributor } from "@/controllers/github.controller"; export default function Footer() { const [contributors, setContributors] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [isMounted, setIsMounted] = useState(false); - + // Prevenir hidratación mismatch useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + if (!isMounted) return; + const fetchContributors = async () => { try { setIsLoading(true); - // Obtener contribuidores del repositorio - console.log('🚀 Cargando contribuidores del proyecto...'); - const contributorsData = await getGitHubContributors(); + // Obtener contribuidores + console.log('Cargando contribuidores del proyecto...'); + const contributorsData = await getGitHubContributorsFromRepos( + undefined, + 6, + 10 + ); setContributors(contributorsData); } catch (error) { console.error('Error loading contributors:', error); @@ -26,7 +37,7 @@ export default function Footer() { }; fetchContributors(); - }, []); + }, [isMounted]); // Convertir contribuidores al formato del tooltip const contributorItems = contributors.map((contributor) => ({ @@ -37,7 +48,7 @@ export default function Footer() { className: "border-gray-200 hover:border-blue-400", })); - // Fallback al logo ACM si no hay contribuidores o está cargando + // Fallback al logo ACM si no hay contribuidores const acmLogo = [ { id: 1, @@ -49,6 +60,11 @@ export default function Footer() { }, ]; + // Prevenir hidratación mismatch + const displayItems = !isMounted || isLoading || contributorItems.length === 0 + ? acmLogo + : contributorItems; + return (