feat: create Partnership and Blog functional components

This commit is contained in:
hkiett265
2026-04-14 11:40:55 +07:00
parent ad68b7d8c4
commit 4bfad8481b
10 changed files with 511 additions and 0 deletions

176
app/blog/blog-page.css Normal file
View File

@@ -0,0 +1,176 @@
/* ============================================
Blog Page — Scoped Styles
Scope: .blog-page
============================================ */
/* Reset heading override từ main.css */
.blog-page h1,
.blog-page h2,
.blog-page h3,
.blog-page h4 {
font-size: unset;
font-weight: unset;
line-height: unset;
margin: 0;
padding: 0;
}
/* Typography classes thay thế h1/h2/h3 */
.blog-page .blog-heading {
font-size: clamp(1.75rem, 4vw, 3rem);
font-weight: 700;
line-height: 1.15;
}
.blog-page .blog-card-title {
font-size: 1.1rem;
font-weight: 700;
line-height: 1.3;
}
.blog-page .blog-widget-title {
font-size: 1.1rem;
font-weight: 700;
line-height: 1.3;
}
/* ---------- Color tokens ---------- */
.blog-page .bg-brand-blue { background-color: rgb(38, 60, 111); }
.blog-page .text-brand-blue { color: rgb(38, 60, 111); }
.blog-page .border-brand-blue { border-color: rgb(38, 60, 111); }
.blog-page .bg-brand-light { background-color: #f8fbff; }
.blog-page .text-brand-light { color: #f8fbff; }
.blog-page .bg-brand-hover { background-color: #2d3a8c; }
.blog-page .hover\:bg-brand-hover:hover { background-color: #2d3a8c; }
.blog-page .text-ui-text { color: #111827; }
.blog-page .text-ui-muted { color: #6b7280; }
.blog-page .bg-ui-bg { background-color: #f9fafb; }
.blog-page .border-ui-border { border-color: #e5e7eb; }
/* hover:text-brand-blue */
.blog-page .hover\:text-brand-blue:hover { color: #1b254b; }
.blog-page .hover\:border-brand-blue:hover { border-color: #1b254b; }
/* group-hover */
.blog-page .group:hover .group-hover\:text-brand-blue { color: #1b254b; }
.blog-page .group:hover .group-hover\:border-brand-blue { border-color: #1b254b; }
.blog-page .group:hover .group-hover\:gap-2 { gap: 0.5rem; }
.blog-page .group:hover .group-hover\:scale-105 { transform: scale(1.05); }
/* shadow tokens */
.blog-page .shadow-soft {
box-shadow: 0 1px 3px rgb(0 0 0 / 0.06), 0 1px 2px rgb(0 0 0 / 0.04);
}
.blog-page .shadow-hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
}
.blog-page .hover\:shadow-hover:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
}
/* border-brand-blue/20, border-brand-blue/50 */
.blog-page .border-brand-blue\/20 { border-color: rgb(27 37 75 / 0.2); }
.blog-page .border-brand-blue\/50 { border-color: rgb(27 37 75 / 0.5); }
.blog-page .hover\:border-brand-blue\/50:hover { border-color: rgb(27 37 75 / 0.5); }
/* text-brand-blue/40 */
.blog-page .text-brand-blue\/40 { color: rgb(27 37 75 / 0.4); }
/* text-brand-light/80, /60 */
.blog-page .text-brand-light\/80 { color: rgb(248 251 255 / 0.8); }
.blog-page .text-brand-light\/60 { color: rgb(248 251 255 / 0.6); }
/* bg-white/10, /90 */
.blog-page .bg-white\/10 { background-color: rgb(255 255 255 / 0.1); }
.blog-page .bg-white\/90 { background-color: rgb(255 255 255 / 0.9); }
/* border-white/20 */
.blog-page .border-white\/20 { border-color: rgb(255 255 255 / 0.2); }
/* placeholder */
.blog-page .placeholder-white\/50::placeholder { color: rgb(255 255 255 / 0.5); }
/* Category filter buttons */
.blog-page #category-filters button {
padding-left: 1.25rem !important;
padding-right: 1.25rem !important;
padding-top: 0.4rem !important;
padding-bottom: 0.4rem !important;
border-radius: 9999px !important;
font-size: 0.8rem !important;
}
/* Category filter hover — viền sáng lên */
.blog-page #category-filters button:not(:first-child):hover {
border-color: #1b254b !important;
color: #1b254b !important;
}
/* Newsletter input placeholder */
.blog-page .newsletter-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
/* Newsletter widget */
.blog-page .folder-tab {
background-color: #263c6f !important;
clip-path: polygon(0 0, calc(100% - 40px) 0, 100% 40px, 100% 100%, 0 100%);
border-radius: 12px !important;
}
.blog-page .folder-tab input {
border-radius: 8px !important;
padding: 0.35rem 0.75rem !important;
font-size: 0.875rem !important;
background-color: rgba(255,255,255,0.12) !important;
border: 1px solid rgba(255,255,255,0.25) !important;
color: white !important;
}
.blog-page .folder-tab input::placeholder {
color: rgba(255,255,255,0.5) !important;
}
.blog-page .folder-tab button[type="submit"] {
border-radius: 8px !important;
padding: 0.35rem 0.75rem !important;
font-size: 0.875rem !important;
font-weight: 700 !important;
background-color: white !important;
color: #1b254b !important;
}
/* Pagination buttons */
.blog-page .border-ui-border[class*="w-10"] {
border-radius: 8px !important;
}
.blog-page .border-ui-border[class*="w-10"]:hover {
border-color: rgb(38, 60, 111) !important;
color: rgb(38, 60, 111) !important;
}
.blog-page .bg-brand-blue[class*="w-10"] {
border-radius: 8px !important;
background-color: rgb(38, 60, 111) !important;
}
/* Pagination */
.blog-page .pg-btn {
border-radius: 8px !important;
transition: border-color 0.2s, color 0.2s;
}
.blog-page .pg-btn:hover {
border-color: rgb(38, 60, 111) !important;
color: rgb(38, 60, 111) !important;
}
.blog-page .pg-active {
background-color: rgb(38, 60, 111) !important;
border: none !important;
}

View File

@@ -0,0 +1,26 @@
import "./blog-page.css";
import FeaturedHero from "../components/blog/FeaturedHero";
import CategoryFilters from "../components/blog/CategoryFilters";
import NewsGrid from "../components/blog/NewsGrid";
import BlogSidebar from "../components/blog/BlogSidebar";
export default function BlogPage() {
return (
<main className="blog-page w-full min-h-screen bg-ui-bg pb-20">
<FeaturedHero />
<div className="max-w-[1440px] mx-auto px-6 lg:px-8">
<div className="flex flex-col lg:flex-row gap-12">
{/* Left: filters + news grid */}
<div className="flex-1">
<CategoryFilters />
<NewsGrid />
</div>
{/* Right: sidebar */}
<BlogSidebar />
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,13 @@
import NewsletterWidget from "./NewsletterWidget";
import ResearcherSpotlight from "./ResearcherSpotlight";
import UpcomingEvents from "./UpcomingEvents";
export default function BlogSidebar() {
return (
<aside className="w-full lg:w-[400px] shrink-0 space-y-8">
<NewsletterWidget />
<ResearcherSpotlight />
<UpcomingEvents />
</aside>
);
}

View File

@@ -0,0 +1,21 @@
const categories = ["All News", "Campus", "Research", "Partnerships", "Events"];
export default function CategoryFilters() {
return (
<div id="category-filters" className="flex flex-wrap items-center gap-3 mb-10 pb-6 border-b border-ui-border">
{categories.map((cat, i) => (
<button
key={cat}
style={{ borderRadius: "9999px", fontSize: "0.8rem" }}
className={
i === 0
? "px-6 py-2 bg-brand-blue text-white text-sm font-semibold shadow-sm transition-colors"
: "px-6 py-2 bg-white border border-ui-border text-ui-text hover:border-brand-blue hover:text-brand-blue text-sm font-medium transition-colors"
}
>
{cat}
</button>
))}
</div>
);
}

View File

@@ -0,0 +1,44 @@
export default function FeaturedHero() {
return (
<section id="featured-hero" className="w-full bg-white border-b border-ui-border mb-12">
<div className="max-w-[1440px] mx-auto">
<div className="flex flex-col lg:flex-row min-h-[500px]">
{/* Left: Content */}
<div className="w-full lg:w-1/2 p-8 lg:p-16 flex flex-col justify-center">
<span className="inline-block px-3 py-1 bg-brand-light text-brand-blue border border-brand-blue/20 text-xs font-semibold mb-4 w-fit" style={{ borderRadius: "6px" }}>
Featured Research
</span>
<div className="blog-heading text-ui-text tracking-tight mb-6 leading-tight font-bold">
Pioneering Sustainable Urban Development in the Heart of Paris
</div>
<p className="text-lg text-ui-muted mb-8 leading-relaxed">
Our latest collaborative research initiative uncovers groundbreaking methods for integrating green infrastructure into historic urban environments, setting a new standard for European cities.
</p>
<div className="flex items-center gap-4 text-sm text-ui-muted mb-8 mt-4">
<div className="flex items-center gap-2">
<img src="https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-1.jpg" alt="Author" className="w-8 h-8 rounded-full border border-ui-border" />
<span className="font-medium text-ui-text">Dr. Amélie Dubois</span>
</div>
<span></span>
<span>October 15, 2024</span>
<span></span>
<span>5 min read</span>
</div>
<a href="#" className="inline-flex items-center justify-center px-5 py-3 text-white font-semibold text-sm hover:bg-brand-hover transition-colors shadow-sm w-fit gap-2" style={{ borderRadius: "5px", backgroundColor: "rgb(38, 60, 111)" }}>
Read Full Story <i className="fas fa-arrow-right text-xs"></i>
</a>
</div>
{/* Right: Image */}
<div className="w-full lg:w-1/2 bg-gray-100 relative min-h-[300px] lg:min-h-full">
<img
className="absolute inset-0 w-full h-full object-cover"
src="https://storage.googleapis.com/uxpilot-auth.appspot.com/75c7c284e8-18119bca345a35938b88.png"
alt="Aerial view of Paris with sustainable green roofs and modern eco-friendly architecture blending with historic buildings"
/>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,48 @@
type NewsCardProps = {
category: string;
date: string;
title: string;
excerpt: string;
image?: string;
icon?: string;
};
export default function NewsCard({ category, date, title, excerpt, image, icon }: NewsCardProps) {
return (
<article className="bg-white rounded-xl border border-ui-border overflow-hidden shadow-soft hover:shadow-hover hover:border-brand-blue/50 transition-all group flex flex-col">
{/* Thumbnail */}
<div className="h-48 overflow-hidden relative">
{image ? (
<img
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
src={image}
alt={title}
/>
) : (
<div className="w-full h-full bg-brand-light flex items-center justify-center">
<i className={`${icon} text-4xl text-brand-blue/40`}></i>
</div>
)}
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-white/90 backdrop-blur-sm text-brand-blue rounded-md text-xs font-bold shadow-sm">
{category}
</span>
</div>
</div>
{/* Content */}
<div className="p-6 flex flex-col flex-1">
<div className="text-xs text-ui-muted mb-3 flex items-center gap-2">
<i className="far fa-calendar"></i> {date}
</div>
<div className="blog-card-title text-ui-text group-hover:text-brand-blue transition-colors mb-3 leading-tight font-bold">
{title}
</div>
<p className="text-sm text-ui-muted mb-6 flex-1 line-clamp-3">{excerpt}</p>
<a href="#" className="text-sm font-bold text-brand-blue flex items-center gap-1 group-hover:gap-2 transition-all pt-4">
Read more <i className="fas fa-arrow-right text-xs"></i>
</a>
</div>
</article>
);
}

View File

@@ -0,0 +1,59 @@
import NewsCard from "./NewsCard";
const news = [
{
category: "Campus",
date: "Oct 12, 2024",
title: "New Liberal Arts Library Wing Opens to Students",
excerpt: "The state-of-the-art facility provides expanded collaborative spaces and access to over 50,000 new digital and print resources for our growing student body.",
image: "https://storage.googleapis.com/uxpilot-auth.appspot.com/8eafd095d9-314bdad36b2119266084.png",
},
{
category: "Partnerships",
date: "Oct 10, 2024",
title: "ULP Announces Strategic Alliance with TechGlobal Institute",
excerpt: "A new partnership aimed at bridging the gap between liberal arts education and emerging technological paradigms in the 21st century.",
icon: "fas fa-handshake",
},
{
category: "Events",
date: "Oct 05, 2024",
title: "Annual Global Ethics Symposium Draws Record Attendance",
excerpt: "Scholars from over 40 countries gathered at ULP this weekend to discuss the evolving landscape of international human rights and digital privacy.",
image: "https://storage.googleapis.com/uxpilot-auth.appspot.com/fc832714d8-ee7527b4d94c300aae71.png",
},
{
category: "Research",
date: "Sep 28, 2024",
title: "Department of Sociology Publishes Landmark Study on Urban Migration",
excerpt: "A comprehensive 5-year study reveals shifting demographic patterns in post-industrial European cities, highlighting new socio-economic challenges.",
icon: "fas fa-flask",
},
];
export default function NewsGrid() {
return (
<div className="flex-1">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
{news.map((item) => (
<NewsCard key={item.title} {...item} />
))}
</div>
{/* Pagination */}
<div className="flex justify-center items-center gap-2 border-t border-ui-border pt-8">
<button className="pg-btn w-10 h-10 flex items-center justify-center border border-ui-border text-ui-muted transition-colors disabled:opacity-50" disabled>
<i className="fas fa-chevron-left"></i>
</button>
<button className="pg-btn pg-active w-10 h-10 flex items-center justify-center text-white font-bold">1</button>
<button className="pg-btn w-10 h-10 flex items-center justify-center border border-ui-border text-ui-text transition-colors font-medium">2</button>
<button className="pg-btn w-10 h-10 flex items-center justify-center border border-ui-border text-ui-text transition-colors font-medium">3</button>
<span className="text-ui-muted mx-2">...</span>
<button className="pg-btn w-10 h-10 flex items-center justify-center border border-ui-border text-ui-text transition-colors font-medium">12</button>
<button className="pg-btn w-10 h-10 flex items-center justify-center border border-ui-border text-ui-text transition-colors">
<i className="fas fa-chevron-right"></i>
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
export default function NewsletterWidget() {
return (
<div className="bg-brand-blue rounded-xl p-8 text-white shadow-hover relative overflow-hidden folder-tab">
<div className="relative z-10">
<div className="blog-widget-title font-bold mb-3">Stay Informed</div>
<p className="text-sm text-brand-light/80 mb-6 leading-relaxed ">
Subscribe to our weekly newsletter for the latest research updates, campus news, and upcoming events.
</p>
<form className="space-y-4">
<div>
<input
type="email"
placeholder="Enter your email address"
className="w-full px-4 py-3 mt-4 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white focus:ring-1 focus:ring-white text-sm"
required
/>
</div>
<button
type="submit"
className="w-full py-3 bg-white text-brand-blue rounded-lg font-bold hover:bg-gray-50 transition-colors text-sm shadow-sm"
>
Subscribe Now
</button>
</form>
<p className="text-xs text-brand-light/60 mt-4 text-center">
By subscribing, you agree to our{" "}
<a href="#" style={{ color: "inherit", textDecoration: "underline" }}>Privacy Policy</a>.
</p>
</div>
{/* Decorative background element */}
<div className="absolute -bottom-12 -right-12 text-white/5 text-9xl">
<i className="far fa-envelope"></i>
</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
const researchers = [
{
name: "Dr. Sarah Jenkins",
field: "Cognitive Linguistics",
bio: "Exploring the intersection of language processing and modern ethical frameworks in digital communication.",
avatar: "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-5.jpg",
},
{
name: "Prof. Marcus Chen",
field: "Economic History",
bio: "Recent recipient of the European Heritage Grant for his work on interwar economic policies.",
avatar: "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-4.jpg",
},
{
name: "Dr. Elena Rostova",
field: "Political Science",
bio: "Leading the new comparative study on post-war democratic institutions across Western Europe.",
avatar: "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-6.jpg",
},
];
export default function ResearcherSpotlight() {
return (
<div className="bg-white rounded-xl border border-ui-border shadow-soft p-6">
<div className="flex items-center justify-between mb-6 pb-4 border-b border-ui-border">
<div className="blog-widget-title font-bold text-ui-text">Researcher Spotlight</div>
<a href="#" className="text-sm text-brand-blue font-medium hover:underline">View All</a>
</div>
<div className="space-y-6">
{researchers.map((r) => (
<div key={r.name} className="flex gap-4 items-start group cursor-pointer">
<img
src={r.avatar}
alt={r.name}
className="w-16 h-16 rounded-lg object-cover border border-ui-border group-hover:border-brand-blue transition-colors"
/>
<div>
<div className="font-bold text-ui-text text-sm group-hover:text-brand-blue transition-colors">{r.name}</div>
<p className="text-xs text-brand-blue font-medium mb-1">{r.field}</p>
<p className="text-xs text-ui-muted line-clamp-2">{r.bio}</p>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
const events = [
{
month: "Nov",
day: "12",
title: "Open Campus Day",
time: "09:00 AM - 04:00 PM",
},
{
month: "Nov",
day: "18",
title: "Guest Lecture: Future of AI in Arts",
time: "06:00 PM - Main Hall",
},
];
export default function UpcomingEvents() {
return (
<div className="bg-white rounded-xl border border-ui-border shadow-soft p-6">
<div className="blog-widget-title font-bold text-ui-text mb-6 pb-4 border-b border-ui-border">
Upcoming Events
</div>
<ul className="space-y-4">
{events.map((e) => (
<li key={e.title} className="flex gap-4">
<div className="flex flex-col items-center justify-center w-12 h-12 bg-brand-light rounded-lg border border-brand-blue/20 shrink-0">
<span className="text-xs font-bold text-brand-blue uppercase">{e.month}</span>
<span className="text-lg font-bold text-ui-text leading-none">{e.day}</span>
</div>
<div>
<div className="text-sm font-bold text-ui-text hover:text-brand-blue cursor-pointer transition-colors">{e.title}</div>
<p className="text-xs text-ui-muted mt-1">
<i className="far fa-clock mr-1"></i>{e.time}
</p>
</div>
</li>
))}
</ul>
</div>
);
}