forked from izu/student-web-if-development-kit
feat(acaab): implementasi halaman prestasi & karya mahasiswa
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
import { useData } from '../context/DataContext';
|
||||
import { BookOpen, Award, Globe2 } from 'lucide-react';
|
||||
|
||||
/* ── Animated counter hook ─────────────────────────────────────────────── */
|
||||
function useCountUp(target: number, duration = 1600) {
|
||||
const [count, setCount] = useState(0);
|
||||
const elRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggered = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
triggered.current = false;
|
||||
let animationFrameId: number;
|
||||
let observer: IntersectionObserver;
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !triggered.current) {
|
||||
triggered.current = true;
|
||||
|
||||
let startTimestamp: number | null = null;
|
||||
|
||||
const step = (timestamp: number) => {
|
||||
if (!startTimestamp) startTimestamp = timestamp;
|
||||
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
||||
|
||||
// easeOutQuart easing function
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 4);
|
||||
|
||||
setCount(Math.round(target * easeProgress));
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameId = requestAnimationFrame(step);
|
||||
} else {
|
||||
setCount(target);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(step);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.4 },
|
||||
);
|
||||
|
||||
if (elRef.current) observer.observe(elRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [target, duration]);
|
||||
|
||||
return { count, elRef };
|
||||
}
|
||||
|
||||
/* ── Big stat ──────────────────────────────────────────────────────────── */
|
||||
function BigStat({ value, label }: { value: number; label: string }) {
|
||||
const { count, elRef } = useCountUp(value);
|
||||
return (
|
||||
<div ref={elRef} className="flex flex-col items-center justify-center gap-1 md:gap-1.5 px-2 h-full">
|
||||
<div className="text-[3.5rem] md:text-[4.5rem] lg:text-[5rem] font-black font-display text-primary-navy leading-none tabular-nums">
|
||||
{count}
|
||||
</div>
|
||||
<div className="text-[10px] md:text-[11px] lg:text-[13px] leading-none font-extrabold uppercase tracking-[0.1em] text-primary-navy text-center">
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Level row ──────────────────────────────────────────────────────────── */
|
||||
function LevelRow({ label, value, delay = 0 }: { label: string; value: number; delay?: number }) {
|
||||
const { count, elRef } = useCountUp(value, 1600 + delay);
|
||||
return (
|
||||
<div ref={elRef} className="flex items-center gap-2 md:gap-3 lg:gap-4">
|
||||
<span className="text-[10px] md:text-[11px] lg:text-[12px] leading-none font-extrabold uppercase tracking-[0.1em] text-primary-navy text-right">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-lg md:text-xl lg:text-2xl leading-none font-black font-display text-primary-navy tabular-nums text-right w-[1rem] md:w-[1.25rem] lg:w-[1.5rem]">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Dividers ───────────────────────────────────────────────────────────── */
|
||||
function VDivider() {
|
||||
return (
|
||||
<div className="hidden md:flex items-center justify-center self-stretch">
|
||||
<div className="w-px h-full bg-primary-navy/10 rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function HDivider() {
|
||||
return <div className="block md:hidden h-px w-2/3 mx-auto bg-primary-navy/10 my-2 rounded-full" />;
|
||||
}
|
||||
|
||||
/* ── Main export ────────────────────────────────────────────────────────── */
|
||||
export default function Stats() {
|
||||
const { t, lang } = useLang();
|
||||
const { achievements, projects } = useData();
|
||||
const totalPrestasi = achievements.length;
|
||||
const totalProyek = projects.length;
|
||||
const internasional = achievements.filter(a => a.level === 'Internasional').length;
|
||||
const nasional = achievements.filter(a => a.level === 'Nasional').length;
|
||||
const regional = achievements.filter(a => a.level === 'Regional').length;
|
||||
|
||||
const today = new Date().toLocaleDateString(
|
||||
lang === 'id' ? 'id-ID' : 'en-US',
|
||||
{ day: 'numeric', month: 'long', year: 'numeric' }
|
||||
);
|
||||
const dataNote = lang === 'id'
|
||||
? `Data Terkumpul Per ${today}`
|
||||
: `Data Collected As Of ${today}`;
|
||||
|
||||
return (
|
||||
<div id="tentang">
|
||||
{/* ── Stats bar ─────────────────────────────────────────────────── */}
|
||||
<section className="relative z-10 mb-2 rounded-xl overflow-hidden shadow-[0_10px_30px_-10px_rgba(253,184,19,0.4)]">
|
||||
<div className="bg-[#FDB813] relative">
|
||||
<div className="absolute inset-0 bg-white/5 mix-blend-overlay pointer-events-none" />
|
||||
<div className="relative grid grid-cols-1 stats-grid items-stretch px-4 md:px-6 lg:px-8 py-5 md:py-6 lg:py-7">
|
||||
|
||||
{/* ① Total Prestasi */}
|
||||
<div className="flex items-center justify-center">
|
||||
<BigStat value={totalPrestasi} label={t.stats.totalPrestasi} />
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
<HDivider />
|
||||
|
||||
{/* ② Total Proyek */}
|
||||
<div className="flex items-center justify-center">
|
||||
<BigStat value={totalProyek} label={t.stats.totalProyek} />
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
<HDivider />
|
||||
|
||||
{/* ③ Level breakdown */}
|
||||
<div className="flex justify-center items-center px-4 md:px-0">
|
||||
<div className="flex flex-col justify-center items-end gap-2 md:gap-3 lg:gap-3">
|
||||
<LevelRow label="Internasional" value={internasional} delay={0} />
|
||||
<LevelRow label="Nasional" value={nasional} delay={150} />
|
||||
<LevelRow label="Regional" value={regional} delay={300} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keterangan tanggal — di luar kotak, rata kanan */}
|
||||
<p className="text-[9px] md:text-[10px] font-semibold text-slate-400 tracking-wide mb-16 text-right pr-1">
|
||||
{dataNote}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Fact card helper ───────────────────────────────────────────────────── */
|
||||
function FactCard({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 bg-slate-50 rounded-lg px-4 py-3 border border-slate-100">
|
||||
<span className="text-primary-gold">{icon}</span>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wide text-slate-400">{label}</p>
|
||||
<p className="text-sm font-bold text-primary-navy">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user