feat: implement references section with marquee-scrolling testimonial cards and data model

This commit is contained in:
Poyraz
2026-06-29 09:40:54 +03:00
parent efd95da0d0
commit 4100d7d9e2
6 changed files with 124 additions and 16 deletions
+15
View File
@@ -1,2 +1,17 @@
@import "tailwindcss"; @import "tailwindcss";
@import "poyraz-ui/preset.css"; @import "poyraz-ui/preset.css";
@layer utilities {
@keyframes marquee {
0% { transform: translateX(0%); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
animation: marquee 35s linear infinite;
}
.pause-on-hover:hover {
animation-play-state: paused;
}
}
+64
View File
@@ -0,0 +1,64 @@
import React from "react";
import Image from "next/image";
export interface CustomTestimonialCardProps extends React.HTMLAttributes<HTMLDivElement> {
quote: string;
author: string;
role?: string;
avatar?: string;
rating?: number;
lineClamp?: boolean;
}
export const CustomTestimonialCard = React.forwardRef<HTMLDivElement, CustomTestimonialCardProps>(
({ quote, author, role, avatar, rating, lineClamp, className, ...props }, ref) => {
return (
<div
ref={ref}
className={`flex flex-col p-6 bg-white border border-neutral-200 rounded-2xl shadow-sm transition-shadow ${className || ""}`}
{...props}
>
<div className="flex-1">
{rating && (
<div className="flex gap-1 mb-4 text-yellow-400">
{Array.from({ length: 5 }).map((_, i) => (
<svg
key={i}
className={`w-5 h-5 ${i < rating ? "fill-current" : "text-neutral-200 fill-current"}`}
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
)}
<p className={`text-neutral-700 italic leading-relaxed ${lineClamp ? "line-clamp-4" : ""}`}>
&quot;{quote}&quot;
</p>
</div>
<div className="flex items-center gap-4 mt-6">
{avatar ? (
<Image
src={avatar}
alt={author}
width={48}
height={48}
className="rounded-full object-cover w-12 h-12"
/>
) : (
<div className="w-12 h-12 rounded-full bg-neutral-200 flex items-center justify-center text-neutral-500 font-bold">
{author.charAt(0)}
</div>
)}
<div className="flex flex-col">
<span className="font-semibold text-neutral-900">{author}</span>
{role && <span className="text-sm text-neutral-500">{role}</span>}
</div>
</div>
</div>
);
}
);
CustomTestimonialCard.displayName = "CustomTestimonialCard";
+16 -12
View File
@@ -1,5 +1,5 @@
import { Link } from "@/i18n/routing";
import { TestimonialCard } from "poyraz-ui/molecules"; import { CustomTestimonialCard } from "@/components/custom-testimonial-card";
import { REFERENCES } from "@/data/references"; import { REFERENCES } from "@/data/references";
import { useLocale } from "next-intl"; import { useLocale } from "next-intl";
import { getLocalizedValue } from "@/lib/locale"; import { getLocalizedValue } from "@/lib/locale";
@@ -7,40 +7,44 @@ import { getLocalizedValue } from "@/lib/locale";
type ReferenceCardsProps = { type ReferenceCardsProps = {
className?: string; className?: string;
cardClassName?: string; cardClassName?: string;
lineClamp?: boolean;
}; };
export function ReferenceCards({ className, cardClassName }: ReferenceCardsProps) { export function ReferenceCards({ className, cardClassName, lineClamp }: ReferenceCardsProps) {
const locale = useLocale(); const locale = useLocale();
return ( return (
<div className={className}> <div className={className}>
{REFERENCES.map((item) => { {REFERENCES.map((item) => {
const card = ( const card = (
<TestimonialCard <CustomTestimonialCard
quote={getLocalizedValue(item.quote, locale)} quote={getLocalizedValue(item.quote, locale)}
author={item.author} author={item.author}
role={getLocalizedValue(item.role, locale)} role={getLocalizedValue(item.role, locale)}
avatar={item.avatar} avatar={item.avatar}
rating={item.rating} rating={item.rating}
className={`h-full rounded-sm ${cardClassName || "w-70 shrink-0"}`} lineClamp={lineClamp}
className={`h-full ${cardClassName || "w-70 shrink-0"}`}
/> />
); );
if (!item.profileHref) { const href = item.documentHref || item.profileHref;
return <div key={item.id}>{card}</div>;
if (!href) {
return <div key={item.id} className="h-full">{card}</div>;
} }
return ( return (
<Link <a
key={item.id} key={item.id}
href={item.profileHref} href={href}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
aria-label={`${item.author} LinkedIn profili`} aria-label={`${item.author} bağlantısı`}
className="block" className="block h-full transition-transform hover:scale-[1.02]"
> >
{card} {card}
</Link> </a>
); );
})} })}
</div> </div>
+2 -1
View File
@@ -15,8 +15,9 @@ export function ReferencesDetailContent() {
</Link> </Link>
<ReferenceCards <ReferenceCards
className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3" className="columns-1 gap-4 sm:columns-2 lg:columns-3 [&>a]:mb-4 [&>a]:break-inside-avoid [&>div]:mb-4 [&>div]:break-inside-avoid"
cardClassName="w-full" cardClassName="w-full"
lineClamp={false}
/> />
</section> </section>
); );
+12 -3
View File
@@ -5,11 +5,20 @@ import { ReferenceCards } from "@/components/reference-cards";
export function ReferencesSection() { export function ReferencesSection() {
return ( return (
<section className="space-y-8 pt-0 sm:pt-12"> <section className="space-y-8 pt-0 sm:pt-12">
<div className="relative overflow-hidden rounded-sm max-h-48"> <div className="relative overflow-hidden rounded-sm py-4">
<ReferenceCards className="flex w-max gap-3" /> {/* We use two sets of cards for seamless infinite marquee effect */}
<div className="flex w-max gap-4 animate-marquee pause-on-hover">
<ReferenceCards className="flex gap-4 shrink-0" lineClamp={true} cardClassName="w-80" />
<ReferenceCards className="flex gap-4 shrink-0" lineClamp={true} cardClassName="w-80" />
</div>
<div <div
aria-hidden="true" aria-hidden="true"
className="pointer-events-none opacity-95 absolute top-0 right-0 h-full w-48 bg-linear-to-l from-white via-white/80 to-transparent" className="pointer-events-none absolute top-0 right-0 h-full w-32 bg-gradient-to-l from-white to-transparent"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute top-0 left-0 h-full w-32 bg-gradient-to-r from-white to-transparent"
/> />
</div> </div>
</section> </section>
+15
View File
@@ -12,6 +12,7 @@ export type Reference = {
avatar: string; avatar: string;
rating?: number; rating?: number;
profileHref?: string; profileHref?: string;
documentHref?: string;
}; };
export const REFERENCES: Reference[] = [ export const REFERENCES: Reference[] = [
@@ -85,4 +86,18 @@ export const REFERENCES: Reference[] = [
en: "I've been working with Poyraz since the early days of Shipin, and he continues to impress me with his professionalism. He has a rare combination of solid technical skills and passion for teaching others. If I was to say one thing about him, it would be his reliability. He never over-promises, always delivers on his commitments, and genuinely incorporates feedback to improve his work.", en: "I've been working with Poyraz since the early days of Shipin, and he continues to impress me with his professionalism. He has a rare combination of solid technical skills and passion for teaching others. If I was to say one thing about him, it would be his reliability. He never over-promises, always delivers on his commitments, and genuinely incorporates feedback to improve his work.",
}, },
}, },
{
id: "ilker-yoncaci",
author: "İlker Yoncacı",
role: {
tr: "Dr. Öğr. Üyesi - OSTİM Teknik Üniversitesi - VİA Bilgisayar Sistemleri Founder / CEO",
en: "Assist. Prof. Dr. - OSTİM Technical University - VİA Computer Systems Founder / CEO",
},
avatar: "/avatars/ilker.png",
documentHref: "/referances/ilker.pdf",
quote: {
tr: "Poyraz AVSEVERle Ostim Teknik Üniversitesi bünyesinde çalışırken tanıştım. Kendisi 2025-2026 Eğitim Öğretim Yılı Bahar döneminde iki dersimi birden almış ve gayet iyi notlarla derslerimden geçmiştir. Çalıştığı süre zarfında başarılı çalışmalara imza atan Poyraz AVSEVERin yazılı ve sözlü iletişim becerileri son derece güçlüdür. Hem bağımsız hem de bir ekiple çalışabilen Poyraz, iş disiplini ile de beğenimizi kazanmıştır. Derslerin yanısıra Poyraz, web ve mobil teknolojiler konularında pek çok süreçte görev almıştır. Öğrenmeye açık yapısı ve araştırma hevesi sayesinde temel mühendislik altyapısını iyi bir seviyeye taşımıştır. Gerek ekip arkadaşları gerekse paydaşları ile kuvvetli bir iletişim kurmayı başaran; proaktif olarak fikir ve öneri geliştirebilen Poyraz’ın kurumunuza da aynı şekilde katkı sağlayacağına inanıyorum. Disiplinli çalışması ve uyumlu karakteri göz önüne alındığında, yer alacağı çalışmalarda verimli olacağını düşünüyor ve kendisini tavsiye ediyorum. Konuyla ilgili herhangi bir sorunuz olursa benimle iletişim kurabilirsiniz.",
en: "I met Poyraz AVSEVER while working at Ostim Technical University. He took two of my courses in the Spring semester of the 2025-2026 Academic Year and passed them with very good grades. Poyraz AVSEVER, who has carried out successful works during his time working, has extremely strong written and verbal communication skills. Capable of working both independently and with a team, Poyraz has won our appreciation with his work discipline. In addition to courses, Poyraz took part in many processes regarding web and mobile technologies. Thanks to his openness to learning and enthusiasm for research, he has brought his basic engineering infrastructure to a good level. I believe that Poyraz, who successfully establishes strong communication with both his teammates and stakeholders and can proactively develop ideas and suggestions, will contribute to your institution in the same way. Considering his disciplined work and harmonious character, I think he will be productive in the projects he will take part in and I recommend him. If you have any questions about the subject, you can contact me.",
},
},
]; ];