Explore Data Analysis
@@ -137,41 +260,6 @@ function SectionCards() {
);
}
-interface HomepageProjectCardProps {
- id: string;
- title: ReactNode;
- summary: ReactNode;
-}
-
-/**
- * date/url/tags come from the shared PROJECTS source of truth; title/summary
- * are passed in as already-built elements from the call site
- * (not string props) so Docusaurus's static i18n extraction — which needs a
- * literal with literal children right there in the
- * JSX — can still find them despite the shared layout being a component.
- */
-function HomepageProjectCard({id, title, summary}: HomepageProjectCardProps) {
- const meta = projectMeta(id);
- const {
- i18n: {currentLocale},
- } = useDocusaurusContext();
-
- return (
-
-
{title}
-
{formatProjectDate(meta.date, currentLocale)}
-
{summary}
-
- {meta.tags.map((tag) => (
-
- {tag}
-
- ))}
-
-
- );
-}
-
function RealWorldProjects() {
return (
@@ -189,515 +277,13 @@ function RealWorldProjects() {
browse any time, no need to finish the course first.
-
-
- Build a Dependency-Freshness Checker
-
- }
- summary={
-
- A real CLI that reads a pyproject.toml, checks PyPI for newer dependency
- versions, and reports what's outdated — no API key needed.
-
- }
- />
-
- Build a Git Commit-Message Generator
-
- }
- summary={
-
- Draft a Conventional-Commits-style message from a real staged git diff with a
- free-tier LLM, and only commit it after you explicitly confirm.
-
- }
- />
-
- Query a Database in Plain English with MCP
-
- }
- summary={
-
- Build an MCP server that exposes a local SQLite database, then watch an LLM
- client write and run its own SQL to answer plain-English questions about it.
-
- }
- />
-
- Build a Discord Trivia Bot
-
- }
- summary={
-
- Run trivia rounds in a Discord server with discord.py: a persistent leaderboard, and
- questions generated fresh on any topic with a free-tier LLM.
-
- }
- />
-
- Build an MCP Server for Your Notes
-
- }
- summary={
-
- Index a real folder of Markdown notes and expose it to Claude Desktop as
- searchable tools with the Model Context Protocol.
-
- }
- />
-
- Build a Recipe-Planner Agent
-
- }
- summary={
-
- Build a tool-using AI agent that suggests meals from ingredients you have on
- hand, grounded in a real local recipe database instead of guessing.
-
- }
- />
-
- Build a Meeting-Notes Summarizer
-
- }
- summary={
-
- Turn a raw meeting transcript into structured decisions, action items, and open
- questions using a free-tier LLM and a carefully designed JSON-extraction prompt.
-
- }
- />
-
- Build a GitHub Issue Triage Agent
-
- }
- summary={
-
- Fetch open issues from a real public GitHub repo and use a free-tier LLM to draft
- suggested triage labels for a human maintainer to review.
-
- }
- />
-
- Build a Voice-to-Task Agent
-
- }
- summary={
-
- Transcribe a voice memo locally and for free with OpenAI's open-source Whisper
- model, then use a free-tier LLM to turn it into a structured task list.
-
- }
- />
-
- Build a Study-Buddy Quiz Agent
-
- }
- summary={
-
- Turn your own study notes into an interactive quiz: a free-tier LLM writes
- questions grounded in your notes, then judges your typed answers.
-
- }
- />
-
- Turn a Codebase into a Knowledge Graph
-
- }
- summary={
-
- Parse a real codebase's Python files with the ast module, build a graph of its
- structure with networkx, and visualize and query it — no API key needed.
-
- }
- />
-
- Build a RAG-Backed Docs Q&A Discord Bot
-
- }
- summary={
-
- Wrap the RAG App project's retrieval pipeline in a live Discord bot that answers
- questions from a folder of documentation.
-
- }
- />
-
- Build a Personal Email-Triage Agent
-
- }
- summary={
-
- Categorize, prioritize, and draft replies for a batch of emails with a free-tier
- LLM — the agent never sends anything, only you do.
-
- }
- />
-
- Build a Multi-Agent Research Assistant
-
- }
- summary={
-
- Build a small multi-agent system — a planner, a researcher, and a writer — that
- breaks down a research question and synthesizes a final report, using deepagents
- sub-agents and a free-tier LLM.
-
- }
- />
-
- Build an Agentic Code Reviewer
-
- }
- summary={
-
- Build a CLI tool that reads a real git diff via subprocess and asks a free-tier
- LLM to review it like a human reviewer would.
-
- }
- />
-
- Build an MCP Server
-
- }
- summary={
-
- Build a Model Context Protocol server exposing your own tools, and connect it to a
- real AI client like Claude Desktop.
-
- }
- />
-
- Train Your First Machine Learning Model
-
- }
- summary={
-
- Go from describing data to predicting from it: train and compare binary
- classifiers with scikit-learn on the Titanic dataset.
-
- }
- />
-
- Build a RAG App Over Your Own Notes
-
- }
- summary={
-
- Chat with your own notes: local embeddings with sentence-transformers, NumPy
- similarity search, and a free-tier LLM for the final answer.
-
- }
- />
-
- Chat with Your PDFs
-
- }
- summary={
-
- Multi-document RAG over a folder of PDFs: page-aware chunking, local embeddings, and
- page-cited answers from a free-tier LLM.
-
- }
- />
-
- Scrape and Analyze a Live Website
-
- }
- summary={
-
- Scrape a real, scraping-friendly website with requests and BeautifulSoup, then
- clean and chart the results with pandas and matplotlib — no API key needed.
-
- }
- />
-
- Build a Job-Listing Aggregator
-
- }
- summary={
-
- Scrape multiple job-board-style sources, dedupe listings across them, and alert on
- new matches against a keyword filter — no API key needed.
-
- }
- />
-
- Fine-tune a Small Language Model
-
- }
- summary={
-
- Fine-tune a small open-source language model with LoRA using Unsloth, on a free
- Colab/Kaggle GPU.
-
- }
- />
-
- Build an AI Agent
-
- }
- summary={
-
- Install Python for real and build your first AI agent with LangChain's deepagents,
- using a free-tier API key.
-
- }
- />
-
- Build a Rate-Limited API Service
-
- }
- summary={
-
- Build a real FastAPI service wrapping your own dataset, with API-key auth and a
- sliding-window rate limiter built from scratch.
-
- }
- />
-
- Build a Personal Finance Agent
-
- }
- summary={
-
- Categorize a bank CSV export and flag spending anomalies, combining pandas with an
- LLM tool-calling agent.
-
- }
- />
-
- Build a Browser-Automation Agent
-
- }
- summary={
-
- Combine Playwright browser automation with a free-tier LLM tool-calling agent
- that fills out a real practice web form on its own.
-
- }
- />
-
- Count Objects in Real Time with a Webcam
-
- }
- summary={
-
- Count objects live from a webcam feed with OpenCV and a pretrained YOLO11n model, no
- API key or signup needed.
-
- }
- />
-
- Build a Wordle Clone
-
- }
- summary={
-
- Build a real terminal Wordle game from scratch: correct guess feedback, a custom
- word list, and persistent stats tracking across sessions.
-
- }
- />
-
- Build a Habit-Streak Visualizer
-
- }
- summary={
-
- Track daily habit check-ins locally and render a GitHub-contributions-graph-style
- calendar heatmap, for any habit you want to track.
-
- }
- />
-
+
+ );
+}
export default function ProgressPage(): ReactNode {
const courseComplete = useCourseComplete();
@@ -17,6 +104,11 @@ export default function ProgressPage(): ReactNode {
id: 'progressPage.pageDescription',
message: 'Track your course progress and badges.',
})}>
+ {/* Personal, localStorage-driven page — thin duplicate content with no
+ standalone SEO value, so keep it out of search results. */}
+
+
+
My Progress
@@ -41,6 +133,7 @@ export default function ProgressPage(): ReactNode {
)}
+
diff --git a/src/utils/storageKeys.ts b/src/utils/storageKeys.ts
index 2cf94fe..34a112d 100644
--- a/src/utils/storageKeys.ts
+++ b/src/utils/storageKeys.ts
@@ -9,6 +9,7 @@ export const STORAGE_KEYS = {
weeklyQuiz: 'pda-course:weekly-quiz',
badges: 'pda-course:badges',
projectProgress: 'pda-course:capstone-progress',
+ projectFavorites: 'pda-course:favorite-projects',
studentName: 'pda-course:student-name',
lastVisit: 'pda-course:last-visit',
studentId: 'pda-course:student-id',
diff --git a/static/llms.txt b/static/llms.txt
index 2007976..c1c56d3 100644
--- a/static/llms.txt
+++ b/static/llms.txt
@@ -2,13 +2,13 @@
> A free, browser-based Python and data analysis course. No installs required until the capstone. Runs entirely in-browser via self-hosted JupyterLite (Pyodide/WebAssembly), works offline once installed as a PWA. Available in English, Arabic, Spanish, and French.
-Two 5-week sections, each with a Normal track (guided fundamentals) and a Hard track (project-based, no shortcuts): Python 101 builds a tiny language model from scratch in pure Python; Data Analysis runs a full exploratory data analysis project with pandas. A Capstone project is added every year after the core curriculum, with past years' projects staying available.
+Two 5-week sections, each with a Normal track (guided fundamentals) and a Hard track (project-based, no shortcuts): Python 101 builds a tiny language model from scratch in pure Python; Data Analysis runs a full exploratory data analysis project with pandas. A growing library of real-world projects teaches the graduation step: installing Python for real and building something the in-browser playground can't run — new projects are added whenever a trending tool or framework is worth covering.
## Course
- [Python 101](https://pyda-course.online/docs/python-101): variables, control flow, data structures, functions, CSV files — Normal track for fundamentals, Hard track builds a tiny language model from scratch in pure Python.
- [Pandas & Data Analysis](https://pyda-course.online/docs/data-analysis): loading, cleaning, filtering, grouping real data — Normal track reproduces a Kaggle-style notebook, Hard track runs a full exploratory data analysis project.
-- [Capstone Projects](https://pyda-course.online/docs/bonus): install Python locally for real and build something the in-browser playground never could. A new project every year; past years stay available.
+- [Real-World Projects](https://pyda-course.online/docs/projects): install Python locally for real and build something the in-browser playground never could — AI agents, MCP servers, RAG apps, web scrapers, games, and more. Browse freely any time; no completion gate.
## About
diff --git a/tests/e2e/projects-gallery.spec.ts b/tests/e2e/projects-gallery.spec.ts
new file mode 100644
index 0000000..954330f
--- /dev/null
+++ b/tests/e2e/projects-gallery.spec.ts
@@ -0,0 +1,126 @@
+import {test, expect} from '@playwright/test';
+import {skipOnboarding} from './helpers';
+
+const TOTAL = 29;
+
+test.describe('projects gallery', () => {
+ test.beforeEach(async ({page}) => {
+ await skipOnboarding(page);
+ });
+
+ test('serves only the initial batch server-side (SSR) and loads more on scroll', async ({page, request}) => {
+ // SSR: the raw HTML must contain exactly the initial batch, not all projects.
+ // (React SSR omits attribute quotes, hence the optional quotes in the match.)
+ const raw = await (await request.get('/')).text();
+ expect(raw.match(/data-testid=["']?gallery-card/g)?.length).toBe(12);
+
+ await page.goto('/');
+ await expect(page.getByTestId('gallery-counter')).toHaveText(`Showing ${12} of ${TOTAL}`);
+
+ const cardCount = () => page.getByTestId('gallery-card').count();
+ const atEnd = async () => (await page.getByTestId('gallery-end').count()) > 0;
+
+ // Scroll down a viewport at a time; each batch the sentinel loads pushes
+ // it further down, so this keeps re-crossing its intersection boundary.
+ for (let i = 0; i < 20 && !(await atEnd()); i++) {
+ await page.evaluate(() => window.scrollBy(0, window.innerHeight));
+ await page.waitForTimeout(120);
+ }
+
+ await expect(page.getByTestId('gallery-end')).toBeVisible();
+ await expect(page.getByTestId('gallery-counter')).toHaveText(
+ `Showing ${TOTAL} of ${TOTAL}`,
+ );
+ await expect.poll(async () => cardCount()).toBe(TOTAL);
+ });
+
+ test('starring a card persists to localStorage and toggles off', async ({page}) => {
+ await page.goto('/');
+
+ const card = page.getByTestId('gallery-card').filter({
+ has: page.getByRole('heading', {name: 'Build a Dependency-Freshness Checker'}),
+ });
+ const star = card.getByTestId('favorite-button');
+ await expect(star).toHaveAttribute('aria-pressed', 'false');
+
+ await star.click();
+ await expect(star).toHaveAttribute('aria-pressed', 'true');
+
+ const stored = await page.evaluate(() =>
+ window.localStorage.getItem('pda-course:favorite-projects'),
+ );
+ expect(stored).toContain('2027-dependency-freshness-checker');
+
+ await star.click();
+ await expect(star).toHaveAttribute('aria-pressed', 'false');
+ });
+
+ test('favorites filter shows only starred projects', async ({page}) => {
+ await page.goto('/');
+
+ const card = page.getByTestId('gallery-card').filter({
+ has: page.getByRole('heading', {name: 'Build a Dependency-Freshness Checker'}),
+ });
+ await card.getByTestId('favorite-button').click();
+
+ await page.getByRole('button', {name: /Favorites/}).click();
+
+ await expect(page.getByTestId('gallery-counter')).toHaveText(`Showing ${1} of ${1}`);
+ await expect(page.getByTestId('gallery-card')).toHaveCount(1);
+ await expect(
+ page.getByRole('heading', {name: 'Build a Dependency-Freshness Checker'}),
+ ).toBeVisible();
+ });
+
+ test('favorites are shared across the homepage and the docs projects page', async ({page}) => {
+ await page.goto('/');
+
+ const card = page.getByTestId('gallery-card').filter({
+ has: page.getByRole('heading', {name: 'Build a Browser-Automation Agent'}),
+ });
+ await card.getByTestId('favorite-button').click();
+
+ await page.goto('/docs/projects');
+ const favoritesButton = page.getByRole('button', {name: /Favorites/});
+ await expect(favoritesButton).toContainText('1');
+ await favoritesButton.click();
+ await expect(page.getByTestId('gallery-counter')).toHaveText(`Showing ${1} of ${1}`);
+ await expect(
+ page.getByRole('heading', {name: 'Build a Browser-Automation Agent'}),
+ ).toBeVisible();
+ });
+
+ test('every card shows a level badge and a tools row', async ({page}) => {
+ await page.goto('/');
+
+ const firstCard = page.getByTestId('gallery-card').first();
+ await expect(firstCard.getByTestId('level-badge')).toBeVisible();
+ await expect(firstCard.getByTestId('card-tools')).toBeVisible();
+ await expect(firstCard.getByTestId('card-tools').getByRole('listitem').first()).toBeVisible();
+ await expect(firstCard.locator('svg').first()).toBeAttached();
+ });
+
+ test('level filter narrows to projects of that difficulty', async ({page}) => {
+ await page.goto('/');
+
+ await page.getByTestId('level-filter-beginner').click();
+ await expect(page.getByTestId('gallery-counter')).toHaveText(`Showing ${12} of ${17}`);
+ await expect(page.getByTestId('level-badge')).toHaveCount(12);
+ await expect(page.getByTestId('level-badge').first()).toHaveText('Beginner');
+
+ // A beginner project is in the first batch and now visible.
+ await expect(
+ page.getByRole('heading', {name: 'Build a Wordle Clone'}),
+ ).toBeVisible();
+
+ // Toggle off, then try Advanced (a single Hard-track project).
+ await page.getByTestId('level-filter-beginner').click();
+ await page.getByTestId('level-filter-advanced').click();
+ await expect(page.getByTestId('gallery-counter')).toHaveText(`Showing ${1} of ${1}`);
+ await expect(page.getByTestId('level-badge')).toHaveCount(1);
+ await expect(page.getByTestId('level-badge').first()).toHaveText('Advanced');
+ await expect(
+ page.getByRole('heading', {name: 'Fine-tune a Small Language Model'}),
+ ).toBeVisible();
+ });
+});