Skip to content

Commit 06f5f47

Browse files
committed
feat: landscape project page
1 parent 333da1a commit 06f5f47

File tree

3 files changed

+369
-1
lines changed

3 files changed

+369
-1
lines changed

src/components/Layout.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ const Layout = ({
6161
projectStyle === 'stylish' ||
6262
projectStyle === 'editorial' ||
6363
projectStyle === 'minimal-modern' ||
64-
projectStyle === 'museum';
64+
projectStyle === 'museum' ||
65+
projectStyle === 'landscape';
6566
// Check if we are inside a specific app (but not the apps listing page)
6667
const isAppDetail =
6768
location.pathname.startsWith('/apps/') && location.pathname !== '/apps/';

src/components/ProjectRouteHandler.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const LuxeProjectDetailPage = lazy(
1919
() => import('../pages/luxe-views/LuxeProjectDetailPage'),
2020
);
2121
const BentoProjectPage = lazy(() => import('../pages/project-pages/BentoProjectPage'));
22+
const LandscapeProjectPage = lazy(
23+
() => import('../pages/project-pages/LandscapeProjectPage'),
24+
);
2225

2326
const ProjectRouteHandler = () => {
2427
const { slug } = useParams();
@@ -80,6 +83,14 @@ const ProjectRouteHandler = () => {
8083
);
8184
}
8285

86+
if (projectStyle === 'landscape') {
87+
return (
88+
<Suspense fallback={<Loading />}>
89+
<LandscapeProjectPage />
90+
</Suspense>
91+
);
92+
}
93+
8394
// Fallback to theme based routing if style is default
8495
if (fezcodexTheme === 'luxe') {
8596
return (
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useParams, Link } from 'react-router-dom';
3+
import { motion } from 'framer-motion';
4+
import { useProjects } from '../../utils/projectParser';
5+
import Seo from '../../components/Seo';
6+
import MarkdownContent from '../../components/MarkdownContent';
7+
import * as Icons from '@phosphor-icons/react';
8+
import {
9+
CaretRightIcon,
10+
GlobeHemisphereWestIcon,
11+
BuildingsIcon,
12+
} from '@phosphor-icons/react';
13+
14+
// Helper to render icon by name string
15+
const DynamicIcon = ({ name, ...props }) => {
16+
if (!name) return null;
17+
// Phosphor icons in this project use 'Icon' suffix
18+
const IconComponent = Icons[`${name}Icon`];
19+
if (!IconComponent) return null;
20+
return <IconComponent {...props} />;
21+
};
22+
23+
const LandscapeProjectPage = () => {
24+
const { slug } = useParams();
25+
const { projects, loading: loadingProjects } = useProjects();
26+
const [sections, setSections] = useState([]);
27+
const [landingConfig, setLandingConfig] = useState(null);
28+
const [navbarConfig, setNavbarConfig] = useState({ logo: {}, links: [] });
29+
const [sectionContent, setSectionContent] = useState({});
30+
const [activeSection, setActiveSection] = useState(0);
31+
32+
const project = projects.find((p) => p.slug === slug);
33+
34+
// Fetch Configurations
35+
useEffect(() => {
36+
const fetchConfigs = async () => {
37+
try {
38+
const [landingRes, navbarRes, sectionsRes] = await Promise.all([
39+
fetch(`/projects/${slug}/details/landing.txt`),
40+
fetch(`/projects/${slug}/details/navbar.txt`),
41+
fetch(`/projects/${slug}/details/sections.txt`),
42+
]);
43+
44+
if (landingRes.ok) {
45+
const lData = await landingRes.json();
46+
setLandingConfig(lData);
47+
}
48+
if (navbarRes.ok) {
49+
const nData = await navbarRes.json();
50+
setNavbarConfig(nData);
51+
}
52+
if (sectionsRes.ok) {
53+
const sectionsData = await sectionsRes.json();
54+
setSections(sectionsData);
55+
56+
// Fetch content for sections after loading sections config
57+
const contentMap = {};
58+
await Promise.all(
59+
sectionsData.map(async (section) => {
60+
try {
61+
const response = await fetch(
62+
`/projects/${slug}/details/${section.file}`,
63+
);
64+
if (response.ok) {
65+
const text = await response.text();
66+
contentMap[section.id] = text;
67+
}
68+
} catch (error) {
69+
console.error(`Failed to fetch content for ${section.id}`, error);
70+
}
71+
}),
72+
);
73+
setSectionContent(contentMap);
74+
}
75+
} catch (error) {
76+
console.error('Failed to load project configurations', error);
77+
}
78+
};
79+
80+
if (slug) fetchConfigs();
81+
}, [slug]);
82+
83+
// Handle scroll to track active section for the indicator
84+
const handleScroll = (e) => {
85+
const scrollPos = e.target.scrollTop;
86+
const windowHeight = window.innerHeight;
87+
if (windowHeight === 0) return;
88+
const index = Math.round(scrollPos / windowHeight);
89+
setActiveSection(index);
90+
};
91+
92+
if (loadingProjects || !project || !landingConfig) {
93+
return (
94+
<div className="min-h-screen bg-[#050505] flex items-center justify-center text-white font-mono uppercase tracking-widest text-xs">
95+
<span className="animate-pulse">Initializing System...</span>
96+
</div>
97+
);
98+
}
99+
100+
const totalSections = sections.length + 1; // +1 for Hero
101+
102+
return (
103+
<div className="bg-[#050505] text-white h-screen overflow-y-scroll snap-y snap-mandatory font-instr-sans selection:bg-rose-500/30 selection:text-rose-200" onScroll={handleScroll}>
104+
<Seo
105+
title={`${project.title} | Fezcodex`}
106+
description={project.shortDescription}
107+
image={project.image}
108+
keywords={project.tags}
109+
/>
110+
111+
{/* Dot Indicator */}
112+
<div className="fixed right-8 top-1/2 -translate-y-1/2 z-50 flex flex-col gap-4">
113+
{Array.from({ length: totalSections }).map((_, i) => (
114+
<div
115+
key={i}
116+
className={`w-1 h-8 transition-colors duration-200 ${
117+
activeSection === i ? 'bg-rose-500' : 'bg-white/10'
118+
}`}
119+
/>
120+
))}
121+
</div>
122+
123+
{/* Navigation Bar (Customizable) */}
124+
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 py-6 md:px-12 backdrop-blur-sm mix-blend-difference">
125+
<Link
126+
to="/projects"
127+
className="text-xl font-instr-serif font-bold tracking-tight flex items-center gap-2 group text-white no-underline"
128+
>
129+
{navbarConfig.logo?.icon && (
130+
<DynamicIcon
131+
name={navbarConfig.logo.icon}
132+
className="group-hover:rotate-12 transition-transform"
133+
/>
134+
)}
135+
{navbarConfig.logo?.label || landingConfig.title}
136+
</Link>
137+
<div className="hidden md:flex items-center gap-8 text-sm font-medium opacity-80 font-instr-sans uppercase tracking-widest">
138+
{navbarConfig.links?.map((item, index) => (
139+
item.link.startsWith('/') ? (
140+
<Link key={index} to={item.link} className="hover:opacity-100 transition-opacity text-white no-underline">
141+
{item.label}
142+
</Link>
143+
) : (
144+
<a key={index} href={item.link} className="hover:opacity-100 transition-opacity text-white no-underline">
145+
{item.label}
146+
</a>
147+
)
148+
))}
149+
</div>
150+
{navbarConfig.cta && (
151+
<a
152+
href={navbarConfig.cta.link}
153+
target={navbarConfig.cta.link.startsWith('http') ? "_blank" : "_self"}
154+
rel={navbarConfig.cta.link.startsWith('http') ? "noopener noreferrer" : ""}
155+
className="hidden md:flex items-center gap-2 border border-white/30 px-6 py-2 rounded text-xs font-instr-sans uppercase tracking-widest hover:bg-white hover:text-black transition-colors no-underline text-white"
156+
>
157+
{navbarConfig.cta.icon && (
158+
<DynamicIcon name={navbarConfig.cta.icon} size={16} />
159+
)}
160+
{navbarConfig.cta.label}
161+
</a>
162+
)}
163+
</nav>
164+
165+
{/* Hero Section */}
166+
<section className="relative h-screen w-full flex items-center overflow-hidden snap-start snap-always">
167+
{/* Background Image */}
168+
<div className="absolute inset-0 z-0">
169+
<img
170+
src={`/projects/${slug}/maps/${landingConfig.backgroundImage || (sections.length > 0 ? sections[0].image : project.image)}`}
171+
alt="Hero Background"
172+
className="w-full h-full object-cover filter brightness-[0.6] saturate-[0.8]"
173+
/>
174+
<div className="absolute inset-0 bg-gradient-to-r from-black/60 via-black/30 to-transparent" />
175+
</div>
176+
177+
{/* Content */}
178+
<div className="relative z-10 container mx-auto px-6 md:px-12 grid grid-cols-1 md:grid-cols-2 gap-12 h-full items-center">
179+
<div className="pt-20">
180+
<h1 className="text-5xl md:text-7xl lg:text-9xl font-instr-serif font-medium tracking-tight leading-[0.9] mb-12">
181+
{landingConfig.title}: <br />
182+
<span className="text-rose-400">{landingConfig.subtitle}</span>
183+
</h1>
184+
185+
<motion.div
186+
initial={{ opacity: 0, y: 30 }}
187+
animate={{ opacity: 1, y: 0 }}
188+
transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }}
189+
className="w-full max-w-2xl bg-black/40 backdrop-blur-2xl p-10 md:p-12 border border-white/10 shadow-2xl relative"
190+
>
191+
{/* Industrial Accents */}
192+
<div className="absolute -top-px -left-px w-8 h-px bg-rose-500/50" />
193+
<div className="absolute -top-px -left-px w-px h-8 bg-rose-500/50" />
194+
<div className="absolute -bottom-px -right-px w-8 h-px bg-rose-500/50" />
195+
<div className="absolute -bottom-px -right-px w-px h-8 bg-rose-500/50" />
196+
197+
<p className="text-xl md:text-2xl text-gray-200 leading-relaxed font-garamond italic mb-10">
198+
{landingConfig.description}
199+
</p>
200+
201+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-10 border-t border-white/5 pt-8">
202+
{landingConfig.credits &&
203+
landingConfig.credits.map((credit, index) => (
204+
<div key={index}>
205+
<span className="text-[10px] font-instr-sans uppercase tracking-[0.3em] text-white/40 block mb-1">
206+
{credit.role}
207+
</span>
208+
<span className="text-sm font-instr-serif italic text-white/80">
209+
{credit.name}
210+
</span>
211+
</div>
212+
))}
213+
</div>
214+
215+
<div className="flex flex-wrap items-center gap-4 pt-8 border-t border-white/5">
216+
{landingConfig.buttons &&
217+
landingConfig.buttons.map((btn, index) => (
218+
<a
219+
key={index}
220+
href={btn.link}
221+
target={btn.link.startsWith('http') ? '_blank' : '_self'}
222+
rel={btn.link.startsWith('http') ? 'noopener noreferrer' : ''}
223+
className={`${
224+
btn.type === 'primary'
225+
? 'bg-white text-black hover:bg-rose-400'
226+
: 'border border-white/30 hover:bg-white/10 backdrop-blur-md text-white'
227+
} px-8 py-4 rounded font-instr-sans font-bold text-xs uppercase tracking-[0.2em] transition-colors flex items-center gap-2 no-underline`}
228+
>
229+
{btn.label}
230+
</a>
231+
))}
232+
</div>
233+
</motion.div>
234+
</div>
235+
</div>
236+
237+
{/* Bottom Stats / Logos */}
238+
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-12 z-20 flex flex-col md:flex-row items-end justify-between gap-8 pointer-events-none">
239+
<div className="flex items-center gap-8 opacity-50 grayscale pointer-events-auto font-instr-sans uppercase tracking-widest text-[10px]">
240+
<span className="font-bold flex items-center gap-2">
241+
<GlobeHemisphereWestIcon size={18} /> Aether
242+
</span>
243+
<span className="font-bold flex items-center gap-2">
244+
<BuildingsIcon size={18} /> UrbanOS
245+
</span>
246+
</div>
247+
248+
<div className="flex items-center gap-12 bg-black/20 backdrop-blur-md p-8 rounded-t-2xl border-t border-x border-white/10">
249+
{landingConfig.stats && landingConfig.stats.map((stat, index) => (
250+
<React.Fragment key={index}>
251+
<div>
252+
<div className="text-4xl font-instr-serif font-bold">{stat.value}</div>
253+
<div className="text-[10px] font-instr-sans text-gray-400 uppercase tracking-[0.2em] mt-2">
254+
{stat.label}
255+
</div>
256+
</div>
257+
{index < landingConfig.stats.length - 1 && <div className="w-px h-12 bg-white/10" />}
258+
</React.Fragment>
259+
))}
260+
</div>
261+
</div>
262+
263+
{/* Decorative Grid Lines */}
264+
<div className="absolute top-0 left-12 bottom-0 w-px bg-white/10 hidden md:block" />
265+
<div className="absolute top-0 right-12 bottom-0 w-px bg-white/10 hidden md:block" />
266+
<div className="absolute top-24 left-0 right-0 h-px bg-white/10 hidden md:block" />
267+
<div className="absolute bottom-32 left-0 right-0 h-px bg-white/10 hidden md:block" />
268+
</section>
269+
270+
{/* Scrollable Detail Sections */}
271+
{sections.map((section, index) => (
272+
<Section
273+
key={section.id}
274+
section={section}
275+
slug={slug}
276+
content={sectionContent[section.id]}
277+
index={index}
278+
/>
279+
))}
280+
</div>
281+
);
282+
};
283+
284+
const Section = ({ section, slug, content, index }) => {
285+
const isEven = index % 2 === 0;
286+
287+
return (
288+
<section className="relative h-screen w-full flex items-center overflow-hidden border-t border-white/5 snap-start snap-always">
289+
{/* Background Wallpaper - Clear and Full Screen */}
290+
<div className="absolute inset-0 z-0">
291+
<img
292+
src={`/projects/${slug}/maps/${section.image}`}
293+
alt={section.title}
294+
className="w-full h-full object-cover transition-transform duration-1000"
295+
/>
296+
{/* Subtle gradient to ensure text readability without killing the wallpaper */}
297+
<div className={`absolute inset-0 bg-gradient-to-b from-black/40 via-transparent to-black/60`} />
298+
<div className={`absolute inset-0 bg-black/20`} />
299+
</div>
300+
301+
<div className="container mx-auto px-6 md:px-12 relative z-10">
302+
<div
303+
className={`flex w-full ${isEven ? 'justify-start' : 'justify-end'}`}
304+
>
305+
{/* Detail Box with Negative Background (Glassmorphism / Industrial) */}
306+
<motion.div
307+
initial={{ opacity: 0, y: 30 }}
308+
whileInView={{ opacity: 1, y: 0 }}
309+
viewport={{ once: true, margin: '-100px' }}
310+
transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }}
311+
className="w-full max-w-2xl bg-black/40 backdrop-blur-2xl p-10 md:p-16 border border-white/10 shadow-2xl relative group"
312+
>
313+
{/* Industrial Accents */}
314+
<div className="absolute -top-px -left-px w-8 h-px bg-rose-500/50" />
315+
<div className="absolute -top-px -left-px w-px h-8 bg-rose-500/50" />
316+
<div className="absolute -bottom-px -right-px w-8 h-px bg-rose-500/50" />
317+
<div className="absolute -bottom-px -right-px w-px h-8 bg-rose-500/50" />
318+
319+
{/* Component Identifier */}
320+
<div className="mb-6 flex items-center gap-3">
321+
<span className="text-[10px] font-instr-sans font-bold uppercase tracking-[0.5em] text-rose-400">
322+
System Module / 0{index + 1}
323+
</span>
324+
<div className="h-px w-12 bg-rose-500/30" />
325+
</div>
326+
327+
<h2 className="text-4xl md:text-6xl font-instr-serif font-medium tracking-tight text-white mb-8 leading-tight">
328+
{section.title}
329+
</h2>
330+
331+
<div className="prose prose-invert prose-xl font-garamond
332+
prose-p:text-gray-200 prose-p:leading-relaxed prose-p:font-light
333+
prose-li:text-gray-300 prose-strong:text-rose-400 prose-strong:font-medium
334+
prose-h2:text-2xl prose-h2:font-instr-serif prose-h2:mb-4 prose-h2:mt-8
335+
prose-h3:text-xl prose-h3:font-instr-serif prose-h3:text-rose-500">
336+
<MarkdownContent content={content || ''} />
337+
</div>
338+
339+
<div className="mt-12 pt-8 border-t border-white/5 flex items-center justify-between">
340+
<div className="text-[10px] font-mono text-white/20 uppercase tracking-widest">
341+
ID: ###-{section.id.toUpperCase()}-###
342+
</div>
343+
</div>
344+
</motion.div>
345+
</div>
346+
</div>
347+
348+
{/* Side Label */}
349+
<div className={`absolute top-1/2 -translate-y-1/2 hidden xl:block mix-blend-difference opacity-30 font-instr-sans text-[10px] uppercase tracking-[1em] [writing-mode:vertical-lr] ${isEven ? 'right-12 rotate-180' : 'left-12'}`}>
350+
Module Archive / 0{index + 1}
351+
</div>
352+
</section>
353+
);
354+
};
355+
356+
export default LandscapeProjectPage;

0 commit comments

Comments
 (0)