diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..431ff44 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,139 @@ +# AGENTS.md — InteractivETH + +## What This Project Is + +**InteractivETH** is an interactive educational platform for learning Ethereum protocol concepts. It uses hands-on simulations (transaction lifecycle, block building, gas auctions, security attacks) to teach concepts from the book [Mastering Ethereum](https://github.com/ethereumbook/ethereumbook). + +**Branding**: The name is InteractivETH (capital ETH). Favicon is a lotus flower 🪷 (The Red Guild). + +## Critical Rules + +### 1. ALWAYS USE `pnpm` +Never use `npm` or `yarn`. All commands use `pnpm`: +```bash +pnpm install +pnpm dev +pnpm build +``` + +### 2. ALWAYS EXTRACT STRINGS TO TRANSLATIONS +Never hardcode user-facing text. Always use `useTranslations()`: + +```typescript +import { useTranslations } from 'next-intl'; + +const t = useTranslations('namespace'); // or useTranslations() for root +``` + +**Translation workflow:** +1. Add the key to `messages/en.json` first +2. Add the Spanish equivalent to `messages/es.json` +3. Keep both files in sync — same keys, translated values +4. Use the component with `t('key')` or `t('nested.key')` + +**Example:** +```json +// messages/en.json +{ "sandwich": { "title": "Sandwich Attack Simulator" } } + +// messages/es.json +{ "sandwich": { "title": "Simulador de Ataque Sandwich" } } +``` + +```tsx +const t = useTranslations('sandwich'); +

{t('title')}

