Skip to content

Commit 616dc32

Browse files
adamintCopilot
andcommitted
Redesign homepage to Mission Control split layout
- Hero: split into left content + right side panel with 6 mini-cards (Playground, Gallery, Concept Map, Curriculum, What's New, Learning Tracks) - Remove separate quick links bar (merged into hero side panel) - CTA buttons: purple primary + teal secondary (replaces gold) - New slogan: 'Learn Aspire · Build real apps · Level up your stack' - All content, navigation, auth logic, and accessibility preserved Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5a3f067 commit 616dc32

2 files changed

Lines changed: 90 additions & 212 deletions

File tree

AspireAcademy.Api.Tests/E2E/HomePageImprovementsTests.cs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ public async Task HomePage_QuickLinks_Visible()
1717
await page.GotoAsync(fixture.WebBaseUrl + "/");
1818
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
1919

20-
// Verify quick links bar exists with expected links
21-
var playgroundLink = page.GetByRole(AriaRole.Link, new() { NameRegex = new Regex("playground", RegexOptions.IgnoreCase) });
22-
var galleryLink = page.GetByRole(AriaRole.Link, new() { NameRegex = new Regex("gallery", RegexOptions.IgnoreCase) });
23-
var conceptMapLink = page.GetByRole(AriaRole.Link, new() { NameRegex = new Regex("concept.*map", RegexOptions.IgnoreCase) });
24-
var curriculumLink = page.GetByRole(AriaRole.Link, new() { NameRegex = new Regex("curriculum", RegexOptions.IgnoreCase) });
20+
// Verify hero mini-card links exist with expected links
21+
var playgroundLink = page.GetByTestId("hero-link-playground");
22+
var galleryLink = page.GetByTestId("hero-link-gallery");
23+
var conceptMapLink = page.GetByTestId("hero-link-conceptmap");
24+
var curriculumLink = page.GetByTestId("hero-link-curriculum");
25+
var whatsNewLink = page.GetByTestId("hero-link-whatsnew");
26+
var tracksLink = page.GetByTestId("hero-link-tracks");
2527

26-
await Assertions.Expect(playgroundLink.First).ToBeVisibleAsync(new() { Timeout = 10_000 });
27-
await Assertions.Expect(galleryLink.First).ToBeVisibleAsync(new() { Timeout = 10_000 });
28-
await Assertions.Expect(conceptMapLink.First).ToBeVisibleAsync(new() { Timeout = 10_000 });
29-
await Assertions.Expect(curriculumLink.First).ToBeVisibleAsync(new() { Timeout = 10_000 });
28+
await Assertions.Expect(playgroundLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
29+
await Assertions.Expect(galleryLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
30+
await Assertions.Expect(conceptMapLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
31+
await Assertions.Expect(curriculumLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
32+
await Assertions.Expect(whatsNewLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
33+
await Assertions.Expect(tracksLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
3034
}
3135
finally { await fixture.ClosePageAsync(page); }
3236
}
@@ -40,10 +44,10 @@ public async Task HomePage_QuickLinks_NavigateCorrectly()
4044
await page.GotoAsync(fixture.WebBaseUrl + "/");
4145
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
4246

43-
// Find the Playground quick link and click it
44-
var playgroundLink = page.GetByRole(AriaRole.Link, new() { NameRegex = new Regex("playground", RegexOptions.IgnoreCase) });
45-
await Assertions.Expect(playgroundLink.First).ToBeVisibleAsync(new() { Timeout = 10_000 });
46-
await playgroundLink.First.ClickAsync();
47+
// Find the Playground hero link and click it
48+
var playgroundLink = page.GetByTestId("hero-link-playground");
49+
await Assertions.Expect(playgroundLink).ToBeVisibleAsync(new() { Timeout = 10_000 });
50+
await playgroundLink.ClickAsync();
4751

4852
// Verify navigation to /playground
4953
await Assertions.Expect(page).ToHaveURLAsync(new Regex("/playground"), new() { Timeout = 10_000 });

AspireAcademy.Web/src/pages/HomePage.tsx

Lines changed: 73 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -404,216 +404,90 @@ export default function HomePage() {
404404
</Flex>
405405

406406
{/* ═══════════════════ HERO ═══════════════════ */}
407-
<Box id="hero" position="relative" minH={{ base: '70vh', md: '75vh' }} overflow="hidden">
407+
<Box id="hero" position="relative" minH={{ base: 'auto', md: '440px' }} overflow="hidden">
408408
<NetworkCanvas />
409-
{/* Subtle multi-color gradient overlay */}
410-
<Box
411-
position="absolute"
412-
inset="0"
413-
background="linear-gradient(135deg, rgba(107,79,187,0.12), rgba(45,212,191,0.08), rgba(251,191,36,0.04))"
414-
pointerEvents="none"
415-
zIndex={0}
416-
aria-hidden="true"
417-
/>
409+
<Box position="absolute" inset="0" background="linear-gradient(135deg, rgba(107,79,187,0.12), rgba(45,212,191,0.08), rgba(251,191,36,0.04))" pointerEvents="none" zIndex={0} aria-hidden="true" />
410+
411+
{/* Split hero: left content + right side panel */}
412+
<Flex position="relative" zIndex={1} direction={{ base: 'column', md: 'row' }} minH={{ base: 'auto', md: '440px' }}>
413+
{/* Left: main hero content */}
414+
<Flex flex="1" direction="column" justify="center" px={{ base: '6', md: '14' }} py={{ base: '12', md: '14' }} gap="4">
415+
{isLoggedIn && user && (
416+
<Box data-testid="welcome-back" px="4" py="2" borderRadius="md" bg="dark.card" border="1px solid" borderColor="game.pixelBorder" display="inline-block" alignSelf="flex-start">
417+
<Text {...pixelFontProps} fontSize="10px" color="game.gold">
418+
Welcome back, {user.displayName || user.username}!
419+
</Text>
420+
<Text fontSize="xs" color="dark.muted" mt="1">Continue your journey where you left off</Text>
421+
</Box>
422+
)}
418423

419-
<Flex
420-
position="relative"
421-
zIndex={1}
422-
direction="column"
423-
align="center"
424-
justify="center"
425-
textAlign="center"
426-
minH={{ base: '70vh', md: '75vh' }}
427-
pt={{ base: '8', md: '12' }}
428-
px="4"
429-
gap="6"
430-
>
431-
{/* Welcome back banner */}
432-
{isLoggedIn && user && (
433-
<Box
434-
data-testid="welcome-back"
435-
{...retroCardProps}
436-
bg="dark.card"
437-
px="6"
438-
py="3"
439-
mb="6"
440-
>
441-
<Text {...pixelFontProps} fontSize={{ base: '10px', md: '12px' }} color="game.gold">
442-
Welcome back, {user.displayName || user.username}!
443-
</Text>
444-
<Text fontSize="sm" color="dark.muted" mt="1">
445-
Continue your journey where you left off
424+
<Heading data-testid="hero-title" as="h1" {...pixelFontProps} fontSize={{ base: '28px', sm: '36px', md: '44px', lg: '48px' }} color="aspire.600" lineHeight="1.3" style={{ animation: 'rainbow-glow 4s ease-in-out infinite' }}>
425+
ASPIRE<br />LEARN
426+
</Heading>
427+
428+
<Box h="24px" data-testid="hero-tagline">
429+
<Text fontSize={{ base: 'md', md: 'lg' }} color="dark.text" letterSpacing="wide">
430+
{tagline.displayed}
431+
<Text as="span" style={{ animation: tagline.done ? 'typewriter-cursor 1s step-end infinite' : 'none', opacity: tagline.done ? undefined : 1 }} ml="1px">|</Text>
446432
</Text>
447433
</Box>
448-
)}
449-
450-
{/* Main title */}
451-
<Heading
452-
data-testid="hero-title"
453-
as="h1"
454-
{...pixelFontProps}
455-
fontSize={{ base: '28px', sm: '36px', md: '56px', lg: '64px' }}
456-
color="aspire.600"
457-
lineHeight="1.3"
458-
style={{ animation: 'rainbow-glow 4s ease-in-out infinite' }}
459-
>
460-
ASPIRE
461-
<br />
462-
LEARN
463-
</Heading>
464434

465-
{/* Tagline with typewriter */}
466-
<Box h="28px" data-testid="hero-tagline">
467-
<Text
468-
fontSize={{ base: 'md', md: 'xl' }}
469-
color="dark.text"
470-
letterSpacing="wide"
471-
>
472-
{tagline.displayed}
473-
<Text
474-
as="span"
475-
style={{
476-
animation: tagline.done ? 'typewriter-cursor 1s step-end infinite' : 'none',
477-
opacity: tagline.done ? undefined : 1,
478-
}}
479-
ml="1px"
480-
>
481-
|
482-
</Text>
435+
<Text maxW="440px" fontSize={{ base: 'sm', md: 'md' }} color="dark.muted" lineHeight="1.7">
436+
Distributed apps are hard — tangled configs, scattered logs, painful deploys. Aspire fixes that. Learn the platform by building real apps.
483437
</Text>
484-
</Box>
485-
486-
{/* Value proposition subtitle */}
487-
<Text
488-
maxW="520px"
489-
fontSize={{ base: 'sm', md: 'md' }}
490-
color="dark.muted"
491-
lineHeight="1.7"
492-
mt="2"
493-
>
494-
Distributed apps are hard — tangled configs, scattered logs, painful deploys.
495-
Aspire fixes that. Learn the platform by building real apps.
496-
</Text>
497-
498-
{/* CTA buttons */}
499-
<Flex
500-
data-testid="hero-ctas"
501-
direction={{ base: 'column', sm: 'row' }}
502-
gap="4"
503-
mt="4"
504-
>
505-
<Button
506-
data-testid="cta-start-journey"
507-
size="lg"
508-
bg="game.xpGold"
509-
color="dark.bg"
510-
{...pixelFontProps}
511-
fontSize={{ base: '11px', md: '13px' }}
512-
px="8"
513-
py="6"
514-
{...retroCardProps}
515-
borderColor="game.xpGold"
516-
boxShadow="4px 4px 0 rgba(255,215,0,0.4)"
517-
_hover={{
518-
transform: 'translateY(-2px)',
519-
boxShadow: '4px 6px 0 rgba(255,215,0,0.5)',
520-
}}
521-
transition="all 0.2s"
522-
onClick={() => navigate(isLoggedIn ? '/dashboard' : '/register')}
523-
>
524-
{isLoggedIn ? 'Continue Journey' : 'Start Your Journey'}
525-
</Button>
526438

527-
<Button
528-
data-testid="cta-browse-curriculum"
529-
size="lg"
530-
bg="transparent"
531-
color="aspire.accent"
532-
{...pixelFontProps}
533-
fontSize={{ base: '11px', md: '13px' }}
534-
px="8"
535-
py="6"
536-
border="3px solid"
537-
borderColor="aspire.600"
538-
boxShadow="4px 4px 0 #2B1260"
539-
borderRadius="sm"
540-
_hover={{
541-
bg: 'rgba(107,79,187,0.15)',
542-
transform: 'translateY(-2px)',
543-
}}
544-
transition="all 0.2s"
545-
onClick={() => navigate('/dashboard')}
546-
>
547-
Browse Curriculum
548-
</Button>
439+
<Flex data-testid="hero-ctas" direction={{ base: 'column', sm: 'row' }} gap="3" mt="2">
440+
<Button data-testid="cta-start-journey" size="lg" bg="aspire.600" color="white" {...pixelFontProps} fontSize={{ base: '10px', md: '12px' }} px="7" py="5" {...retroCardProps} borderColor="aspire.600" boxShadow="0 0 20px rgba(139,92,246,0.3), 4px 4px 0 #4C1D95" _hover={{ transform: 'translateY(-2px)', boxShadow: '0 0 30px rgba(139,92,246,0.4), 4px 6px 0 #4C1D95' }} transition="all 0.2s" onClick={() => navigate(isLoggedIn ? '/dashboard' : '/register')}>
441+
{isLoggedIn ? 'Continue Journey' : 'Start Your Journey'}
442+
</Button>
443+
<Button data-testid="cta-browse-curriculum" size="lg" bg="transparent" color="accent.teal" {...pixelFontProps} fontSize={{ base: '10px', md: '12px' }} px="7" py="5" border="3px solid" borderColor="accent.teal" boxShadow="4px 4px 0 rgba(45,212,191,0.2)" borderRadius="sm" _hover={{ bg: 'rgba(45,212,191,0.1)', transform: 'translateY(-2px)' }} transition="all 0.2s" onClick={() => navigate('/dashboard')}>
444+
Browse Curriculum
445+
</Button>
446+
</Flex>
549447
</Flex>
550448

551-
{/* Scroll hint */}
552-
<Text
553-
mt="10"
554-
fontSize="xs"
555-
color="dark.muted"
556-
style={{ animation: 'float-node 2s ease-in-out infinite' }}
557-
cursor="pointer"
558-
role="button"
559-
tabIndex={0}
560-
onClick={() => scrollTo('why-aspire')}
561-
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); scrollTo('why-aspire'); } }}
562-
>
563-
▼ Scroll to explore ▼
564-
</Text>
449+
{/* Right: side panel with mini-cards */}
450+
<Flex direction="column" w={{ base: '100%', md: '380px' }} gap="2.5" px={{ base: '6', md: '7' }} py={{ base: '6', md: '10' }} borderLeft={{ base: 'none', md: '1px solid' }} borderTop={{ base: '1px solid', md: 'none' }} borderColor="dark.border" justify="center">
451+
{[
452+
{ icon: '🎮', label: 'Playground', sub: 'Build app models visually', to: '/playground', accentBg: 'rgba(45,212,191,0.1)', accentBorder: 'rgba(45,212,191,0.3)', testId: 'hero-link-playground' },
453+
{ icon: '🖼️', label: 'Gallery', sub: '12 architecture examples', to: '/gallery', accentBg: 'rgba(251,191,36,0.1)', accentBorder: 'rgba(251,191,36,0.3)', testId: 'hero-link-gallery' },
454+
{ icon: '🗺️', label: 'Concept Map', sub: 'Explore Aspire concepts', to: '/concept-map', accentBg: 'rgba(52,211,153,0.1)', accentBorder: 'rgba(52,211,153,0.3)', testId: 'hero-link-conceptmap' },
455+
{ icon: '📚', label: 'Curriculum', sub: `${worlds?.length ?? 13} worlds, ${worlds?.reduce((s, w) => s + w.totalLessons, 0) ?? 174} lessons`, to: '/dashboard', accentBg: 'rgba(56,189,248,0.1)', accentBorder: 'rgba(56,189,248,0.3)', testId: 'hero-link-curriculum' },
456+
{ icon: '📰', label: "What's New", sub: 'Latest updates & features', to: '/whats-new', accentBg: 'rgba(251,146,60,0.1)', accentBorder: 'rgba(251,146,60,0.3)', testId: 'hero-link-whatsnew' },
457+
{ icon: '🎓', label: 'Learning Tracks', sub: 'Find your personalized path', to: '/personas', accentBg: 'rgba(52,211,153,0.1)', accentBorder: 'rgba(52,211,153,0.3)', testId: 'hero-link-tracks' },
458+
].map((card) => (
459+
<Flex
460+
key={card.label}
461+
data-testid={card.testId}
462+
align="center"
463+
gap="3"
464+
px="4"
465+
py="3"
466+
borderRadius="lg"
467+
bg="dark.card"
468+
border="1px solid"
469+
borderColor="dark.border"
470+
cursor="pointer"
471+
transition="all 0.2s"
472+
role="link"
473+
tabIndex={0}
474+
_hover={{ borderColor: card.accentBorder, transform: 'translateX(4px)', boxShadow: `0 0 12px ${card.accentBg}` }}
475+
onClick={() => navigate(card.to)}
476+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(card.to); } }}
477+
>
478+
<Flex align="center" justify="center" w="36px" h="36px" borderRadius="lg" bg={card.accentBg} border="1px solid" borderColor={card.accentBorder} flexShrink={0}>
479+
<Text fontSize="16px">{card.icon}</Text>
480+
</Flex>
481+
<Box>
482+
<Text fontSize="12px" color="dark.text" fontWeight="bold">{card.label}</Text>
483+
<Text fontSize="10px" color="dark.muted" mt="0.5">{card.sub}</Text>
484+
</Box>
485+
</Flex>
486+
))}
487+
</Flex>
565488
</Flex>
566489
</Box>
567490

