Skip to content

Commit d0b345f

Browse files
committed
feat: Initialize project with Vite and React
Sets up the project structure using Vite and React. Includes initial configurations for Vite, TypeScript, HTML, and basic styling in index.html. Adds a placeholder README and essential type definitions.
1 parent a8b246d commit d0b345f

23 files changed

+1915
-5
lines changed

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

App.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
2+
import React, { useState, useEffect, useRef } from 'react';
3+
import { CHAPTERS } from './constants';
4+
import Sidebar from './components/Sidebar';
5+
import ChapterSection from './components/ChapterSection';
6+
import VueLogo from './components/VueLogo';
7+
import EndScreen from './components/EndScreen';
8+
9+
const App: React.FC = () => {
10+
const [activeChapter, setActiveChapter] = useState(1);
11+
const scrollContainerRef = useRef<HTMLDivElement>(null);
12+
13+
const handleScroll = () => {
14+
if (!scrollContainerRef.current) return;
15+
16+
// Logic to determine active chapter based on scroll position
17+
// We check which chapter element is closest to the top of the viewport
18+
const containerTop = scrollContainerRef.current.scrollTop;
19+
const containerHeight = scrollContainerRef.current.clientHeight;
20+
const centerLine = containerTop + containerHeight / 2;
21+
22+
CHAPTERS.forEach((chapter) => {
23+
const element = document.getElementById(`chapter-${chapter.id}`);
24+
if (element) {
25+
const { offsetTop, offsetHeight } = element;
26+
if (centerLine >= offsetTop && centerLine < offsetTop + offsetHeight) {
27+
setActiveChapter(chapter.id);
28+
}
29+
}
30+
});
31+
};
32+
33+
const scrollToChapter = (id: number) => {
34+
const element = document.getElementById(`chapter-${id}`);
35+
if (element && scrollContainerRef.current) {
36+
// We scroll the container, not the window
37+
const top = element.offsetTop;
38+
scrollContainerRef.current.scrollTo({ top, behavior: 'smooth' });
39+
}
40+
};
41+
42+
// Initial scroll to hero
43+
const startReading = () => {
44+
scrollToChapter(1);
45+
};
46+
47+
return (
48+
<div className="relative h-screen w-screen overflow-hidden bg-[#FFF8E7] text-[#2c3e50]">
49+
{/* Background Grid Pattern */}
50+
<div className="fixed inset-0 pointer-events-none z-0 opacity-5">
51+
<div className="absolute inset-0" style={{
52+
backgroundImage: 'radial-gradient(#000 1px, transparent 1px)',
53+
backgroundSize: '24px 24px'
54+
}}></div>
55+
</div>
56+
57+
{/* Github Corner Ribbon */}
58+
<a href="https://github.com/CoderSerio/vue-source-book" target="_blank" className="fixed top-0 right-0 z-[60] group" aria-label="View source on GitHub">
59+
<svg width="80" height="80" viewBox="0 0 250 250" style={{ fill: '#151513', color: '#fff', position: 'absolute', top: 0, border: 0, right: 0 }} aria-hidden="true">
60+
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
61+
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style={{ transformOrigin: '130px 106px' }} className="group-hover:animate-[wave_0.5s_ease-in-out_infinite]"></path>
62+
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" className="opacity-90"></path>
63+
</svg>
64+
<style>{`
65+
@keyframes wave {
66+
0%, 100% { transform: rotate(0) }
67+
20%, 60% { transform: rotate(-25deg) }
68+
40%, 80% { transform: rotate(10deg) }
69+
}
70+
`}</style>
71+
</a>
72+
73+
<Sidebar currentChapterId={activeChapter} scrollToChapter={scrollToChapter} />
74+
75+
{/* Main Scroll Snap Container - Added strict scroll rules */}
76+
<div
77+
ref={scrollContainerRef}
78+
onScroll={handleScroll}
79+
className="h-full w-full overflow-y-scroll snap-y snap-mandatory scroll-smooth no-scrollbar"
80+
style={{ scrollBehavior: 'smooth' }}
81+
>
82+
{/* Hero Section (Snap Item) */}
83+
<section className="min-h-screen snap-start snap-always flex flex-col items-center justify-center text-center p-6 relative">
84+
{/* Decorative Blobs */}
85+
<div className="absolute top-20 right-10 w-64 h-64 bg-green-200 rounded-full mix-blend-multiply filter blur-2xl opacity-60 animate-pulse pointer-events-none"></div>
86+
<div className="absolute bottom-20 left-10 w-72 h-72 bg-blue-200 rounded-full mix-blend-multiply filter blur-2xl opacity-60 animate-pulse pointer-events-none" style={{animationDelay: '1s'}}></div>
87+
88+
<div className="bg-white p-12 rounded-[3rem] sketchy-border border-4 max-w-4xl relative z-10 transform hover:scale-[1.01] transition-transform duration-500">
89+
<div className="flex justify-center mb-8">
90+
<VueLogo className="transform scale-150" />
91+
</div>
92+
93+
<h1 className="text-6xl md:text-8xl font-bold mb-4 tracking-tighter font-hand">
94+
SOURCE BOOK
95+
</h1>
96+
97+
<div className="w-full h-1 bg-black my-6 rounded-full opacity-20"></div>
98+
99+
<h2 className="text-2xl md:text-4xl font-bold text-gray-600 mb-8 font-mono">
100+
The Vue.js Chronicles
101+
</h2>
102+
103+
<p className="text-xl mb-10 max-w-lg mx-auto font-medium text-gray-500">
104+
An interactive, hand-drawn journey through Reactivity, VDOM, Compilers, and Vapor Mode.
105+
</p>
106+
107+
<button
108+
onClick={startReading}
109+
className="bg-[#42b883] text-white text-2xl px-12 py-4 rounded-full font-bold hover:bg-[#35495e] border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] active:translate-y-1 active:shadow-none transition-all"
110+
>
111+
Start Journey ↓
112+
</button>
113+
</div>
114+
</section>
115+
116+
{/* Render Chapters */}
117+
{CHAPTERS.map((chapter, index) => (
118+
<ChapterSection
119+
key={chapter.id}
120+
data={chapter}
121+
isActive={activeChapter === chapter.id}
122+
nextChapterId={index < CHAPTERS.length - 1 ? CHAPTERS[index + 1].id : null}
123+
scrollToChapter={scrollToChapter}
124+
/>
125+
))}
126+
127+
{/* End Screen */}
128+
<EndScreen />
129+
</div>
130+
</div>
131+
);
132+
};
133+
134+
export default App;

