forked from izu/student-web-if-development-kit
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afe7dcce3a | |||
| c1b7f8cb23 | |||
| c5a476c6bd | |||
| 6a4fa612e9 | |||
| fd477e4c22 | |||
| b5fba9238e |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,20 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ACAAB — Dev Preview</title>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ACAAB — Dev Preview</title>
|
||||
<style>
|
||||
#dev-banner {
|
||||
background: #feb401; color: #003150; font-size: 11px; font-weight: 700;
|
||||
text-align: center; padding: 6px; letter-spacing: .05em; position: sticky; top: 0; z-index: 999;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
<script type="module" crossorigin src="../acaab.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="../acaab.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="dev-banner">⚡ DEV MODE — Navbar & footer tidak tampil di sini. Gunakan kit dev server (root) untuk preview lengkap.</div>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="dev-banner">⚡ DEV MODE — Navbar & footer tidak tampil di sini. Gunakan kit dev server (root) untuk preview lengkap.</div>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -1148,9 +1148,9 @@
|
||||
</div>
|
||||
<div id="assignment-body" class="prose prose-slate max-w-none">
|
||||
<!-- ================= START: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
<link rel="stylesheet" href="./assets/acaab.css">
|
||||
<link rel="stylesheet" href="./assets/acaab.css?v=8">
|
||||
<div id="root" class="not-prose"></div>
|
||||
<script type="module" src="./assets/acaab.js"></script>
|
||||
<script type="module" src="./assets/acaab.js?v=8"></script>
|
||||
<!-- ================= END: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function App() {
|
||||
<LanguageProvider>
|
||||
<DataProvider>
|
||||
{/* Breadcrumb */}
|
||||
<nav className="py-4" aria-label="Breadcrumb">
|
||||
<nav className="max-w-7xl mx-auto px-4 lg:px-8 py-4" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center text-[10px] md:text-xs text-slate-500 font-medium tracking-wide uppercase">
|
||||
<li>
|
||||
<a href="https://informatika.untan.ac.id" target="_blank" rel="noopener noreferrer" className="hover:text-untan-blue transition-colors">
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function AchievementCard({ achievement, onReadMore }: Achievement
|
||||
<article className="bg-white rounded-lg overflow-hidden group hover:-translate-y-1 transition-all duration-300 shadow-md hover:shadow-xl border border-slate-200 flex flex-col h-full relative">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-primary-navy z-10 group-hover:bg-primary-gold transition-colors"></div>
|
||||
|
||||
<div className="relative h-56 overflow-hidden bg-slate-100">
|
||||
<div className="relative h-44 overflow-hidden bg-slate-100">
|
||||
<img
|
||||
src={achievement.image}
|
||||
alt={achievement.title}
|
||||
@@ -24,50 +24,50 @@ export default function AchievementCard({ achievement, onReadMore }: Achievement
|
||||
(e.target as HTMLImageElement).src = 'https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-3 left-3 bg-white/95 backdrop-blur text-primary-navy font-bold px-3 py-1 rounded shadow-sm flex items-center gap-1.5 text-xs">
|
||||
<Trophy size={14} className="text-primary-gold" />
|
||||
<div className="absolute top-2.5 left-2.5 bg-white/95 backdrop-blur text-primary-navy font-bold px-2 py-0.5 rounded shadow-sm flex items-center gap-1 text-[10px]">
|
||||
<Trophy size={12} className="text-primary-gold" />
|
||||
{achievement.rank}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-grow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-untan-blue text-xs font-bold uppercase tracking-wider">
|
||||
<div className="p-4 sm:p-5 flex flex-col flex-grow">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-untan-blue text-[10px] font-bold uppercase tracking-wider">
|
||||
{achievement.category}
|
||||
</span>
|
||||
<span className="text-slate-400 text-xs font-medium flex items-center gap-1 border-l border-slate-300 pl-3">
|
||||
<Calendar size={12} />
|
||||
<span className="text-slate-400 text-[10px] font-medium flex items-center gap-1 border-l border-slate-300 pl-2">
|
||||
<Calendar size={10} />
|
||||
{achievement.year}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold font-display text-primary-navy mb-3 leading-snug group-hover:text-untan-blue transition-colors">
|
||||
<h3 className="text-lg font-bold font-display text-primary-navy mb-2 leading-snug group-hover:text-untan-blue transition-colors">
|
||||
{achievement.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-slate-600 text-sm leading-relaxed mb-6 line-clamp-3">
|
||||
<p className="text-slate-600 text-xs leading-relaxed mb-4 line-clamp-3">
|
||||
{achievement.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto border-t border-slate-100 pt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded bg-primary-navy/5 flex items-center justify-center text-primary-navy font-bold text-xs ring-1 ring-primary-navy/10">
|
||||
<div className="mt-auto border-t border-slate-100 pt-3">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded bg-primary-navy/5 flex items-center justify-center text-primary-navy font-bold text-[10px] ring-1 ring-primary-navy/10">
|
||||
{achievement.avatar}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-slate-800 block text-sm">{achievement.team}</span>
|
||||
<span className="text-slate-500 block text-xs">{achievement.generation}</span>
|
||||
<span className="font-bold text-slate-800 block text-xs leading-none mb-0.5">{achievement.team}</span>
|
||||
<span className="text-slate-500 block text-[10px] leading-none">{achievement.generation}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="bg-slate-100 text-slate-600 text-[10px] px-2 py-1 rounded font-bold uppercase tracking-wide">
|
||||
<span className="bg-slate-100 text-slate-600 text-[9px] px-1.5 py-0.5 rounded font-bold uppercase tracking-wide">
|
||||
{achievement.level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onReadMore(achievement)}
|
||||
className="w-full py-2.5 rounded bg-primary-navy/5 text-primary-navy font-bold hover:bg-primary-navy hover:text-white transition-all duration-300 text-sm min-h-[44px]"
|
||||
className="w-full py-2 rounded bg-primary-navy/5 text-primary-navy font-bold hover:bg-primary-navy/10 hover:text-untan-blue transition-all duration-300 text-xs min-h-[38px] flex items-center justify-center gap-1.5"
|
||||
>
|
||||
{t.achievement.readMore}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { X, Trophy, Calendar, Award, Users, BookOpen, Tag } from 'lucide-react';
|
||||
import type { Achievement } from '../data';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
@@ -22,6 +22,18 @@ function InfoRow({ icon, label, value }: { icon: ReactNode; label: string; value
|
||||
|
||||
export default function AchievementModal({ achievement, onClose }: AchievementModalProps) {
|
||||
const { t } = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (achievement) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [achievement]);
|
||||
|
||||
if (!achievement) return null;
|
||||
|
||||
return (
|
||||
@@ -33,11 +45,11 @@ export default function AchievementModal({ achievement, onClose }: AchievementMo
|
||||
aria-label={achievement.title}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-fade-in"
|
||||
className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-fade-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Image header */}
|
||||
<div className="relative h-56 overflow-hidden rounded-t-xl bg-slate-200 shrink-0">
|
||||
<div className="relative h-56 md:h-64 overflow-hidden rounded-t-xl bg-slate-200 shrink-0">
|
||||
<img
|
||||
src={achievement.image}
|
||||
alt={achievement.title}
|
||||
@@ -46,61 +58,60 @@ export default function AchievementModal({ achievement, onClose }: AchievementMo
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
{/* Close button */}
|
||||
{/* Close button — dark background so icon always visible on any image */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label={t.modal.close}
|
||||
className="absolute top-3 right-3 w-9 h-9 bg-white/90 backdrop-blur rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-md"
|
||||
className="absolute top-3 right-3 z-50 w-8 h-8 bg-slate-900/50 hover:bg-slate-900/80 backdrop-blur rounded-full flex items-center justify-center transition-colors shadow-md"
|
||||
>
|
||||
<X size={18} className="text-slate-700" />
|
||||
<X size={16} className="text-white" />
|
||||
</button>
|
||||
|
||||
{/* Rank badge */}
|
||||
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||
<span className="bg-primary-gold text-slate-900 text-xs font-bold px-3 py-1.5 rounded flex items-center gap-1.5">
|
||||
<Trophy size={12} />
|
||||
{/* Rank Badge only */}
|
||||
<div className="absolute bottom-3 left-4">
|
||||
<span className="bg-primary-gold text-black text-[10px] font-bold px-2.5 py-1 rounded shadow-sm flex items-center gap-1">
|
||||
{achievement.rank}
|
||||
</span>
|
||||
<span className="bg-white/20 backdrop-blur text-white text-xs font-bold px-3 py-1.5 rounded uppercase tracking-wide">
|
||||
{achievement.level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<div className="mb-1">
|
||||
<span className="text-primary-untan-blue text-xs font-bold uppercase tracking-wider">
|
||||
{achievement.category}
|
||||
</span>
|
||||
<h2 className="text-lg font-bold text-slate-900 leading-snug mb-1">{achievement.title}</h2>
|
||||
|
||||
<div className="text-xs text-slate-400 mb-4 font-medium">
|
||||
{achievement.year} • {achievement.category} • {achievement.level}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-primary-navy leading-snug mb-5">{achievement.title}</h2>
|
||||
|
||||
<p className="text-slate-600 text-xs leading-relaxed mb-5">
|
||||
{achievement.description.replace(/\*\*/g, '')}
|
||||
</p>
|
||||
|
||||
{/* Info grid */}
|
||||
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg mb-5">
|
||||
<InfoRow icon={<Users size={14} />} label={t.modal.tim} value={achievement.team} />
|
||||
{achievement.nim && (
|
||||
<InfoRow icon={<BookOpen size={14} />} label={t.modal.nim} value={achievement.nim} />
|
||||
)}
|
||||
<InfoRow icon={<Calendar size={14} />} label={t.modal.angkatan} value={achievement.generation} />
|
||||
<InfoRow icon={<Calendar size={14} />} label={t.modal.tahun} value={String(achievement.year)} />
|
||||
<InfoRow icon={<Award size={14} />} label={t.modal.kompetisi} value={achievement.competition} />
|
||||
<InfoRow icon={<Tag size={14} />} label={t.modal.bidang} value={achievement.category} />
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 text-xs">
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">Tim / Peserta</p>
|
||||
<p className="font-bold text-slate-900">{achievement.team}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">Angkatan</p>
|
||||
<p className="font-bold text-slate-900">{achievement.generation}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="border-t border-slate-100 pt-4">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">{t.modal.deskripsi}</p>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{achievement.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-6 w-full py-3 bg-primary-navy text-white font-bold rounded-lg hover:bg-untan-blue transition-colors duration-200 text-sm"
|
||||
>
|
||||
{t.modal.close}
|
||||
</button>
|
||||
{/* Members list */}
|
||||
{achievement.members && achievement.members.length > 0 && (
|
||||
<div className="border-t border-slate-100 pt-3">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">Anggota Tim</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{achievement.members.map((m, i) => (
|
||||
<span key={i} className="bg-slate-100 text-slate-600 text-[10px] px-2.5 py-1 rounded">
|
||||
{m.nama}{m.angkatan ? ` (${m.angkatan})` : ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function Hero() {
|
||||
return (
|
||||
<div
|
||||
id="hero"
|
||||
className="acaab-full-bleed bg-untan-navy text-white pt-6 md:pt-8 lg:pt-10 mb-8 border-t border-white/10 relative"
|
||||
className="acaab-full-bleed bg-untan-navy text-white pt-6 md:pt-8 lg:pt-10 mb-8 border-t border-white/10 relative overflow-hidden"
|
||||
>
|
||||
<div className="container mx-auto px-4 lg:px-8 relative z-10">
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function ProjectCarousel() {
|
||||
const [filterYear, setFilterYear] = useState('');
|
||||
|
||||
const categories = useMemo(
|
||||
() => [...new Set(projects.map((p) => p.category))].sort(),
|
||||
() => [...new Set(projects.flatMap((p) => p.category))].sort(),
|
||||
[projects]
|
||||
);
|
||||
|
||||
@@ -40,9 +40,9 @@ export default function ProjectCarousel() {
|
||||
!q ||
|
||||
p.title.toLowerCase().includes(q) ||
|
||||
p.studentName.toLowerCase().includes(q) ||
|
||||
p.category.toLowerCase().includes(q) ||
|
||||
p.category.join(' ').toLowerCase().includes(q) ||
|
||||
p.description.toLowerCase().includes(q);
|
||||
const matchCategory = !filterCategory || p.category === filterCategory;
|
||||
const matchCategory = !filterCategory || p.category.includes(filterCategory);
|
||||
const matchYear = !filterYear || String(p.year) === filterYear;
|
||||
return matchSearch && matchCategory && matchYear;
|
||||
});
|
||||
@@ -267,16 +267,45 @@ export default function ProjectCarousel() {
|
||||
(e.target as HTMLImageElement).src = 'https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-primary-navy/80 to-transparent p-5 flex flex-col justify-end">
|
||||
<h4 className="text-lg font-bold font-display text-white leading-snug mb-1">
|
||||
{project.title}
|
||||
</h4>
|
||||
{project.category && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-primary-gold uppercase tracking-wider">
|
||||
<Tag size={10} />
|
||||
{project.category}
|
||||
|
||||
{/* Tag at top-left, raised like achievements, max 2 tags then (..) */}
|
||||
{project.category && project.category.length > 0 && (
|
||||
<div className="absolute top-3 left-3 flex flex-wrap gap-1.5 z-20">
|
||||
{project.category.slice(0, 2).map((cat, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="bg-white/95 backdrop-blur text-primary-navy font-bold px-2.5 py-1 rounded shadow-sm flex items-center gap-1 text-[10px] uppercase tracking-wider"
|
||||
>
|
||||
<Tag size={10} className="text-primary-gold" />
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
{project.category.length > 2 && (
|
||||
<span className="bg-white/95 backdrop-blur text-primary-navy font-bold px-2 py-1 rounded shadow-sm flex items-center gap-1 text-[10px]">
|
||||
(..)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title block — inline background follows text width */}
|
||||
<div className="absolute bottom-3 left-3 z-20 max-w-[85%]">
|
||||
<h4
|
||||
className="text-sm font-bold font-display text-slate-900 leading-snug"
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
<span
|
||||
className="bg-white/55 backdrop-blur-sm px-2 py-0.5 rounded"
|
||||
style={{
|
||||
boxDecorationBreak: 'clone',
|
||||
WebkitBoxDecorationBreak: 'clone',
|
||||
lineHeight: '1.9',
|
||||
textShadow: '0 0 8px rgba(255,255,255,0.8)',
|
||||
}}
|
||||
>
|
||||
{project.title}
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { X, ExternalLink, Users, BookOpen, Calendar, Tag, FileText } from 'lucide-react';
|
||||
import type { Project } from '../data';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
@@ -22,6 +22,18 @@ function InfoRow({ icon, label, value }: { icon: ReactNode; label: string; value
|
||||
|
||||
export default function ProjectModal({ project, onClose }: ProjectModalProps) {
|
||||
const { t } = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (project) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [project]);
|
||||
|
||||
if (!project) return null;
|
||||
|
||||
return (
|
||||
@@ -33,11 +45,11 @@ export default function ProjectModal({ project, onClose }: ProjectModalProps) {
|
||||
aria-label={project.title}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-fade-in"
|
||||
className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-fade-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Image header */}
|
||||
<div className="relative h-56 overflow-hidden rounded-t-xl bg-slate-200 shrink-0">
|
||||
<div className="relative h-56 md:h-64 overflow-hidden rounded-t-xl bg-slate-200 shrink-0">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
@@ -46,76 +58,88 @@ export default function ProjectModal({ project, onClose }: ProjectModalProps) {
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
{/* Close button */}
|
||||
{/* Close button — dark bg so icon always visible on any image */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label={t.modal.close}
|
||||
className="absolute top-3 right-3 w-9 h-9 bg-white/90 backdrop-blur rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-md"
|
||||
className="absolute top-3 right-3 z-50 w-8 h-8 bg-slate-900/50 hover:bg-slate-900/80 backdrop-blur rounded-full flex items-center justify-center transition-colors shadow-md"
|
||||
>
|
||||
<X size={18} className="text-slate-700" />
|
||||
<X size={16} className="text-white" />
|
||||
</button>
|
||||
|
||||
{/* Category badge */}
|
||||
{project.category && (
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<span className="bg-primary-gold text-slate-900 text-xs font-bold px-3 py-1.5 rounded flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<Tag size={12} />
|
||||
{project.category}
|
||||
</span>
|
||||
{project.category && project.category.length > 0 && (
|
||||
<div className="absolute bottom-3 left-4 flex flex-wrap gap-1.5">
|
||||
{project.category.map((cat, i) => (
|
||||
<span key={i} className="bg-primary-gold text-slate-900 text-[10px] font-bold px-2.5 py-1 rounded shadow-sm flex items-center gap-1 uppercase tracking-wide">
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-bold text-primary-navy leading-snug mb-5">{project.title}</h2>
|
||||
<h2 className="text-lg font-bold text-slate-900 leading-snug mb-1">{project.title}</h2>
|
||||
|
||||
{project.category && project.category.length > 0 && (
|
||||
<div className="text-xs text-slate-400 mb-4 font-medium">
|
||||
{project.year} • {project.category.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-slate-600 text-xs leading-relaxed mb-5">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* Info grid */}
|
||||
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg mb-5">
|
||||
{project.studentName && (
|
||||
<InfoRow icon={<Users size={14} />} label={t.modal.mahasiswa} value={project.studentName} />
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 text-xs">
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">Mahasiswa</p>
|
||||
<p className="font-bold text-slate-900">{project.studentName}</p>
|
||||
</div>
|
||||
{project.nim && (
|
||||
<InfoRow icon={<BookOpen size={14} />} label={t.modal.nim} value={project.nim} />
|
||||
)}
|
||||
{project.year && (
|
||||
<InfoRow icon={<Calendar size={14} />} label={t.modal.tahun} value={String(project.year)} />
|
||||
)}
|
||||
{project.category && (
|
||||
<InfoRow icon={<Tag size={14} />} label={t.modal.bidang} value={project.category} />
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">NIM</p>
|
||||
<p className="font-bold text-slate-900">{project.nim}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="border-t border-slate-100 pt-4 mb-5">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">{t.modal.deskripsi}</p>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{project.description}</p>
|
||||
</div>
|
||||
{/* Members list */}
|
||||
{project.members && project.members.length > 0 && (
|
||||
<div className="border-t border-slate-100 pt-3 mb-4">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">Anggota Tim</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.members.map((m, i) => (
|
||||
<span key={i} className="bg-slate-100 text-slate-600 text-[10px] px-2.5 py-1 rounded">
|
||||
{m.nama}{m.nim ? ` (${m.nim})` : ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{project.catatan && (
|
||||
<div className="mb-4 p-2.5 bg-blue-50/50 border border-blue-100 rounded-lg">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-blue-400 mb-1">Catatan Tambahan</p>
|
||||
<p className="text-xs text-slate-600 italic">{project.catatan}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project link */}
|
||||
{project.link ? (
|
||||
{project.link && (
|
||||
<a
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-primary-navy text-white font-bold rounded-lg hover:bg-untan-blue transition-colors duration-200 text-sm mb-3"
|
||||
className="flex items-center justify-center gap-2 w-full py-2.5 bg-[#003150] text-white font-bold rounded-lg hover:bg-[#1e40af] transition-colors duration-200 text-sm"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
<ExternalLink size={14} />
|
||||
{t.project.visitLink}
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 w-full py-3 bg-slate-100 text-slate-400 font-bold rounded-lg text-sm mb-3 cursor-not-allowed">
|
||||
<FileText size={16} />
|
||||
{t.project.noLink}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-2.5 border-2 border-slate-200 text-slate-600 font-bold rounded-lg hover:border-slate-300 hover:bg-slate-50 transition-colors duration-200 text-sm"
|
||||
>
|
||||
{t.modal.close}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,47 +55,55 @@ function useCountUp(target: number, duration = 1600) {
|
||||
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 }) {
|
||||
/* ── Stat number with label ─────────────────────────────────────────────── */
|
||||
function StatItem({ value, label, delay = 0 }: { value: number; label: string; 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]">
|
||||
<div ref={elRef} className="flex flex-col items-center gap-1" style={{ minWidth: 'clamp(5rem, 10vw, 8rem)' }}>
|
||||
<div
|
||||
className="font-display font-black text-primary-navy leading-none tabular-nums"
|
||||
style={{ fontSize: 'clamp(2.8rem, 6vw, 5rem)' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="font-display font-bold text-primary-navy text-center leading-tight uppercase tracking-wide"
|
||||
style={{ fontSize: 'clamp(0.6rem, 1.1vw, 0.8rem)' }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Dividers ───────────────────────────────────────────────────────────── */
|
||||
function VDivider() {
|
||||
/* ── Section panel ──────────────────────────────────────────────────────── */
|
||||
function StatPanel({
|
||||
title,
|
||||
children,
|
||||
flexClass = 'flex-1',
|
||||
cols = 'grid-cols-2 sm:grid-cols-4',
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
flexClass?: string;
|
||||
cols?: string;
|
||||
}) {
|
||||
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 className={`flex flex-col gap-5 ${flexClass}`}>
|
||||
{/* Dark pill header — full width of this panel */}
|
||||
<div
|
||||
className="bg-primary-navy text-white font-display font-bold text-center rounded-xl px-8 py-3 w-full uppercase tracking-wide"
|
||||
style={{ fontSize: 'clamp(0.85rem, 1.5vw, 1.05rem)' }}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{/* Numbers row — responsive grid */}
|
||||
<div className={`grid ${cols} items-end justify-items-center gap-y-6 gap-x-2 pb-2`}>
|
||||
{children}
|
||||
</div>
|
||||
</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() {
|
||||
@@ -119,34 +127,30 @@ export default function Stats() {
|
||||
<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="bg-[#FDB813] relative px-6 md:px-10 py-7 md:py-9">
|
||||
<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>
|
||||
<div className="relative flex flex-col md:flex-row md:items-start gap-8 md:gap-6">
|
||||
|
||||
<VDivider />
|
||||
<HDivider />
|
||||
{/* ① Total Prestasi — 3 kolom → flex-[3] */}
|
||||
<StatPanel
|
||||
title={lang === 'id' ? 'Total Prestasi' : 'Total Achievements'}
|
||||
flexClass="flex-[4]"
|
||||
>
|
||||
<StatItem value={totalPrestasi} label={lang === 'id' ? 'prestasi mahasiswa' : 'student achievements'} delay={0} />
|
||||
<StatItem value={internasional} label={lang === 'id' ? 'internasional' : 'international'} delay={150} />
|
||||
<StatItem value={nasional} label={lang === 'id' ? 'nasional' : 'national'} delay={300} />
|
||||
<StatItem value={regional} label="regional" delay={450} />
|
||||
</StatPanel>
|
||||
|
||||
{/* ② 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>
|
||||
{/* ② Total Portofolio — 1 kolom → flex-[1] */}
|
||||
<StatPanel
|
||||
title={lang === 'id' ? 'Total Portofolio' : 'Total Portfolio'}
|
||||
flexClass="flex-[1.5]"
|
||||
cols="grid-cols-1"
|
||||
>
|
||||
<StatItem value={totalProyek} label={lang === 'id' ? 'proyek mahasiswa' : 'student projects'} delay={200} />
|
||||
</StatPanel>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ const KIT_STORAGE_KEY = 'if_untan_lang';
|
||||
|
||||
function readKitLang(): Lang {
|
||||
const saved = localStorage.getItem(KIT_STORAGE_KEY);
|
||||
if (saved === 'en') return 'en';
|
||||
if (saved === 'id' || saved === 'en') return saved;
|
||||
const browser = (navigator.language || 'id').toLowerCase();
|
||||
return browser.startsWith('en') ? 'en' : 'id';
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Achievement {
|
||||
generation: string;
|
||||
image: string;
|
||||
avatar: string;
|
||||
members?: Array<{nama: string; angkatan?: number}>;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
@@ -20,10 +21,12 @@ export interface Project {
|
||||
studentName: string;
|
||||
nim?: string;
|
||||
year: number;
|
||||
category: string;
|
||||
category: string[];
|
||||
description: string;
|
||||
image: string;
|
||||
link?: string;
|
||||
catatan?: string;
|
||||
members?: Array<{nama: string; nim?: string}>;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&family=Roboto:wght@300;400;500;700&family=Merriweather:wght@700;900&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Break out of the kit's container to go full-width */
|
||||
.acaab-full-bleed {
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
/* Override parent index.html container to allow full-width React content */
|
||||
#main-content > .container {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
body { overflow-x: hidden; }
|
||||
/* Re-apply standard container constraints to the static title block in index.html */
|
||||
#main-content > .container > div.border-b {
|
||||
max-width: 80rem; /* max-w-7xl */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 3rem; /* py-12 = 3rem */
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
#main-content > .container > div.border-b {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Clean, modern full-bleed styling since parent is now full-width */
|
||||
.acaab-full-bleed {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Prevent horizontal overflow on html, body, and the assignment wrapper */
|
||||
html, body {
|
||||
overflow-x: hidden !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#assignment-body {
|
||||
overflow-x: hidden !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: "Roboto", ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
@@ -3,6 +3,21 @@ import { Achievement, Project } from '../data';
|
||||
// Determine API Base URL dynamically from Vite env, fallback to hardcoded if not set
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.ifuntanhub.dev';
|
||||
|
||||
const BIDANG_MAP: Record<number, string> = {
|
||||
1: 'Kecerdasan Buatan',
|
||||
2: 'Cyber Security',
|
||||
3: 'Data',
|
||||
4: 'Software Engineering',
|
||||
5: 'Competitive Programming',
|
||||
6: 'IoT',
|
||||
7: 'Smart City',
|
||||
8: 'Web3',
|
||||
9: 'Karya Tulis Ilmiah',
|
||||
10: 'Robotik',
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Helper to parse Rank/Juara from Achievement title
|
||||
*/
|
||||
@@ -53,7 +68,7 @@ function extractCompetition(title: string): string {
|
||||
* Fetch achievements from API
|
||||
*/
|
||||
export async function fetchAchievementsFromAPI(): Promise<Achievement[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/items/prestasi`);
|
||||
const response = await fetch(`${API_BASE_URL}/items/prestasi?fields=*.*`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch achievements: ${response.statusText}`);
|
||||
}
|
||||
@@ -65,24 +80,39 @@ export async function fetchAchievementsFromAPI(): Promise<Achievement[]> {
|
||||
.map((item: any) => {
|
||||
const id = String(item.id);
|
||||
const title = item.nama_prestasi || 'Untitled Achievement';
|
||||
const category = item.bidang || 'Akademik';
|
||||
|
||||
let category = 'Umum';
|
||||
if (item.bidang_id) {
|
||||
if (typeof item.bidang_id === 'object' && item.bidang_id.nama_bidang) {
|
||||
category = item.bidang_id.nama_bidang;
|
||||
} else {
|
||||
category = BIDANG_MAP[item.bidang_id as number] || 'Umum';
|
||||
}
|
||||
}
|
||||
|
||||
const rank = extractRank(title);
|
||||
const level = extractLevel(title, category);
|
||||
const competition = extractCompetition(title);
|
||||
const year = parseInt(item.tahun) || new Date().getFullYear();
|
||||
const team = item.nama_tim || 'Mahasiswa';
|
||||
|
||||
const anggota = Array.isArray(item.anggota) ? item.anggota : [];
|
||||
const team = item.nama_tim || (anggota.length > 0 ? anggota[0].nama : 'Mahasiswa');
|
||||
|
||||
// Handle image url mapping for directus assets
|
||||
const image = item.foto
|
||||
? `${API_BASE_URL}/assets/${item.foto}`
|
||||
: `https://picsum.photos/seed/prestasi-${id}/800/600`;
|
||||
let image = `https://picsum.photos/seed/prestasi-${id}/800/600`;
|
||||
if (item.galeri && Array.isArray(item.galeri) && item.galeri.length > 0) {
|
||||
const firstImg = item.galeri[0];
|
||||
const imgId = typeof firstImg === 'string' ? firstImg : (firstImg.directus_files_id || firstImg.id);
|
||||
if (imgId) image = `${API_BASE_URL}/assets/${imgId}`;
|
||||
} else if (item.foto) {
|
||||
image = `${API_BASE_URL}/assets/${item.foto}`;
|
||||
}
|
||||
|
||||
const avatar = (item.nama_tim || item.nama_prestasi || 'IF')
|
||||
.substring(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
// Create a friendly fallback description since Directus model lacks one
|
||||
const description = `${team} berhasil meraih prestasi luar biasa sebagai ${rank} dalam ajang ${competition} tahun ${year} pada bidang ${category.toLowerCase()}.`;
|
||||
const description = item.deskripsi || `${team} berhasil meraih prestasi luar biasa sebagai ${rank} dalam ajang ${competition} tahun ${year} pada bidang ${category.toLowerCase()}.`;
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -98,6 +128,7 @@ export async function fetchAchievementsFromAPI(): Promise<Achievement[]> {
|
||||
generation: item.angkatan ? `Angkatan ${item.angkatan}` : 'Informatika',
|
||||
image,
|
||||
avatar,
|
||||
members: anggota.map((m: any) => ({ nama: m.nama, angkatan: m.angkatan ? parseInt(m.angkatan) : undefined })),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -106,7 +137,7 @@ export async function fetchAchievementsFromAPI(): Promise<Achievement[]> {
|
||||
* Fetch projects/portfolio from API
|
||||
*/
|
||||
export async function fetchProjectsFromAPI(): Promise<Project[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/items/portfolio`);
|
||||
const response = await fetch(`${API_BASE_URL}/items/portfolio?fields=*,bidang.bidang_id.nama_bidang,galeri.*`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch portfolio: ${response.statusText}`);
|
||||
}
|
||||
@@ -118,18 +149,43 @@ export async function fetchProjectsFromAPI(): Promise<Project[]> {
|
||||
.map((item: any) => {
|
||||
const id = String(item.id);
|
||||
const title = item.nama_proyek || 'Untitled Project';
|
||||
const studentName = item.nama_mahasiswa || 'Mahasiswa';
|
||||
|
||||
const anggota = Array.isArray(item.anggota) ? item.anggota : [];
|
||||
const studentName = item.nama_mahasiswa || (anggota.length > 0 ? anggota[0].nama : 'Mahasiswa');
|
||||
|
||||
const year = parseInt(item.tahun) || new Date().getFullYear();
|
||||
const category = item.bidang || 'Software Development';
|
||||
const description = item.deskripsi || 'Tidak ada deskripsi.';
|
||||
|
||||
let categories: string[] = [];
|
||||
if (item.bidang && Array.isArray(item.bidang)) {
|
||||
categories = item.bidang.map((b: any) => {
|
||||
if (b.bidang_id && typeof b.bidang_id === 'object' && b.bidang_id.nama_bidang) {
|
||||
return b.bidang_id.nama_bidang;
|
||||
}
|
||||
if (typeof b.bidang_id === 'number') {
|
||||
return BIDANG_MAP[b.bidang_id] || 'Umum';
|
||||
}
|
||||
return 'Umum';
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
if (categories.length === 0) {
|
||||
categories = ['Umum'];
|
||||
}
|
||||
|
||||
let link = item.url || undefined;
|
||||
if (link && !/^https?:\/\//i.test(link)) {
|
||||
link = `https://${link}`;
|
||||
}
|
||||
|
||||
const image = item.foto
|
||||
? `${API_BASE_URL}/assets/${item.foto}`
|
||||
: `https://picsum.photos/seed/project-${id}/800/600`;
|
||||
let image = `https://picsum.photos/seed/project-${id}/800/600`;
|
||||
if (item.galeri && Array.isArray(item.galeri) && item.galeri.length > 0) {
|
||||
const firstImg = item.galeri[0];
|
||||
const imgId = typeof firstImg === 'string' ? firstImg : (firstImg.directus_files_id || firstImg.id);
|
||||
if (imgId) image = `${API_BASE_URL}/assets/${imgId}`;
|
||||
} else if (item.foto) {
|
||||
image = `${API_BASE_URL}/assets/${item.foto}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -137,10 +193,12 @@ export async function fetchProjectsFromAPI(): Promise<Project[]> {
|
||||
studentName,
|
||||
nim: item.nim || undefined,
|
||||
year,
|
||||
category,
|
||||
category: categories,
|
||||
description,
|
||||
image,
|
||||
link,
|
||||
catatan: item.catatan || undefined,
|
||||
members: anggota.map((m: any) => ({ nama: m.nama, nim: m.nim || undefined })),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user