568-
{/* ═══════════════════ QUICK LINKS BAR ═══════════════════ */}
569-
<Flex
570-
justify="center"
571-
align="center"
572-
gap={{ base: '3', md: '6' }}
573-
py="4"
574-
px="4"
575-
bg="dark.surface"
576-
borderTop="1px solid rgba(107,79,187,0.2)"
577-
borderBottom="1px solid rgba(107,79,187,0.2)"
578-
wrap="wrap"
579-
>
580-
{[
581-
{ icon: '🎮', label: 'Playground', to: '/playground', accent: '#2DD4BF', accentBg: 'rgba(45,212,191,0.1)', accentBorder: 'rgba(45,212,191,0.3)', hoverBg: 'rgba(45,212,191,0.2)' },
582-
{ icon: '🖼️', label: 'Gallery', to: '/gallery', accent: '#FBBF24', accentBg: 'rgba(251,191,36,0.1)', accentBorder: 'rgba(251,191,36,0.3)', hoverBg: 'rgba(251,191,36,0.2)' },
583-
{ icon: '🗺️', label: 'Concept Map', to: '/concept-map', accent: '#34D399', accentBg: 'rgba(52,211,153,0.1)', accentBorder: 'rgba(52,211,153,0.3)', hoverBg: 'rgba(52,211,153,0.2)' },
584-
{ icon: '📚', label: 'Curriculum', to: '/dashboard', accent: '#38BDF8', accentBg: 'rgba(56,189,248,0.1)', accentBorder: 'rgba(56,189,248,0.3)', hoverBg: 'rgba(56,189,248,0.2)' },
585-
].map((link) => (
586-
<Flex
587-
key={link.label}
588-
align="center"
589-
gap="2"
590-
px="4"
591-
py="2"
592-
borderRadius="full"
593-
bg={link.accentBg}
594-
border="1px solid"
595-
borderColor={link.accentBorder}
596-
cursor="pointer"
597-
transition="all 0.25s ease"
598-
role="link"
599-
tabIndex={0}
600-
_hover={{
601-
bg: link.hoverBg,
602-
borderColor: link.accent,
603-
boxShadow: `0 0 16px ${link.accentBorder}`,
604-
transform: 'translateY(-1px)',
605-
}}
606-
onClick={() => navigate(link.to)}
607-
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(link.to); } }}
608-
>
609-
<Text fontSize="sm">{link.icon}</Text>
610-
<Text fontSize="xs" color={link.accent} fontWeight="bold" letterSpacing="wide">
611-
{link.label}
612-
</Text>
613-
</Flex>
614-
))}
615-
</Flex>
616-
617491
{/* ═══════════════════ WHY ASPIRE ═══════════════════ */}
618492
<Box id="why-aspire" py={{ base: '16', md: '20' }} px="4">
619493
<Heading
@@ -1082,7 +956,7 @@ export default function HomePage() {
1082956
Aspire Learn
1083957
</Text>
1084958
<Text fontSize="xs" color="dark.muted">
1085-
Learn Aspire · Build distributed apps · Have fun doing it
959+
Learn Aspire · Build real apps · Level up your stack
1086960
</Text>
1087961
<Flex gap="4" mt="2">
1088962
<a

0 commit comments

Comments
 (0)