ChapterSection.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, { useState } from 'react';
2+
import { ChapterData, PageData } from '../types';
3+
import CodePlayground from './CodePlayground';
4+
import PreviewBox from './PreviewBox';
5+
import ReadingSection from './ReadingSection';
6+
import { getGeminiExplanation } from '../services/geminiService';
7+
8+
interface ChapterSectionProps {
9+
data: ChapterData;
10+
isActive: boolean;
11+
nextChapterId: number | null;
12+
scrollToChapter: (id: number) => void;
13+
}
14+
15+
const ChapterSection: React.FC<ChapterSectionProps> = ({
16+
data,
17+
isActive,
18+
nextChapterId,
19+
scrollToChapter
20+
}) => {
21+
// State for challenges within this chapter
22+
const [completedPages, setCompletedPages] = useState<Record<string, boolean>>({});
23+
const [userCodes, setUserCodes] = useState<Record<string, string>>({});
24+
const [errors, setErrors] = useState<Record<string, string | null>>({});
25+
const [hints, setHints] = useState<Record<string, string>>({});
26+
const [loadingHints, setLoadingHints] = useState<Record<string, boolean>>({});
27+
28+
const handleRun = (page: PageData, code: string) => {
29+
if (!page.challenge) return;
30+
31+
const normalizedInput = code.replace(/\s+/g, '').toLowerCase();
32+
const validAnswers = page.challenge.correctAnswer;
33+
34+
const isValid = validAnswers.some(ans =>
35+
normalizedInput.includes(ans.replace(/\s+/g, '').toLowerCase())
36+
);
37+
38+
if (isValid) {
39+
setCompletedPages(prev => ({ ...prev, [page.id]: true }));
40+
setErrors(prev => ({ ...prev, [page.id]: null }));
41+
} else {
42+
setErrors(prev => ({ ...prev, [page.id]: "Hmm, that doesn't look quite right..." }));
43+
setCompletedPages(prev => ({ ...prev, [page.id]: false }));
44+
}
45+
};
46+
47+
const askWizard = async (page: PageData) => {
48+
if (!page.challenge) return;
49+
50+
setLoadingHints(prev => ({ ...prev, [page.id]: true }));
51+
const code = userCodes[page.id] || "empty";
52+
const help = await getGeminiExplanation(data.title + ": " + page.challenge.subtitle, code);
53+
setHints(prev => ({ ...prev, [page.id]: help }));
54+
setLoadingHints(prev => ({ ...prev, [page.id]: false }));
55+
};
56+
57+
return (
58+
<div id={`chapter-${data.id}`} className="w-full">
59+
{/* Chapter Title Slide (Optional, integrated into first page or just distinct) */}
60+
61+
{data.pages.map((page) => {
62+
// Wrapper for snap behavior
63+
return (
64+
<div key={page.id} className="min-h-screen w-full snap-start flex items-center justify-center p-4 md:p-8 relative">
65+
66+
{/* Chapter Indicator Watermark */}
67+
<div className={`absolute top-4 left-4 md:top-8 md:left-8 px-4 py-1 rounded-full border border-black/20 font-bold text-sm opacity-50 ${data.color}`}>
68+
CH.{data.id} - {data.title}
69+
</div>
70+
71+
{page.type === 'read' ? (
72+
<ReadingSection data={page} />
73+
) : (
74+
/* Challenge Layout */
75+
<div className="w-full max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8 items-center h-full max-h-[90vh]">
76+
77+
{/* Left: Challenge Code */}
78+
<div className="flex flex-col gap-6 h-full justify-center">
79+
<div className="bg-white p-6 sketchy-border border-2 relative z-10 shadow-lg">
80+
<div className="absolute -top-3 -left-2 bg-black text-white px-3 py-1 text-xs font-bold rounded transform -rotate-3 shadow-md">
81+
CHALLENGE
82+
</div>
83+
<h3 className="text-3xl font-bold mb-2 font-hand">{page.challenge?.subtitle}</h3>
84+
<p className="text-gray-600 text-lg leading-snug font-hand">{page.challenge?.description}</p>
85+
</div>
86+
87+
<CodePlayground
88+
codePre={page.challenge!.codePre}
89+
codePost={page.challenge!.codePost}
90+
placeholder={page.challenge!.placeholder}
91+
userCode={userCodes[page.id] || ""}
92+
onChange={(val) => setUserCodes(prev => ({ ...prev, [page.id]: val }))}
93+
isCompleted={!!completedPages[page.id]}
94+
onRun={() => handleRun(page, userCodes[page.id] || "")}
95+
error={errors[page.id] || null}
96+
/>
97+
98+
{/* Hints */}
99+
{!completedPages[page.id] && (
100+
<div className="flex justify-start pl-2">
101+
{!hints[page.id] ? (
102+
<button
103+
onClick={() => askWizard(page)}
104+
disabled={loadingHints[page.id]}
105+
className="text-sm font-bold text-purple-600 hover:text-purple-800 transition-all flex items-center gap-2"
106+
>
107+
<span className="text-lg">🧙‍♂️</span>
108+
{loadingHints[page.id] ? "Consulting the orbs..." : "Need a hint?"}
109+
</button>
110+
) : (
111+
<div className="bg-purple-50 p-3 rounded-lg border border-purple-200 text-purple-900 text-sm max-w-md animate-in fade-in slide-in-from-bottom-2">
112+
<strong>Wizard says:</strong> {hints[page.id]}
113+
</div>
114+
)}
115+
</div>
116+
)}
117+
</div>
118+
119+
{/* Right: Preview */}
120+
<div className="h-full max-h-[600px] flex items-center justify-center">
121+
<div className="w-full h-full">
122+
<PreviewBox type={page.challenge!.visualType} isCompleted={!!completedPages[page.id]} />
123+
</div>
124+
</div>
125+
</div>
126+
)}
127+
</div>
128+
);
129+
})}
130+
131+
{/* Optional: Next Chapter Button Page if needed, or just rely on scroll */}
132+
</div>
133+
);
134+
};
135+
136+
export default ChapterSection;

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
<div align="center">
2-
32
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3+
</div>
44

5-
<h1>Built with AI Studio</h2>
5+
# Run and deploy your AI Studio app
66

7-
<p>The fastest path from prompt to production with Gemini.</p>
7+
This contains everything you need to run your app locally.
88

9-
<a href="https://aistudio.google.com/apps">Start building</a>
9+
View your app in AI Studio: https://ai.studio/apps/drive/1xMx-9w9avmuyYdhEiNELKzFgabfK5LNr
1010

11-
</div>
11+
## Run Locally
12+
13+
**Prerequisites:** Node.js
14+
15+
16+
1. Install dependencies:
17+
`npm install`
18+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19+
3. Run the app:
20+
`npm run dev`

0 commit comments

Comments
 (0)