Skip to content

Commit 283cd7d

Browse files
committed
feat: Introduce Crucible event directory page, integrate a featured event component, and refine event fetching and display logic.
1 parent 8cfa31f commit 283cd7d

File tree

7 files changed

+462
-8
lines changed

7 files changed

+462
-8
lines changed

client/src/App.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ import CrucibleAuth from './pages/crucible/CrucibleAuth';
2222
import ParticipantProtectedRoute from './components/ParticipantProtectedRoute';
2323
import EventDetailPage from './pages/crucible/EventDetailPage';
2424
import ParticipantDashboard from './pages/crucible/ParticipantDashboard';
25+
import CrucibleDirectory from './pages/crucible/CrucibleDirectory';
2526

2627
export default function App() {
2728
const location = useLocation();
2829
const isAdminRoute = location.pathname.startsWith('/admin');
29-
const isCrucibleRoute = location.pathname.startsWith('/crucible');
30+
const isCrucibleRoute = location.pathname.startsWith('/crucible') && location.pathname !== '/crucible';
3031

3132
// Admin routes render without the main site layout
3233
if (isAdminRoute) {
@@ -76,6 +77,7 @@ export default function App() {
7677
<main className="container mx-auto px-6 pt-32">
7778
<Routes>
7879
<Route path="/" element={<HomePage />} />
80+
<Route path="/crucible" element={<CrucibleDirectory />} />
7981
<Route path="/showroom" element={<ShowroomPage />} />
8082
<Route path="/forge" element={<ForgePage />} />
8183
<Route path="/about" element={<CruciblePage />} />

client/src/components/Header.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export default function Header() {
3333

3434
// Array of navigation links to keep the code clean
3535
const navLinks = [
36+
{ href: '/crucible', label: 'Crucible' },
3637
{ href: '/forge', label: 'The Forge' },
3738
{ href: '/showroom', label: 'The Showroom' },
3839
{ href: '/about', label: 'About Us' },

client/src/lib/eventsApi.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export async function getPublishedEvents() {
99
const { data, error } = await supabase
1010
.from('crucible_events')
1111
.select('*')
12-
.in('status', ['open', 'in_progress'])
13-
.order('start_date', { ascending: true });
12+
.in('status', ['open', 'in_progress', 'completed', 'archived'])
13+
.order('start_date', { ascending: false });
1414

1515
if (error) {
1616
console.error('Error fetching published events:', error);

client/src/pages/HomePage.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import StarterKit from '../sections/StarterKit';
1111
import Crucible from '../sections/Crucible';
1212
import ActiveSprints from '../sections/ActiveSprints';
1313

14+
import FeaturedEvent from '../sections/FeaturedEvent';
15+
1416
export default function HomePage() {
1517
useEffect(() => {
1618
const lenis = new Lenis();
@@ -35,6 +37,7 @@ export default function HomePage() {
3537
return (
3638
<>
3739
<Hero />
40+
<FeaturedEvent />
3841
<ActiveSprints />
3942

4043
{/* Wrapper for content */}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { useState, useEffect } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { Link } from 'react-router-dom';
4+
import { Flame, Calendar, ArrowRight, Clock, Box, Layers, Zap, Search, Filter } from 'lucide-react';
5+
import { getPublishedEvents } from '../../lib/eventsApi';
6+
7+
// Reusing the Crucible About content components for consistency
8+
import { Target, Lightbulb, Anvil, Hammer } from 'lucide-react';
9+
10+
export default function CrucibleDirectory() {
11+
const [events, setEvents] = useState([]);
12+
const [loading, setLoading] = useState(true);
13+
const [activeTab, setActiveTab] = useState('live'); // live, upcoming, archive
14+
const [filter, setFilter] = useState('');
15+
16+
useEffect(() => {
17+
// Scroll to top on mount
18+
window.scrollTo(0, 0);
19+
20+
const fetchEvents = async () => {
21+
const { data, success } = await getPublishedEvents();
22+
if (success) {
23+
setEvents(data);
24+
}
25+
setLoading(false);
26+
};
27+
fetchEvents();
28+
}, []);
29+
30+
// Filter Logic
31+
const filteredEvents = events.filter(e => {
32+
const matchesSearch = e.title.toLowerCase().includes(filter.toLowerCase());
33+
34+
let matchesTab = false;
35+
const now = new Date();
36+
const startDate = new Date(e.start_date);
37+
const endDate = new Date(e.end_date);
38+
39+
if (activeTab === 'live') {
40+
// Open or In Progress
41+
matchesTab = e.status === 'open' || e.status === 'in_progress';
42+
} else if (activeTab === 'upcoming') {
43+
// Future start date or announced but not open
44+
matchesTab = startDate > now && e.status !== 'open' && e.status !== 'in_progress';
45+
} else if (activeTab === 'archive') {
46+
// Completed or localized past
47+
matchesTab = e.status === 'completed' || e.status === 'archived' || endDate < now;
48+
}
49+
50+
return matchesSearch && matchesTab;
51+
});
52+
53+
return (
54+
<div className="min-h-screen bg-black pt-20">
55+
{/* --- HERO SECTION --- */}
56+
<section className="relative py-24 px-6 overflow-hidden">
57+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-orange-900/20 via-black to-black opacity-80" />
58+
<div className="max-w-7xl mx-auto relative z-10 text-center">
59+
<motion.div
60+
initial={{ opacity: 0, y: 30 }}
61+
animate={{ opacity: 1, y: 0 }}
62+
transition={{ duration: 0.8 }}
63+
>
64+
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8 backdrop-blur-md">
65+
<Flame className="text-orange-500 w-4 h-4" />
66+
<span className="font-mono text-sm uppercase tracking-widest text-white/60">The Digital Foundry</span>
67+
</div>
68+
<h1 className="text-5xl md:text-7xl font-black text-white font-heading mb-6 tracking-tight">
69+
ENTER THE <span className="text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-red-600">CRUCIBLE</span>
70+
</h1>
71+
<p className="text-xl text-white/50 max-w-2xl mx-auto leading-relaxed mb-10">
72+
Where raw talent is forged into professional excellence. Join our competitive sprints, build real products, and launch your career.
73+
</p>
74+
</motion.div>
75+
</div>
76+
</section>
77+
78+
{/* --- DIRECTORY --- */}
79+
<section className="px-6 py-12 bg-[#0a0a0a] border-t border-white/5 min-h-[80vh]">
80+
<div className="max-w-7xl mx-auto">
81+
82+
{/* Controls */}
83+
<div className="flex flex-col md:flex-row justify-between items-center gap-6 mb-12">
84+
{/* Tabs */}
85+
<div className="flex bg-white/5 p-1 rounded-xl border border-white/5">
86+
{['live', 'upcoming', 'archive'].map(tab => (
87+
<button
88+
key={tab}
89+
onClick={() => setActiveTab(tab)}
90+
className={`px-6 py-2.5 rounded-lg text-sm font-bold uppercase tracking-wider transition-all ${activeTab === tab
91+
? 'bg-orange-500 text-white shadow-lg shadow-orange-500/20'
92+
: 'text-white/40 hover:text-white hover:bg-white/5'
93+
}`}
94+
>
95+
{tab}
96+
</button>
97+
))}
98+
</div>
99+
100+
{/* Search */}
101+
<div className="relative w-full md:w-80">
102+
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
103+
<input
104+
type="text"
105+
placeholder="Search events..."
106+
value={filter}
107+
onChange={e => setFilter(e.target.value)}
108+
className="w-full bg-white/5 border border-white/5 rounded-xl py-3 pl-10 pr-4 text-white placeholder:text-white/20 focus:outline-none focus:border-orange-500/50 transition-colors"
109+
/>
110+
</div>
111+
</div>
112+
113+
{/* Grid */}
114+
{loading ? (
115+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
116+
{[1, 2, 3].map(i => (
117+
<div key={i} className="h-96 rounded-2xl bg-white/5 animate-pulse" />
118+
))}
119+
</div>
120+
) : filteredEvents.length === 0 ? (
121+
<div className="text-center py-32 border border-white/5 rounded-3xl bg-white/[0.02]">
122+
<Box className="w-16 h-16 text-white/10 mx-auto mb-4" />
123+
<h3 className="text-2xl font-bold text-white mb-2">No events found</h3>
124+
<p className="text-white/40">Check back later for new sprints.</p>
125+
</div>
126+
) : (
127+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
128+
{filteredEvents.map((event, idx) => (
129+
<DirectoryCard key={event.id} event={event} index={idx} />
130+
))}
131+
</div>
132+
)}
133+
134+
</div>
135+
</section>
136+
137+
{/* --- ABOUT SECTION (Reused Content) --- */}
138+
<AboutSection />
139+
</div>
140+
);
141+
}
142+
143+
function DirectoryCard({ event, index }) {
144+
const isRegistrationOpen = event.status === 'open';
145+
const isCompleted = event.status === 'completed' || event.status === 'archived';
146+
147+
return (
148+
<motion.div
149+
initial={{ opacity: 0, y: 20 }}
150+
animate={{ opacity: 1, y: 0 }}
151+
transition={{ delay: index * 0.05 }}
152+
className="group h-full"
153+
>
154+
<Link to={`/crucible/${event.slug}`} className="block h-full relative">
155+
<div className="h-full bg-[#0c0c0c] border border-white/10 rounded-2xl overflow-hidden hover:border-orange-500/50 transition-all duration-300 group-hover:-translate-y-1 shadow-2xl shadow-black/50">
156+
157+
{/* Poster */}
158+
<div className="aspect-video relative overflow-hidden bg-white/5">
159+
{event.poster_url ? (
160+
<img src={event.poster_url} alt={event.title} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" />
161+
) : (
162+
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-white/5 to-white/10">
163+
<Flame className="w-12 h-12 text-white/10 group-hover:text-orange-500/20 transition-colors" />
164+
</div>
165+
)}
166+
167+
{/* Status Tag */}
168+
<div className="absolute top-4 left-4">
169+
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider backdrop-blur-md border ${isRegistrationOpen ? 'bg-green-500/20 text-green-400 border-green-500/30' :
170+
isCompleted ? 'bg-white/10 text-white/50 border-white/10' :
171+
'bg-blue-500/20 text-blue-400 border-blue-500/30'
172+
}`}>
173+
{isRegistrationOpen && <span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />}
174+
{event.status.replace('_', ' ')}
175+
</span>
176+
</div>
177+
</div>
178+
179+
{/* Body */}
180+
<div className="p-6">
181+
<h3 className="text-xl font-bold text-white mb-2 group-hover:text-orange-500 transition-colors">{event.title}</h3>
182+
<div className="flex items-center gap-4 text-sm text-white/40 mb-6">
183+
<div className="flex items-center gap-1.5">
184+
<Calendar size={14} />
185+
<span>{new Date(event.start_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</span>
186+
</div>
187+
<div className="flex items-center gap-1.5">
188+
<Clock size={14} />
189+
<span>Sprint</span>
190+
</div>
191+
</div>
192+
193+
{/* Footer Action */}
194+
<div className="flex items-center justify-between mt-auto">
195+
<span className="text-sm font-bold text-white/50 group-hover:text-white transition-colors">Details</span>
196+
<div className={`p-2 rounded-full transition-colors ${isRegistrationOpen ? 'bg-orange-500 text-white' : 'bg-white/5 text-white/50 group-hover:bg-white/10 group-hover:text-white'}`}>
197+
<ArrowRight size={16} />
198+
</div>
199+
</div>
200+
</div>
201+
</div>
202+
</Link>
203+
</motion.div>
204+
);
205+
}
206+
207+
function AboutSection() {
208+
return (
209+
<section className="py-24 px-6 border-t border-white/5 relative bg-[#050505] overflow-hidden">
210+
{/* Decorative BG */}
211+
<div className="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none bg-[radial-gradient(circle_at_bottom_left,_var(--tw-gradient-stops))] from-orange-900 via-transparent to-transparent"></div>
212+
213+
<div className="max-w-7xl mx-auto">
214+
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
215+
<div>
216+
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">Why Join The Crucible?</h2>
217+
<p className="text-white/60 text-lg leading-relaxed mb-8">
218+
Enclope isn't just a dev shop; it's a talent accelerator. The Crucible is our dedicated program for students and early-career developers to prove their mettle.
219+
</p>
220+
221+
<div className="space-y-6">
222+
{[
223+
{ icon: Anvil, title: "Build Real Products", desc: "No toy apps. Work on challenges that mirror real-world industry demands." },
224+
{ icon: Hammer, title: "Mentorship", desc: "Get code reviews and guidance from senior engineers at Enclope." },
225+
{ icon: Zap, title: "Career Velocity", desc: "Top performers get fast-tracked for internships and roles." }
226+
].map((item, i) => (
227+
<div key={i} className="flex gap-4">
228+
<div className="p-3 rounded-lg bg-white/5 h-fit">
229+
<item.icon className="w-6 h-6 text-orange-500" />
230+
</div>
231+
<div>
232+
<h4 className="text-white font-bold mb-1">{item.title}</h4>
233+
<p className="text-white/50 text-sm">{item.desc}</p>
234+
</div>
235+
</div>
236+
))}
237+
</div>
238+
</div>
239+
240+
<div className="relative">
241+
<div className="grid grid-cols-2 gap-4">
242+
<div className="space-y-4 pt-8">
243+
<div className="aspect-[4/5] rounded-2xl bg-white/5 overflow-hidden border border-white/5">
244+
<img src="https://images.unsplash.com/photo-1531403009284-440f080d1e12?q=80&w=2070&auto=format&fit=crop" className="w-full h-full object-cover opacity-60 hover:opacity-100 transition-opacity duration-700" alt="Teamwork" />
245+
</div>
246+
</div>
247+
<div className="space-y-4">
248+
<div className="aspect-[4/5] rounded-2xl bg-white/5 overflow-hidden border border-white/5">
249+
<img src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=2070&auto=format&fit=crop" className="w-full h-full object-cover opacity-60 hover:opacity-100 transition-opacity duration-700" alt="Coding" />
250+
</div>
251+
</div>
252+
</div>
253+
</div>
254+
</div>
255+
</div>
256+
</section>
257+
);
258+
}

client/src/sections/ActiveSprints.jsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export default function ActiveSprints() {
1212
const fetchEvents = async () => {
1313
const { data, success } = await getPublishedEvents();
1414
if (success) {
15-
setEvents(data);
15+
// Only show active sprints (Open or In Progress)
16+
const activeEvents = data.filter(e => e.status === 'open' || e.status === 'in_progress');
17+
setEvents(activeEvents);
1618
}
1719
setLoading(false);
1820
};
@@ -105,8 +107,8 @@ function SprintCard({ event, index }) {
105107
{/* Status Badge */}
106108
<div className="absolute top-4 left-4">
107109
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider backdrop-blur-md ${isRegistrationOpen
108-
? 'bg-green-500/20 text-green-400 border border-green-500/30'
109-
: 'bg-white/10 text-white/60 border border-white/10'
110+
? 'bg-green-500/20 text-green-400 border border-green-500/30'
111+
: 'bg-white/10 text-white/60 border border-white/10'
110112
}`}>
111113
{isRegistrationOpen ? (
112114
<>
@@ -155,8 +157,16 @@ function SprintCard({ event, index }) {
155157
</div>
156158

157159
{/* CTA */}
158-
<div className="flex items-center gap-2 text-sm font-bold text-orange-500 group-hover:gap-3 transition-all">
159-
View Details <ArrowRight className="w-4 h-4" />
160+
<div className="flex items-center justify-between mt-auto">
161+
<div className="flex items-center gap-2 text-sm font-bold text-white group-hover:text-orange-500 transition-colors">
162+
View Details <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
163+
</div>
164+
165+
{isRegistrationOpen && (
166+
<span className="bg-orange-500 hover:bg-orange-600 text-white text-xs font-bold uppercase tracking-wider px-4 py-2 rounded-lg transition-colors shadow-lg shadow-orange-500/20">
167+
Register Now
168+
</span>
169+
)}
160170
</div>
161171
</div>
162172
</div>

0 commit comments

Comments
 (0)