Skip to content

Commit f8dfc98

Browse files
authored
Merge pull request #2973 from tekdi/feat-landing-page
Feat landing page to release [code sync]
2 parents 65d606c + 5b0efa7 commit f8dfc98

14 files changed

Lines changed: 1849 additions & 527 deletions

File tree

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
'use client';
2+
3+
import { useParams, useRouter } from 'next/navigation';
4+
import { useEffect, useState } from 'react';
5+
import {
6+
Box,
7+
Button,
8+
CircularProgress,
9+
Container,
10+
Typography,
11+
} from '@mui/material';
12+
import Header from '@learner/components/Header/Header';
13+
import { getTenantInfo } from '@learner/utils/API/ProgramService';
14+
import { useTranslation } from '@shared-lib';
15+
import EnrolModal from '@learner/components/EnrolModal/EnrolModal';
16+
17+
interface Program {
18+
tenantId: string;
19+
name: string;
20+
description?: string;
21+
type?: string;
22+
params?: {
23+
uiConfig?: { [key: string]: any };
24+
[key: string]: any;
25+
} | null;
26+
programImages?: (
27+
| string
28+
| { label?: string; description?: string; [key: string]: any }
29+
)[];
30+
}
31+
32+
function getFirstImage(program: Program): string {
33+
const item = program.programImages?.[0];
34+
if (!item) return '/images/default.png';
35+
return typeof item === 'string'
36+
? item
37+
: (item as any).description || '/images/default.png';
38+
}
39+
40+
export default function ProgramDetailPage() {
41+
const params = useParams();
42+
const router = useRouter();
43+
const { t } = useTranslation();
44+
const programName = decodeURIComponent(params?.programName as string);
45+
46+
const [program, setProgram] = useState<Program | null>(null);
47+
const [loading, setLoading] = useState(true);
48+
const [notFound, setNotFound] = useState(false);
49+
const [enrolModalOpen, setEnrolModalOpen] = useState(false);
50+
51+
useEffect(() => {
52+
if (!programName) return;
53+
const fetchProgram = async () => {
54+
try {
55+
const res = await getTenantInfo();
56+
const all: Program[] = res?.result || [];
57+
const found = all.find(
58+
(p) => p.name.toLowerCase() === programName.toLowerCase()
59+
);
60+
if (found) {
61+
setProgram(found);
62+
} else {
63+
setNotFound(true);
64+
}
65+
} catch {
66+
setNotFound(true);
67+
} finally {
68+
setLoading(false);
69+
}
70+
};
71+
fetchProgram();
72+
}, [programName]);
73+
74+
const handleEnrolNow = () => {
75+
setEnrolModalOpen(true);
76+
};
77+
78+
const handleLogin = () => {
79+
router.push(`/login?tenantId=${program?.tenantId}`);
80+
};
81+
82+
if (loading) {
83+
return (
84+
<>
85+
<Header isLogin />
86+
<Box
87+
sx={{
88+
display: 'flex',
89+
justifyContent: 'center',
90+
alignItems: 'center',
91+
minHeight: '60vh',
92+
}}
93+
>
94+
<CircularProgress sx={{ color: '#FDBE16' }} />
95+
</Box>
96+
</>
97+
);
98+
}
99+
100+
if (notFound || !program) {
101+
return (
102+
<>
103+
<Header isLogin />
104+
<Box
105+
sx={{
106+
display: 'flex',
107+
flexDirection: 'column',
108+
alignItems: 'center',
109+
justifyContent: 'center',
110+
minHeight: '60vh',
111+
gap: 2,
112+
}}
113+
>
114+
<Typography variant="h5" sx={{ fontFamily: 'Poppins', fontWeight: 600 }}>
115+
{t('LANDING.PROGRAM_NOT_FOUND') || 'Program not found'}
116+
</Typography>
117+
<Button
118+
variant="outlined"
119+
onClick={() => router.push('/landing')}
120+
sx={{ textTransform: 'none', borderColor: '#FDBE16', color: '#1F1B13' }}
121+
>
122+
{t('LANDING.GO_BACK') || 'Go Back'}
123+
</Button>
124+
</Box>
125+
</>
126+
);
127+
}
128+
129+
const heroImage = getFirstImage(program);
130+
131+
return (
132+
<>
133+
<Header />
134+
135+
<Box sx={{ backgroundColor: '#fff', minHeight: '100vh' }}>
136+
{/* ── Hero Banner ── */}
137+
<Box sx={{ position: 'relative', width: '100%' }}>
138+
<Box
139+
component="img"
140+
src={heroImage}
141+
alt={program.name}
142+
onError={(e: React.SyntheticEvent<HTMLImageElement>) => {
143+
e.currentTarget.src = '/images/default.png';
144+
}}
145+
sx={{
146+
width: '100%',
147+
height: { xs: 260, md: 380 },
148+
objectFit: 'cover',
149+
display: 'block',
150+
}}
151+
/>
152+
153+
{/* Program name badge — bottom-left of hero */}
154+
<Box
155+
sx={{
156+
position: 'absolute',
157+
bottom: 0,
158+
left: 0,
159+
backgroundColor: '#FDBE16',
160+
px: 3,
161+
py: 1.2,
162+
display: 'flex',
163+
alignItems: 'center',
164+
}}
165+
>
166+
<Typography
167+
sx={{
168+
fontFamily: 'Poppins',
169+
fontWeight: 700,
170+
fontSize: { xs: '13px', md: '16px' },
171+
letterSpacing: '0.08em',
172+
color: '#1F1B13',
173+
textTransform: 'uppercase',
174+
}}
175+
>
176+
{program.name}
177+
</Typography>
178+
</Box>
179+
</Box>
180+
181+
{/* ── Body ── */}
182+
<Box sx={{ backgroundColor: '#FFFDF7', py: { xs: 4, md: 6 } }}>
183+
<Container maxWidth="lg" sx={{ px: { xs: 2, md: 3 } }}>
184+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
185+
186+
{/* Enrol card — centred, fixed width */}
187+
<Box
188+
sx={{
189+
mx: 'auto',
190+
width: '100%',
191+
maxWidth: 480,
192+
border: '1px solid #E0E0E0',
193+
borderRadius: '12px',
194+
p: { xs: 3, md: 4 },
195+
display: 'flex',
196+
flexDirection: 'column',
197+
gap: 2,
198+
backgroundColor: '#fff',
199+
boxShadow: '0px 4px 16px rgba(0,0,0,0.08)',
200+
}}
201+
>
202+
<Typography
203+
sx={{
204+
fontFamily: 'Poppins',
205+
fontWeight: 700,
206+
fontSize: { xs: '16px', md: '18px' },
207+
color: '#1F1B13',
208+
textAlign: 'center',
209+
lineHeight: 1.4,
210+
}}
211+
>
212+
{`${t('LANDING.READY_TO_JOIN') || 'Ready to join'} ${program.name}?`}
213+
</Typography>
214+
215+
<Button
216+
fullWidth
217+
variant="contained"
218+
onClick={handleEnrolNow}
219+
disableElevation
220+
sx={{
221+
backgroundColor: '#FDBE16',
222+
color: '#1F1B13',
223+
fontFamily: 'Poppins',
224+
fontWeight: 700,
225+
fontSize: '15px',
226+
textTransform: 'none',
227+
borderRadius: '8px',
228+
py: 1.4,
229+
'&:hover': { backgroundColor: '#f0b000' },
230+
}}
231+
>
232+
{t('LANDING.ENROL_NOW') || 'Enrol Now'}
233+
</Button>
234+
235+
<Button
236+
fullWidth
237+
variant="outlined"
238+
onClick={handleLogin}
239+
disableElevation
240+
sx={{
241+
borderColor: '#D0D0D0',
242+
color: '#1F1B13',
243+
fontFamily: 'Poppins',
244+
fontWeight: 500,
245+
fontSize: '14px',
246+
textTransform: 'none',
247+
borderRadius: '8px',
248+
py: 1.2,
249+
'&:hover': { borderColor: '#FDBE16', backgroundColor: 'transparent' },
250+
}}
251+
>
252+
{t('LANDING.ALREADY_ENROLLED_LOGIN') || 'Already enrolled? Log in'}
253+
</Button>
254+
255+
<Typography
256+
variant="caption"
257+
sx={{
258+
textAlign: 'center',
259+
color: '#888',
260+
fontFamily: 'Poppins',
261+
lineHeight: 1.5,
262+
fontSize: '12px',
263+
}}
264+
>
265+
{t('LANDING.ENROL_HINT') ||
266+
"New or already enrolled — just enter your phone number. We'll guide you through."}
267+
</Typography>
268+
</Box>
269+
270+
{/* About the Program */}
271+
<Box>
272+
<Box
273+
sx={{
274+
display: 'inline-flex',
275+
backgroundColor: '#FDBE16',
276+
px: 2.5,
277+
py: 0.8,
278+
mb: 3,
279+
}}
280+
>
281+
<Typography
282+
sx={{
283+
fontFamily: 'Poppins',
284+
fontWeight: 700,
285+
fontSize: '13px',
286+
letterSpacing: '0.08em',
287+
color: '#1F1B13',
288+
textTransform: 'uppercase',
289+
}}
290+
>
291+
{t('LANDING.ABOUT_THE_PROGRAM') || 'About the Program'}
292+
</Typography>
293+
</Box>
294+
295+
<Typography
296+
variant="body1"
297+
sx={{
298+
fontFamily: 'Poppins',
299+
fontSize: { xs: '14px', md: '15px' },
300+
lineHeight: 1.9,
301+
color: '#3D6B5E',
302+
whiteSpace: 'pre-line',
303+
}}
304+
>
305+
{program.description || ''}
306+
</Typography>
307+
</Box>
308+
309+
{/* Contact */}
310+
<Box>
311+
<Box
312+
sx={{
313+
display: 'inline-flex',
314+
backgroundColor: '#FDBE16',
315+
px: 2.5,
316+
py: 0.8,
317+
mb: 3,
318+
}}
319+
>
320+
<Typography
321+
sx={{
322+
fontFamily: 'Poppins',
323+
fontWeight: 700,
324+
fontSize: '13px',
325+
letterSpacing: '0.08em',
326+
color: '#1F1B13',
327+
textTransform: 'uppercase',
328+
}}
329+
>
330+
{t('LANDING.CONTACT') || 'Contact'}
331+
</Typography>
332+
</Box>
333+
334+
<Typography
335+
variant="body1"
336+
sx={{
337+
fontFamily: 'Poppins',
338+
fontSize: { xs: '14px', md: '15px' },
339+
color: '#3D6B5E',
340+
lineHeight: 1.9,
341+
}}
342+
>
343+
{t('LANDING.WRITE_TO_US') || 'Write to us at'}{' '}
344+
<Box
345+
component="a"
346+
href={`mailto:${program.params?.uiConfig?.contactEmail || program.params?.uiConfig?.contact || 'info@pratham.org'}`}
347+
sx={{ color: '#0D599E', textDecoration: 'underline' }}
348+
>
349+
{program.params?.uiConfig?.contactEmail || program.params?.uiConfig?.contact || 'info@pratham.org'}
350+
</Box>
351+
</Typography>
352+
</Box>
353+
354+
</Box>
355+
</Container>
356+
</Box>
357+
</Box>
358+
359+
<EnrolModal
360+
open={enrolModalOpen}
361+
onClose={() => setEnrolModalOpen(false)}
362+
programName={program.name}
363+
tenantId={program.tenantId}
364+
/>
365+
</>
366+
);
367+
}

0 commit comments

Comments
 (0)