fixed: feedback image problem and added smooth transition for feedback

This commit is contained in:
Billy Marcello
2026-01-09 22:20:21 +07:00
parent 801a7bb65c
commit 694a05cb39
3 changed files with 101 additions and 70 deletions
-2
View File
@@ -277,8 +277,6 @@ const Dashboard: React.FC = () => {
} }
}; };
console.log("AXIOS BASE URL:", API_BASE_URL);
fetchDashboardData(); fetchDashboardData();
}, [api]); }, [api]);
+100 -67
View File
@@ -3,7 +3,7 @@ import team from "../assets/teamwokr.jpg";
import bghero1 from "../assets/hero1.png"; import bghero1 from "../assets/hero1.png";
import Footer from "../Layout/Footer"; import Footer from "../Layout/Footer";
import Faq from "../Layout/Faq"; import Faq from "../Layout/Faq";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
type Internship = { type Internship = {
@@ -33,6 +33,11 @@ const Landing = () => {
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]); const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
// Smooth fade animation
const [displayIndex, setDisplayIndex] = useState(0);
const [fadeIn, setFadeIn] = useState(true);
const FADE_MS = 450;
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => { const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (search.trim()) { if (search.trim()) {
@@ -40,8 +45,22 @@ const Landing = () => {
} }
}; };
const getFeedbackImageUrl = (path: string) => // helper url image
`${import.meta.env.VITE_API_BASE_URL}${path}`; const getFeedbackImageUrl = (path: string) => {
if (!path) return "";
if (/^https?:\/\//i.test(path)) return path;
return `${import.meta.env.VITE_API_BASE_URL}${path}`;
};
const currentFeedback = useMemo(() => {
if (feedbacks.length === 0) return null;
return feedbacks[displayIndex];
}, [feedbacks, displayIndex]);
const currentImgUrl = useMemo(() => {
if (!currentFeedback) return "";
return getFeedbackImageUrl(currentFeedback.image_path);
}, [currentFeedback]);
useEffect(() => { useEffect(() => {
// Fetch internships // Fetch internships
@@ -58,23 +77,60 @@ const Landing = () => {
fetch(`${import.meta.env.VITE_API_BASE_URL}/feedback-api/get`) fetch(`${import.meta.env.VITE_API_BASE_URL}/feedback-api/get`)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
setFeedbacks(data.data as Feedback[]); const arr = data.data as Feedback[];
setFeedbacks(arr);
setActiveIndex(0); setActiveIndex(0);
setDisplayIndex(0);
setFadeIn(true);
}) })
.catch((err) => console.error("Error fetching feedback:", err)); .catch((err) => console.error("Error fetching feedback:", err));
}, []); }, []);
// Auto slide testimonial // Auto slide target index
useEffect(() => { useEffect(() => {
if (feedbacks.length === 0) return; if (feedbacks.length === 0) return;
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % feedbacks.length); setActiveIndex((prev) => (prev + 1) % feedbacks.length);
}, 5000); // 5 detik }, 5000);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [feedbacks.length]); }, [feedbacks.length]);
// ✅ Animasi fade 2-phase: fade-out -> switch content -> fade-in
useEffect(() => {
if (feedbacks.length === 0) return;
if (activeIndex === displayIndex) return;
// 1) fade-out konten lama
setFadeIn(false);
// 2) setelah fade-out selesai, ganti konten, lalu fade-in
const t = setTimeout(() => {
setDisplayIndex(activeIndex);
// next frame biar browser apply dom update
requestAnimationFrame(() => {
setFadeIn(true);
});
}, FADE_MS);
return () => clearTimeout(t);
}, [activeIndex, displayIndex, feedbacks.length]);
// preload gambar slide berikutnya biar next slide lebih cepat
useEffect(() => {
if (feedbacks.length === 0) return;
const next = feedbacks[(activeIndex + 1) % feedbacks.length];
const nextUrl = getFeedbackImageUrl(next.image_path);
if (!nextUrl) return;
const img = new Image();
img.decoding = "async";
img.src = nextUrl;
}, [activeIndex, feedbacks]);
return ( return (
<div className="body-landing bg-[#F8F9FA] min-h-screen"> <div className="body-landing bg-[#F8F9FA] min-h-screen">
<div className="navbar-section py-[20px]"> <div className="navbar-section py-[20px]">
@@ -97,8 +153,8 @@ const Landing = () => {
Tingkatkan keahlian, perluas kesempatan Tingkatkan keahlian, perluas kesempatan
</h2> </h2>
<h2 className="font-semibold text-base sm:text-lg lg:text-[20px] text-white mt-2"> <h2 className="font-semibold text-base sm:text-lg lg:text-[20px] text-white mt-2">
Asah keterampilanmu dan jadilah mahasiswa yang siap bersaing di Asah keterampilanmu dan jadilah mahasiswa yang siap bersaing di dunia
dunia industri industri
</h2> </h2>
<div className="search-section bg-white p-[15px] rounded-xl shadow-md w-full max-w-lg mt-[40px] text-black"> <div className="search-section bg-white p-[15px] rounded-xl shadow-md w-full max-w-lg mt-[40px] text-black">
@@ -118,9 +174,7 @@ const Landing = () => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
onClick={() => { onClick={() => {
if (search.trim()) if (search.trim())
navigate( navigate(`/internships?search=${encodeURIComponent(search)}`);
`/internships?search=${encodeURIComponent(search)}`
);
}} }}
> >
<path <path
@@ -141,11 +195,10 @@ const Landing = () => {
Life at Humic Engineering Life at Humic Engineering
</h2> </h2>
<p className="font-medium text-base sm:text-[16px]"> <p className="font-medium text-base sm:text-[16px]">
Raih pengalaman internship yang bernilai dengan berkontribusi Raih pengalaman internship yang bernilai dengan berkontribusi langsung
langsung dalam pengembangan solusi teknologi kami. Kembangkan dalam pengembangan solusi teknologi kami. Kembangkan kompetensi Anda
kompetensi Anda melalui keterlibatan nyata dalam proyek IT serta melalui keterlibatan nyata dalam proyek IT serta bimbingan langsung
bimbingan langsung dari para expert di bidang Software engineering dari para expert di bidang Software engineering dan IoT.
dan IoT.
</p> </p>
</div> </div>
@@ -156,30 +209,35 @@ const Landing = () => {
</div> </div>
{/* Hero 3 Section */} {/* Hero 3 Section */}
{feedbacks.length > 0 && ( {feedbacks.length > 0 && currentFeedback && (
<div className="hero-3-section mx-5 lg:mx-[80px] xl:mx-[140px] mt-[60px] rounded-2xl bg-[#F8F9FA]"> <div className="hero-3-section mx-5 lg:mx-[80px] xl:mx-[140px] mt-[60px] rounded-2xl bg-[#F8F9FA]">
<div className="flex flex-col lg:flex-row gap-10 lg:gap-16 items-stretch py-10 px-4 sm:px-8"> <div className="flex flex-col lg:flex-row gap-10 lg:gap-16 items-stretch py-10 px-4 sm:px-8">
{/* LEFT: big + small image */} {/* LEFT: image */}
<div className="relative flex-1 min-h-[320px] lg:min-h-[380px]"> <div className="relative flex-1 min-h-[320px] lg:min-h-[380px] flex items-center justify-center">
<div
className={`w-[230px] h-[320px] sm:w-[260px] sm:h-[340px] lg:w-[280px] lg:h-[360px]
rounded-xl shadow-xl bg-white/70 backdrop-blur p-3 flex items-center justify-center
transition-opacity duration-[450ms] ease-in-out
${fadeIn ? "opacity-100" : "opacity-0"}`}
>
<img <img
src={getFeedbackImageUrl(feedbacks[activeIndex].image_path)} key={currentImgUrl}
alt={feedbacks[activeIndex].nama} src={currentImgUrl}
className="absolute left-1/2 top-1/2 w-[230px] h-[320px] sm:w-[260px] sm:h-[340px] lg:w-[280px] lg:h-[360px] -translate-x-1/2 -translate-y-1/2 rounded-xl object-cover shadow-xl" alt={currentFeedback.nama}
loading="eager"
decoding="async"
// @ts-ignore
fetchPriority="high"
className="w-full h-full object-contain rounded-lg"
/> />
</div>
{feedbacks.length > 1 && (
<img
src={getFeedbackImageUrl(
feedbacks[(activeIndex + 1) % feedbacks.length].image_path
)}
alt={feedbacks[(activeIndex + 1) % feedbacks.length].nama}
className="absolute left-0 bottom-6 w-[110px] h-[150px] rounded-lg object-cover shadow-md hidden sm:block"
/>
)}
</div> </div>
{/* RIGHT: text + thumbnails */} {/* RIGHT: text */}
<div className="flex-1 flex flex-col justify-between"> <div
className={`flex-1 flex flex-col justify-between transition-opacity duration-[450ms] ease-in-out
${fadeIn ? "opacity-100" : "opacity-0"}`}
>
<div> <div>
<h2 className="font-bold text-2xl sm:text-[32px] text-[#111827]"> <h2 className="font-bold text-2xl sm:text-[32px] text-[#111827]">
Kata mereka tentang Humic Engineering Kata mereka tentang Humic Engineering
@@ -188,44 +246,24 @@ const Landing = () => {
<p <p
className="font-medium text-base sm:text-[16px] text-[#4B5563] mt-4 leading-relaxed" className="font-medium text-base sm:text-[16px] text-[#4B5563] mt-4 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: feedbacks[activeIndex].pesan, __html: currentFeedback.pesan,
}} }}
></p> />
<p className="mt-5 text-[16px] text-[#C3423F] font-semibold"> <p className="mt-5 text-[16px] text-[#C3423F] font-semibold">
{feedbacks[activeIndex].nama},{" "} {currentFeedback.nama},{" "}
<span className="font-normal text-[#6B7280]"> <span className="font-normal text-[#6B7280]">
{feedbacks[activeIndex].posisi} · Humic Internship Batch{" "} {currentFeedback.posisi} · Humic Internship Batch{" "}
{feedbacks[activeIndex].batch}{" "} {currentFeedback.batch} {currentFeedback.tahun}
{feedbacks[activeIndex].tahun}
</span> </span>
</p> </p>
<p className="mt-1 text-sm text-[#9CA3AF]"> <p className="mt-1 text-sm text-[#9CA3AF]">
{feedbacks[activeIndex].universitas} {currentFeedback.universitas}
</p> </p>
</div> </div>
<div className="mt-8 flex items-center gap-4 overflow-x-auto pb-2"> <div className="mt-8" />
{feedbacks.map((fb, index) => (
<button
key={fb.id}
type="button"
onClick={() => setActiveIndex(index)}
className={`relative shrink-0 rounded-xl overflow-hidden border transition-all duration-200 ${
index === activeIndex
? "border-[#C3423F] ring-2 ring-[#C3423F]/40"
: "border-transparent opacity-60 hover:opacity-100"
}`}
>
<img
src={getFeedbackImageUrl(fb.image_path)}
alt={fb.nama}
className="w-[90px] h-[110px] object-cover"
/>
</button>
))}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -255,22 +293,17 @@ const Landing = () => {
} }
}} }}
> >
{/* Background image */}
<div <div
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
backgroundImage: `url(${import.meta.env.VITE_API_BASE_URL}${ backgroundImage: `url(${import.meta.env.VITE_API_BASE_URL}${intern.image_path})`,
intern.image_path
})`,
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
}} }}
/> />
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-60" /> <div className="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-60" />
{/* Text content */}
<div <div
className={`relative z-10 flex flex-col justify-end h-full p-4 ${ className={`relative z-10 flex flex-col justify-end h-full p-4 ${
isClosed ? "text-gray-400" : "text-white" isClosed ? "text-gray-400" : "text-white"
+1 -1
View File
@@ -25,7 +25,7 @@ const LoginAdmin = () => {
if (response.status === 200) { if (response.status === 200) {
document.cookie = `token=${response.data.data.token}; path=/;`; document.cookie = `token=${response.data.data.token}; path=/;`;
navigate("/internships-list"); navigate("/dashboard");
} else { } else {
setErrorMsg(response.data.message || "Login failed"); setErrorMsg(response.data.message || "Login failed");
} }