+``` + +### 3. UPDATE TODOS AND STATUS +After completing work: +- Update `TODO.md` with current status +- Update `SECURITY_TODO.md` for security modules +- Keep this file current if project structure changes + +## Project Structure + +``` +app/[locale]/ → Pages (i18n routing: /en, /es) +components/ + layout/ → Sidebar, breadcrumbs, tutorial layout, page nav + navigation/ → Search modal, locale switcher + ethereum/ → Shared simulator components + tutorials/ → Per-tutorial components (validator/, gas-auction/, etc.) + ui/ → Reusable primitives (CodeBlock, etc.) +messages/ → en.json, es.json (keep keys in sync!) +notes/ + chapters/ → Original Markdown study notes rendered for Spanish and auto-listed in the sidebar + translations/ + en/ → English note translations rendered under /en/notes/[slug] +book/ → Mastering Ethereum git submodule (17 chapters) +public/ → Static assets (lotus favicon) +``` + +## Code Patterns + +### New tutorial page +```tsx +'use client'; +import { useTranslations } from 'next-intl'; +import { TutorialLayout } from '@/components/layout/tutorial-layout'; + +export default function MyTutorialPage() { + const t = useTranslations('myTutorial'); + // ... +} +``` + +### New Coming Soon page +All use the shared pattern in `security/*/page.tsx` with `useTranslations('common')`. + +### File naming +- Components: `kebab-case.tsx` +- Hooks: `use-kebab-case.ts` +- Pages: `page.tsx` in route directories + +## Adding Content + +1. **New tutorial**: Create page in `app/[locale]/slug/`, components in `components/tutorials/slug/`, translations in both JSON files +2. **New translation key**: Add to `en.json` AND `es.json` simultaneously +3. **New route**: Register in `breadcrumbs.tsx` PAGE_HIERARCHY and `sidebar.tsx` +4. **Difficulty badges**: Use `useTranslations('difficulty')` → `td('beginner')`, `td('intermediate')`, `td('advanced')` + +### Study notes +- Add chapter notes as Markdown files in `notes/chapters/` +- Add English translations in `notes/translations/en/` when needed +- Landing-page chapter resources can link to them through `/{locale}/notes/[slug]` + +## Book Reference + +The `book/` directory contains Mastering Ethereum as a git submodule with all 17 chapters in Markdown. Use it as the technical reference when building new tutorials. Key mappings: + +| Tutorial | Chapter | +|----------|---------| +| Transactions | 6 (Transactions) | +| Block Builder | 15 (Consensus) | +| Gas & Fees | 6 (Gas) | +| Block Internals | 14 (EVM) + 15 (Consensus) | +| Smart Contracts | 7 (Solidity) + 8 (Vyper) | +| EVM | 14 (EVM) | +| Security modules | 9 (Smart Contract Security) | +| MEV | 16 (Scaling) | + +## Verification + +Before marking work complete: +```bash +pnpm build # Must compile with zero errors +``` + +Check translation parity: +```bash +node -e "const en=require('./messages/en.json');const es=require('./messages/es.json');const ek=Object.keys(en);const sk=Object.keys(es);console.log('en:',ek.length,'es:',sk.length);" +``` + +## Current Status (as of 2026-04-03) + +**Completed:** Transactions, Block Builder, Gas & Fees, Block Internals, Protocol Visualizer, Sandwich Attack simulator +**In progress:** Security section (7/8 modules are "Coming Soon" placeholders) +**Planned:** Smart Contracts, EVM, MEV tutorials +**i18n:** English + Spanish fully wired, all translation keys synced (510+ keys) diff --git a/README.md b/README.md index 12dc017..7b1f13f 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,129 @@ -# InteractiveETH +# InteractivETH Interactive educational visualizations for understanding Ethereum protocol concepts, based on [Mastering Ethereum](https://github.com/ethereumbook/ethereumbook). ## Features -- Interactive transaction lifecycle visualization -- Block creation and consensus simulation -- Multi-language support (English, Spanish) -- Modular tutorial architecture for adding new content +- **Interactive Simulators** — Hands-on exploration of Ethereum concepts (transactions, block building, gas markets, consensus) +- **Security Section** — Learn about attack vectors (sandwich attacks, front-running, reentrancy, etc.) with both attack and defense perspectives +- **Bilingual** — Full English/Spanish support via `next-intl` with locale-based routing (`/en/...`, `/es/...`) +- **Difficulty Levels** — Beginner and Advanced modes per tutorial +- **Modular Architecture** — Easy to add new tutorials following established patterns +- **Responsive Design** — Works on desktop and mobile with collapsible sidebar +- **Lotus Flower Favicon** — Branded with The Red Guild identity 🪷 ## Getting Started ```bash -# Install dependencies pnpm install - -# Run development server pnpm dev - -# Build for production -pnpm build ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. +Open [http://localhost:3000](http://localhost:3000). ## Available Tutorials -| Tutorial | Status | -|----------|--------| -| Transactions | Available | -| Blocks | Available | -| Smart Contracts | Coming Soon | -| Gas & Fees | Coming Soon | -| EVM | Coming Soon | -| MEV | Coming Soon | +### Core Concepts + +| Tutorial | Route | Status | +|----------|-------|--------| +| Transactions | `/transactions` | ✅ Available | +| Block Builder | `/validator` | ✅ Available | +| Gas & Fees | `/gas` | ✅ Available | +| Block Internals | `/block-internals` | ✅ Available | +| Protocol Visualizer | `/protocol-visualizer` | ✅ Available | + +### Security + +| Tutorial | Route | Status | +|----------|-------|--------| +| Sandwich Attack | `/security/sandwich-attack` | ✅ Available | +| Front-Running | `/security/front-running` | 🚧 Coming Soon | +| Reentrancy | `/security/reentrancy` | 🚧 Coming Soon | +| Oracle Manipulation | `/security/oracle-manipulation` | 🚧 Coming Soon | +| Rogue Proposer | `/security/rogue-proposer` | 🚧 Coming Soon | +| Double-Signing | `/security/double-signing` | 🚧 Coming Soon | +| Eclipse Attack | `/security/eclipse-attack` | 🚧 Coming Soon | +| 51% Attack | `/security/51-percent` | 🚧 Coming Soon | + +### Planned + +| Tutorial | Book Chapter | +|----------|-------------| +| Smart Contracts | Chapter 7 (Solidity) / Chapter 8 (Vyper) | +| EVM Deep Dive | Chapter 14 | +| MEV & PBS | Chapter 16 | ## Project Structure ``` -├── app/ # Next.js app router pages -│ ├── [locale]/ # i18n routing -│ └── [locale]/transactions/ # First tutorial -├── components/ # React components -│ ├── ethereum/ # Simulator components -│ └── ui/ # UI primitives -├── book/ # Mastering Ethereum source (git submodule) -├── hooks/ # Custom React hooks -├── i18n/ # Internationalization config -├── lib/ # Utilities and types -└── messages/ # Translation files +├── app/ +│ ├── [locale]/ # i18n routing (en, es) +│ │ ├── page.tsx # Landing page +│ │ ├── transactions/ # Core concept tutorials +│ │ ├── validator/ +│ │ ├── gas/ +│ │ ├── block-internals/ +│ │ ├── protocol-visualizer/ +│ │ └── security/ # Security section +│ │ ├── page.tsx # Security overview +│ │ ├── sandwich-attack/ +│ │ └── ... # Coming soon modules +│ └── layout.tsx # Root layout with metadata +├── components/ +│ ├── layout/ # Sidebar, breadcrumbs, navigation +│ ├── navigation/ # Search modal, locale switcher +│ ├── ethereum/ # Shared simulator components +│ ├── tutorials/ # Per-tutorial components +│ │ ├── validator/ +│ │ ├── gas-auction/ +│ │ └── block-internals/ +│ └── ui/ # Reusable UI primitives +├── hooks/ # Custom React hooks +├── i18n/ # next-intl config & routing +├── lib/ # Utilities and types +├── messages/ # Translation files (en.json, es.json) +├── notes/ +│ ├── chapters/ # Original Markdown study notes, used for Spanish and auto-listed in the sidebar +│ └── translations/ +│ └── en/ # English note translations rendered for /en/notes/[slug] +├── public/ # Static assets (lotus favicon, etc.) +└── book/ # Mastering Ethereum (git submodule) ``` ## Adding a New Tutorial -1. Create a new page: `app/[locale]/[tutorial]/page.tsx` -2. Add tutorial metadata to the landing page -3. Create translations in `messages/en.json` and `messages/es.json` +1. **Create the page**: `app/[locale]/[tutorial-slug]/page.tsx` +2. **Build components**: `components/tutorials/[tutorial-slug]/` +3. **Add translations**: Keys in both `messages/en.json` and `messages/es.json` +4. **Register on landing page**: Add to `LEARNING_CARDS` or `COMING_SOON_CARDS` +5. **Add to sidebar**: Update `sidebar.tsx` navigation config +6. **Add breadcrumbs**: Register in `components/navigation/breadcrumbs.tsx` + +See `AGENTS.md` for detailed development guidelines. + +## i18n + +- **Default locale**: English (`en`) +- **Supported locales**: English, Spanish +- **Routing**: Prefix-based (`/en/...`, `/es/...`) +- **Translation files**: `messages/en.json`, `messages/es.json` +- **Usage**: Always use `useTranslations()` — never hardcode user-facing strings ## Tech Stack -- Next.js 16 +- Next.js 16 (App Router) - React 19 - TypeScript - Tailwind CSS - Framer Motion - next-intl (i18n) +- Lucide React (icons) +- Vercel Analytics + +## Book Reference + +Content is based on [Mastering Ethereum](https://github.com/ethereumbook/ethereumbook) by Andreas M. Antonopoulos and Gavin Wood. The book is included as a git submodule in `book/` with all 17 chapters in Markdown format. ## License diff --git a/TODO.md b/TODO.md index 22060c1..c407082 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,21 @@ ## Project Overview Interactive educational visualizations for Ethereum protocol concepts, based on [Mastering Ethereum](https://github.com/ethereumbook/ethereumbook). +### Recent Updates +- 2026-04-07: Added a Discord invite CTA to the current chapter area on the front page so people can join the study-group coordination space directly from the landing page. +- 2026-04-07: Removed the dead Chapter 2 notes link from the front page and replaced it with a non-clickable "after reading" status until those notes actually exist. +- 2026-04-07: Updated the front page chapter-link model to use explicit lesson mappings and linked Chapter 2 to the existing Transactions and Gas lessons, based on the chapter’s focus on wallets, transactions, and gas basics. +- 2026-04-07: Added English note translations under `notes/translations/en/` and updated the notes page to render localized content, with an English-only notice that translations were produced using GPT-5.4. +- 2026-04-07: Made the sidebar discover chapter notes dynamically from `notes/chapters/` through an API route, so new Markdown notes appear automatically without manual nav updates. +- 2026-04-07: Installed `marked` and converted chapter notes into rendered in-app pages under `/{locale}/notes/[slug]`, with a banner clarifying that notes are currently available only in Spanish. +- 2026-04-07: Replaced the Chapter 1 notes placeholder with detailed study notes in `notes/chapters/chapter-1.md`, preserving links to the slide deck and replay playlist. +- 2026-04-07: Added a versioned `notes/chapters/` workspace plus a `/notes/[slug]` route so each study chapter can link to Markdown notes directly from the front page. +- 2026-04-07: Added the Chapter 1 slide deck to the front-page archive as a direct resource link while keeping the shared YouTube playlist as the replay entry point. +- 2026-04-07: Adjusted the front page study sequence so the current chapter points to Chapter 2 via the Luma embed and the archive starts with Chapter 1 while reusing the shared playlist for replay access. +- 2026-04-07: Refocused the localized front page around the current chapter, with the Luma embed as the primary study surface, a dedicated playlist replay panel, and an archive of past chapters with linked InteractivETH resources. +- 2026-04-06: Reworked the localized landing page into a minimalist hacker-style front page with embedded Luma and YouTube study surfaces plus a NotebookLM resource entry point. +- 2026-04-06: Added a chapter directory to the front page with Mastering Ethereum chapter links, replay access, InteractivETH routes, and related project resources. + --- ## Completed Tutorials @@ -279,7 +294,7 @@ t('simulator.validator.gasUsed', { used: 15000000, limit: 30000000 }); --- ## Last Updated -Date: 2026-04-02 +Date: 2026-04-07 Current Status: ✅ All 3 block-building tutorials complete (Validator, Gas, Block Internals) Next Milestone: Smart Contracts tutorial diff --git a/app/[locale]/notes/[slug]/page.tsx b/app/[locale]/notes/[slug]/page.tsx new file mode 100644 index 0000000..e6b23bd --- /dev/null +++ b/app/[locale]/notes/[slug]/page.tsx @@ -0,0 +1,227 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { notFound } from 'next/navigation'; +import type { ReactNode } from 'react'; +import { marked, type Token, type Tokens } from 'marked'; +import { getTranslations } from 'next-intl/server'; +import { AlertCircle, BookText, FileText } from 'lucide-react'; +import { TutorialLayout } from '@/components/layout/tutorial-layout'; + +const NOTES_DIRECTORY = path.join(process.cwd(), 'notes', 'chapters'); +const TRANSLATED_NOTES_DIRECTORY = path.join(process.cwd(), 'notes', 'translations'); + +function isSafeSlug(slug: string) { + return /^[a-z0-9-]+$/.test(slug); +} + +function getTokenKey(token: { type: string; raw?: string; text?: string }, fallback: string) { + return `${token.type}-${token.raw ?? token.text ?? fallback}`; +} + +function createSiblingKeyFactory(scope: string) { + const seen = new Map(); + + return (signature: string) => { + const count = seen.get(signature) ?? 0; + seen.set(signature, count + 1); + return `${scope}-${signature}-${count}`; + }; +} + +function renderInlineTokens(tokens: Token[] = []): ReactNode { + const getSiblingKey = createSiblingKeyFactory('inline'); + + return tokens.map((token) => { + const key = getSiblingKey(getTokenKey(token, 'inline')); + + switch (token.type) { + case 'text': + case 'escape': + return {token.text}; + case 'strong': + return {renderInlineTokens(token.tokens)}; + case 'em': + return {renderInlineTokens(token.tokens)}; + case 'codespan': + return {token.text}; + case 'del': + return {renderInlineTokens(token.tokens)}; + case 'br': + return
; + case 'link': + return ( + + {renderInlineTokens(token.tokens)} + + ); + default: + return 'text' in token ? {token.text} : null; + } + }); +} + +function renderBlockTokens(tokens: Token[] = []): ReactNode { + const getSiblingKey = createSiblingKeyFactory('block'); + + return tokens.map((token) => { + const key = getSiblingKey(getTokenKey(token, 'block')); + + switch (token.type) { + case 'space': + return null; + case 'heading': { + switch (token.depth) { + case 1: + return

{renderInlineTokens(token.tokens)}

; + case 2: + return

{renderInlineTokens(token.tokens)}

; + case 3: + return

{renderInlineTokens(token.tokens)}

; + case 4: + return

{renderInlineTokens(token.tokens)}

; + case 5: + return
{renderInlineTokens(token.tokens)}
; + default: + return
{renderInlineTokens(token.tokens)}
; + } + } + case 'paragraph': + return

{renderInlineTokens(token.tokens)}

; + case 'blockquote': + return
{renderBlockTokens(token.tokens)}
; + case 'hr': + return
; + case 'code': + return ( +
+            {token.text}
+          
+ ); + case 'list': { + const ListTag = token.ordered ? 'ol' : 'ul'; + const getItemKey = createSiblingKeyFactory(`${key}-item`); + + return ( + + {token.items.map((item: Tokens.ListItem) => ( +
  • {renderBlockTokens(item.tokens)}
  • + ))} +
    + ); + } + case 'table': + { + const getHeaderKey = createSiblingKeyFactory(`${key}-header`); + const getRowKey = createSiblingKeyFactory(`${key}-row`); + + return ( + + + + {token.header.map((cell: Tokens.TableCell) => ( + + ))} + + + + {token.rows.map((row: Tokens.TableCell[]) => { + const rowKey = getRowKey(row.map((cell) => cell.text).join('|')); + const getCellKey = createSiblingKeyFactory(`${rowKey}-cell`); + + return ( + + {row.map((cell: Tokens.TableCell) => ( + + ))} + + )})} + +
    {renderInlineTokens(cell.tokens)}
    + {renderInlineTokens(cell.tokens)} +
    + ); + } + case 'text': + return token.tokens ?

    {renderInlineTokens(token.tokens)}

    :

    {token.text}

    ; + default: + return null; + } + }); +} + +export default async function NotesPage({ + params, +}: { + params: Promise<{ locale: string; slug: string }>; +}) { + const { locale, slug } = await params; + + if (!isSafeSlug(slug)) { + notFound(); + } + + let markdown: string; + + try { + const translatedFilePath = path.join(TRANSLATED_NOTES_DIRECTORY, locale, `${slug}.md`); + const originalFilePath = path.join(NOTES_DIRECTORY, `${slug}.md`); + + markdown = + locale === 'en' + ? await readFile(translatedFilePath, 'utf8') + : await readFile(originalFilePath, 'utf8'); + } catch { + notFound(); + } + + const t = await getTranslations('notesPage'); + const tokens = marked.lexer(markdown, { + gfm: true, + breaks: true, + }); + + return ( + +
    +
    +
    + +
    +
    + + {t('badge')} +
    + +

    + {t('title')} +

    + +

    + {t('subtitle')} +

    + + {locale === 'en' ? ( +
    +
    + +
    +

    {t('translationNotice')}

    +
    +
    +
    + ) : null} +
    +
    + +
    +
    + + {slug}.md +
    + +
    {renderBlockTokens(tokens)}
    +
    +
    +
    + ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 8a7e027..5776c6f 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,503 +1,655 @@ 'use client'; -import { useTranslations, useLocale } from 'next-intl'; +import { useEffect, useState } from 'react'; import Link from 'next/link'; +import { useLocale, useTranslations } from 'next-intl'; import { motion } from 'framer-motion'; -import { - ArrowRight, - Crown, - Zap, - Binary, - Shield, - Code, - Cpu, - Timer, - ExternalLink, - MessageCircle, - Play, - Search, +import { + CalendarRange, Command, + ExternalLink, + LibraryBig, Menu, + PlayCircle, + Search, } from 'lucide-react'; import { Sidebar } from '@/components/layout/sidebar'; -import { useState, useEffect } from 'react'; import { SearchModal } from '@/components/navigation/search-modal'; -const LEARNING_CARDS = [ - { - key: 'transactions', - icon: ArrowRight, - href: '/transactions', - difficulty: 'beginner' as const, - }, - { - key: 'validator', - icon: Crown, - href: '/validator', - difficulty: 'intermediate' as const, - }, - { - key: 'gas', - icon: Zap, - href: '/gas', - difficulty: 'intermediate' as const, - }, - { - key: 'blockInternals', - icon: Binary, - href: '/block-internals', - difficulty: 'intermediate' as const, - }, - { - key: 'security', - icon: Shield, - href: '/security', - difficulty: 'advanced' as const, - }, -]; - -const DIFFICULTY_BADGES = { - beginner: { label: 'beginner', color: 'text-green-500', bg: 'bg-green-500/10 border-green-500/20' }, - intermediate: { label: 'intermediate', color: 'text-yellow-500', bg: 'bg-yellow-500/10 border-yellow-500/20' }, - advanced: { label: 'advanced', color: 'text-red-500', bg: 'bg-red-500/10 border-red-500/20' }, +const LUMA_EVENT_URL = 'https://luma.com/event/evt-WSD5ZSYfGtvMymJ'; +const LUMA_EVENT_EMBED_URL = 'https://luma.com/embed/event/evt-WSD5ZSYfGtvMymJ/simple'; +const YOUTUBE_PLAYLIST_EMBED_URL = + 'https://www.youtube.com/embed/videoseries?list=PLvTXryB-aecnlPmF9cyA8svSmezw7bTX_&rel=0'; +const YOUTUBE_PLAYLIST_URL = + 'https://www.youtube.com/playlist?list=PLvTXryB-aecnlPmF9cyA8svSmezw7bTX_'; +const NOTEBOOK_LM_URL = 'https://notebooklm.google.com/notebook/f46ed908-e31c-47b5-aea9-fccfb2293c2d'; +const MASTERING_ETHEREUM_URL = 'https://masteringethereum.xyz'; +const DISCORD_INVITE_URL = 'https://discord.com/invite/eegRCDmwbM'; +const CHAPTER_1_SLIDES_URL = + 'https://drive.google.com/file/d/1kZLWj9N8C96wh-Ow2iV1D-Q9G_IU_4CU/view?usp=drive_link'; +const CHAPTER_1_NOTES_URL = '/notes/chapter-1'; +type LessonLink = { + slug: string; + translationKey: string; }; -const COMING_SOON_CARDS = [ - { - key: 'smartContracts', - icon: Code, - }, - { - key: 'evm', - icon: Cpu, - }, - { - key: 'mev', - icon: Timer, - }, -]; +type ResourceLink = { + labelKey: string; + url?: string; + unavailableKey?: string; + prominent: boolean; +}; -// Animated Ethereum diamond SVG -function EthereumDiamond({ className = '' }: { className?: string }) { - return ( - - - - - - - - - ); +function resolveHref(locale: string, href: string) { + return href.startsWith('/') ? `/${locale}${href}` : href; } -// Floating particles background - only renders on client to avoid hydration mismatch -function FloatingParticles() { - const [mounted, setMounted] = useState(false); - const [particles] = useState(() => - Array.from({ length: 20 }, (_, i) => ({ - id: i, - x: Math.random() * 100, - y: Math.random() * 100, - size: Math.random() * 4 + 2, - delay: Math.random() * 5, - duration: Math.random() * 10 + 10, - })) - ); - - useEffect(() => setMounted(true), []); +const CURRENT_CHAPTER = { + slug: 'chapter-2', + chapterNumber: 2, + chapterUrl: 'https://masteringethereum.xyz/chapter_2.html', + lessonLinks: [ + { + slug: 'transactions', + translationKey: 'transactions', + }, + { + slug: 'gas', + translationKey: 'gas', + }, + ] as LessonLink[], + resources: ['notebooklm', 'playlist'], + resourceLinks: [ + { + labelKey: 'notes', + unavailableKey: 'readingInProgress', + prominent: false, + }, + ] as ResourceLink[], +} as const; - if (!mounted) return null; +const PAST_CHAPTERS = [ + { + slug: 'chapter-1', + chapterNumber: 1, + chapterUrl: 'https://masteringethereum.xyz/chapter_1.html', + lessonLinks: [] as LessonLink[], + resources: ['playlist', 'notebooklm'], + resourceLinks: [ + { + labelKey: 'notes', + url: CHAPTER_1_NOTES_URL, + prominent: true, + }, + { + labelKey: 'slides', + url: CHAPTER_1_SLIDES_URL, + prominent: false, + }, + ] as ResourceLink[], + }, +] as const; +function TerminalFrame({ + children, + title, + status, +}: { + children: React.ReactNode; + title: string; + status: string; +}) { return ( -
    - {particles.map((p) => ( - - ))} +
    +
    +
    +
    +
    +
    + + + +
    + {title} +
    + + {status} + +
    +
    +
    {children}
    ); } export default function LandingPage() { const t = useTranslations(); - const td = useTranslations('difficulty'); const locale = useLocale(); const [searchOpen, setSearchOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false); - // Keyboard shortcut for search useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - setSearchOpen(prev => !prev); + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'k') { + event.preventDefault(); + setSearchOpen((previous) => !previous); } }; + window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); return ( -
    - {/* Mobile Menu Button */} +
    - - {/* Sidebar */} - setSidebarOpen(false)} onSearchOpen={() => setSearchOpen(true)} /> - - {/* Main Content - Full width for landing page */} -
    -
    - {/* Hero Section - Ethereum Themed */} -
    - {/* Background effects */} - - - {/* Large subtle diamond watermark */} -
    - + +
    +
    +
    + +
    + +
    +
    +
    + + {t('landing.frontPage.badge')} +
    + +

    + {t('landing.frontPage.title')} +

    + +

    + {t('landing.frontPage.subtitle')} +

    + +

    + {t('landing.frontPage.studyGroup.prefix')}{' '} + + {t('landing.frontPage.studyGroup.linkLabel')} + + . +

    +
    + +
    + + + + {t('landing.frontPage.primaryCta')} + +
    - - {/* Radial glow */} -
    - -
    - {/* Badge */} - -
    - Powered by The Red Guild - - - {/* Title */} - +
    +

    + {t('landing.frontPage.panels.current.label')} +

    +

    {t('landing.frontPage.panels.current.value')}

    +
    +
    +

    + {t('landing.frontPage.panels.past.label')} +

    +

    {t('landing.frontPage.panels.past.value')}

    +
    +
    +

    + {t('landing.frontPage.panels.resources.label')} +

    +

    {t('landing.frontPage.panels.resources.value')}

    +
    +
    + + +
    + + - - {t('hero.title')} - - - - {/* Subtitle */} - +
    +
    + + {t('landing.frontPage.sections.current.kicker')} +
    +

    + {t('landing.frontPage.sections.current.title')} +

    +

    + {t('landing.frontPage.sections.current.description')} +

    +
    + + +
    + +
    +
    +
    +

    + {t('landing.frontPage.sections.current.chapterLabel', { + number: CURRENT_CHAPTER.chapterNumber, + })} +

    +

    + {t(`landing.frontPage.chapters.${CURRENT_CHAPTER.slug}.title`)} +

    +
    +
    + +
    +
    +

    + {t('landing.frontPage.sections.current.routesLabel')} +

    +
    + {CURRENT_CHAPTER.lessonLinks.length > 0 ? ( + CURRENT_CHAPTER.lessonLinks.map((lesson) => ( + + {t(`sidebar.${lesson.translationKey}`)} + + )) + ) : ( + + {t('landing.frontPage.sections.current.noRoute')} + + )} +
    +
    + +
    +

    + {t('landing.frontPage.sections.current.resourcesLabel')} +

    +
      + {CURRENT_CHAPTER.resources.map((resource) => ( +
    • + + {t(`landing.frontPage.resourcesList.${resource}`)} +
    • + ))} +
    + + {CURRENT_CHAPTER.resourceLinks && CURRENT_CHAPTER.resourceLinks.length > 0 ? ( +
    + {CURRENT_CHAPTER.resourceLinks.map((resourceLink) => { + const label = t(`landing.frontPage.linkLabels.${resourceLink.labelKey}`); + + if (resourceLink.url) { + return ( + + {label} + + + ); + } + + const unavailableKey = + resourceLink.unavailableKey ?? 'readingInProgress'; + + return ( + + {label} + + {t(`landing.frontPage.linkStatuses.${unavailableKey}`)} + + + ); + })} +
    + ) : null} +
    +
    +
    + +
    +