diff --git a/src/app/learn/page.tsx b/src/app/learn/page.tsx new file mode 100644 index 0000000..b04e75e --- /dev/null +++ b/src/app/learn/page.tsx @@ -0,0 +1,870 @@ +'use client'; + +import Link from 'next/link'; +import Image from 'next/image'; +import { useEffect, useMemo, useState } from 'react'; +import { + Bell, + Check, + CheckCircle2, + ChevronLeft, + ChevronRight, + Circle, + Clock3, + Copy, + GitBranch, + Lightbulb, + Lock, + PlayCircle, + ShieldCheck, + Sparkles, + Star, + Trophy, + Zap, +} from 'lucide-react'; + +type Module = { + title: string; + duration: string; + xp: number; + readTime: string; + intro: string; + concept: string; + body: string; + code: string; +}; + +type CourseDay = { + title: string; + subtitle: string; + accent: string; + modules: Module[]; +}; + +const STORAGE_KEY = 'mergeship-learn-progress-v1'; +const XP_TO_LEVEL_ONE = 500; + +const curriculum: CourseDay[] = [ + { + title: 'Git & GitHub Fundamentals', + subtitle: + 'Master the core concepts of version control. By the end of this day, you will comfortably initialize repositories, track changes, and understand the basic flow of Git.', + accent: 'from-emerald-400/25 via-cyan-400/10 to-transparent', + modules: [ + { + title: 'What is Git?', + duration: '15m', + xp: 20, + readTime: '15m', + intro: + 'Git is a distributed version control system. Imagine it as a highly detailed time machine for your codebase. It does not just save files; it takes snapshots of your entire project at specific points in time.', + concept: + 'Git tracks changes, not just files. When you commit, you are recording what was added, modified, or deleted since the last snapshot.', + body: 'To start using Git in a project, initialize a repository. This creates a hidden .git folder that stores all tracking data.', + code: '# Navigate to your project folder\ncd my-awesome-project\n\n# Initialize the Git repository\ngit init', + }, + { + title: 'Tracking Changes', + duration: '20m', + xp: 25, + readTime: '20m', + intro: + 'The working tree, staging area, and repository are the three places your changes move through before becoming permanent history.', + concept: + 'The staging area lets you choose exactly which changes belong in the next commit.', + body: 'Use status often. It is the fastest way to understand what Git sees and what is ready to be committed.', + code: '# See changed files\ngit status\n\n# Stage a specific file\ngit add README.md\n\n# Stage all current changes\ngit add .', + }, + { + title: 'Committing History', + duration: '25m', + xp: 30, + readTime: '25m', + intro: + 'Commits are named snapshots. A strong commit message explains why a change exists, not only what files moved.', + concept: 'Small, focused commits make review, rollback, and collaboration much easier.', + body: 'After staging your files, commit them with a clear message that future you and maintainers can scan quickly.', + code: '# Commit staged changes\ngit commit -m "Add onboarding notes"\n\n# View recent history\ngit log --oneline --decorate -5', + }, + { + title: 'Ignoring Files', + duration: '10m', + xp: 20, + readTime: '10m', + intro: + 'Not every file belongs in source control. Generated builds, secrets, dependency folders, and local machine files should stay out of commits.', + concept: '.gitignore keeps noisy or sensitive files from entering your project history.', + body: 'Create a .gitignore file at the project root and add patterns for files Git should ignore.', + code: '# Common JavaScript ignores\nnode_modules/\n.env.local\n.next/\ndist/\n\n# Check ignored files\ngit status --ignored', + }, + { + title: 'Viewing Logs', + duration: '15m', + xp: 25, + readTime: '15m', + intro: + 'Git history is a map. Reading it well helps you understand decisions, find regressions, and prepare cleaner pull requests.', + concept: 'Logs are most useful when you filter them to the question you are asking.', + body: 'Use compact logs for scanning and file-specific logs when investigating a particular area.', + code: '# Compact branch history\ngit log --oneline --graph --decorate\n\n# History for one file\ngit log -- src/app/page.tsx', + }, + ], + }, + { + title: 'Branching Strategies', + subtitle: + 'Practice isolated feature work with branches, switching contexts, and keeping a clean line of development.', + accent: 'from-lime-400/20 via-emerald-400/10 to-transparent', + modules: [ + { + title: 'Create a Branch', + duration: '15m', + xp: 25, + readTime: '15m', + intro: 'Branches let you explore a change without disrupting the main project line.', + concept: 'A branch is a movable pointer to a commit, so creating one is lightweight.', + body: 'Name branches after the work they contain so teammates can quickly understand intent.', + code: 'git checkout -b feature/learn-dashboard\n\ngit branch --show-current', + }, + { + title: 'Switch Contexts', + duration: '15m', + xp: 20, + readTime: '15m', + intro: + 'Switching branches lets you move between tasks while Git updates your working tree.', + concept: + 'Commit or stash work before switching when changes conflict with the target branch.', + body: 'Use the newer switch command for clearer branch movement.', + code: 'git switch main\n\ngit switch feature/learn-dashboard', + }, + { + title: 'Merge Work', + duration: '20m', + xp: 30, + readTime: '20m', + intro: + 'Merging combines work from one branch into another while preserving project history.', + concept: 'Merge from a clean target branch and resolve conflicts with intent.', + body: 'After merging, run tests before pushing so the integration is verified locally.', + code: 'git switch main\n\ngit merge feature/learn-dashboard\n\nnpm run test', + }, + { + title: 'Rebase Basics', + duration: '20m', + xp: 35, + readTime: '20m', + intro: 'Rebasing replays commits on top of a new base, creating a straighter history.', + concept: 'Rebase private branches freely; be careful rebasing shared branches.', + body: 'Use rebase to refresh your feature branch before opening a pull request.', + code: 'git fetch origin\n\ngit rebase origin/main', + }, + { + title: 'Clean Up Branches', + duration: '10m', + xp: 20, + readTime: '10m', + intro: 'Deleting finished branches keeps your workspace readable and reduces mistakes.', + concept: 'Local and remote branches are separate references.', + body: 'Delete branches after the work has merged and the remote no longer needs them.', + code: 'git branch -d feature/learn-dashboard\n\ngit push origin --delete feature/learn-dashboard', + }, + ], + }, + { + title: 'Collaboration & PRs', + subtitle: + 'Turn local changes into maintainable pull requests with review-ready commits, context, and discussion.', + accent: 'from-sky-400/20 via-emerald-400/10 to-transparent', + modules: [ + { + title: 'Fork and Clone', + duration: '15m', + xp: 25, + readTime: '15m', + intro: 'Forking gives you your own copy of a repository so you can contribute safely.', + concept: 'Your fork is origin; the original project is usually tracked as upstream.', + body: 'Clone your fork locally and add upstream so you can keep it current.', + code: 'git clone https://github.com/you/project.git\n\ncd project\n\ngit remote add upstream https://github.com/org/project.git', + }, + { + title: 'Sync Upstream', + duration: '15m', + xp: 25, + readTime: '15m', + intro: 'Open source projects move quickly. Syncing reduces conflicts before review.', + concept: 'Fetch first, then merge or rebase the upstream branch you target.', + body: 'Update your local main before branching for new work.', + code: 'git fetch upstream\n\ngit switch main\n\ngit merge upstream/main', + }, + { + title: 'Open a PR', + duration: '20m', + xp: 35, + readTime: '20m', + intro: 'A pull request packages code, context, and review conversation in one place.', + concept: 'Good PR descriptions explain scope, testing, and any tradeoffs.', + body: 'Push your branch, then open a PR against the project branch requested by maintainers.', + code: 'git push -u origin feature/fix-empty-state\n\ngh pr create --fill', + }, + { + title: 'Respond to Review', + duration: '20m', + xp: 35, + readTime: '20m', + intro: + 'Review is collaboration. Keep responses specific and update code in focused commits.', + concept: 'Acknowledge feedback even when you choose a different approach.', + body: 'After changes, push again and leave a short note about what changed.', + code: 'git add src/app/learn/page.tsx\n\ngit commit -m "Refine learn module states"\n\ngit push', + }, + { + title: 'Squash and Merge', + duration: '15m', + xp: 30, + readTime: '15m', + intro: 'Some projects prefer a clean final history with one commit per PR.', + concept: 'Follow the repository contribution guide for merge style.', + body: 'When maintainers ask for cleanup, interactive rebase can combine local commits.', + code: 'git rebase -i upstream/main\n\ngit push --force-with-lease', + }, + ], + }, + { + title: 'Advanced Workflows', + subtitle: + 'Use advanced Git tools to inspect bugs, recover work, and move carefully through complex histories.', + accent: 'from-teal-400/20 via-green-400/10 to-transparent', + modules: [ + { + title: 'Stash Work', + duration: '10m', + xp: 20, + readTime: '10m', + intro: 'Stashing shelves unfinished work so you can switch tasks quickly.', + concept: 'A stash is temporary. Apply it back as soon as the interruption is handled.', + body: 'Name stashes when you have more than one active thread of work.', + code: 'git stash push -m "wip learn layout"\n\ngit stash list\n\ngit stash pop', + }, + { + title: 'Cherry Pick', + duration: '15m', + xp: 25, + readTime: '15m', + intro: 'Cherry-pick copies one commit from another branch into your current branch.', + concept: 'Use it for surgical moves, not as a default collaboration model.', + body: 'Find the commit hash, switch to the target branch, then cherry-pick it.', + code: 'git log --oneline\n\ngit switch release\n\ngit cherry-pick abc1234', + }, + { + title: 'Bisect Bugs', + duration: '25m', + xp: 40, + readTime: '25m', + intro: + 'Bisect performs a guided binary search through history to find when a bug appeared.', + concept: 'Reliable test commands make bisect extremely powerful.', + body: 'Mark known good and bad commits, then test each commit Git checks out.', + code: 'git bisect start\n\ngit bisect bad\n\ngit bisect good v1.0.0\n\ngit bisect reset', + }, + { + title: 'Reflog Recovery', + duration: '20m', + xp: 35, + readTime: '20m', + intro: 'Reflog records where your branch tips and HEAD have been locally.', + concept: 'Reflog can recover commits that no branch currently points to.', + body: 'Use reflog when a reset, rebase, or checkout moved you away from important work.', + code: 'git reflog\n\ngit switch -c recover-work HEAD@{2}', + }, + { + title: 'Tag Releases', + duration: '15m', + xp: 25, + readTime: '15m', + intro: 'Tags mark important points in history, commonly releases.', + concept: 'Annotated tags include metadata and are preferred for releases.', + body: 'Create tags after tests pass and push them intentionally.', + code: 'git tag -a v1.2.0 -m "Release v1.2.0"\n\ngit push origin v1.2.0', + }, + ], + }, + { + title: 'Final Project', + subtitle: + 'Complete a contribution simulation from issue selection to pull request polish and self-review.', + accent: 'from-emerald-400/20 via-yellow-400/10 to-transparent', + modules: [ + { + title: 'Pick an Issue', + duration: '15m', + xp: 30, + readTime: '15m', + intro: 'A good first issue is scoped, reproducible, and aligned with project needs.', + concept: 'Read comments before starting so you do not duplicate active work.', + body: 'Capture acceptance criteria before writing code.', + code: 'gh issue view 273\n\ngh issue list --label feature --state open', + }, + { + title: 'Plan the Change', + duration: '20m', + xp: 35, + readTime: '20m', + intro: 'Planning keeps implementation narrow and reviewable.', + concept: 'The best plan names the files, behavior, and verification path.', + body: 'Write a short checklist and keep it updated as you work.', + code: 'git checkout -b feature/interactive-learn-page\n\nnpm run lint', + }, + { + title: 'Build the Feature', + duration: '30m', + xp: 45, + readTime: '30m', + intro: 'Implementation should follow the project architecture and design language.', + concept: 'Interactive UI needs explicit states for active, locked, and complete paths.', + body: 'Build in small increments and verify the main workflow after each piece lands.', + code: 'npm run dev\n\n# Visit the local route\n# http://localhost:3001/learn', + }, + { + title: 'Self Review', + duration: '20m', + xp: 35, + readTime: '20m', + intro: 'Self-review catches rough edges before maintainers spend review time.', + concept: 'Review from the user journey first, then from the diff.', + body: 'Check empty states, disabled states, persistence, and responsive layout.', + code: 'npm run lint\n\nnpm run typecheck\n\nnpm run build', + }, + { + title: 'Ship the PR', + duration: '15m', + xp: 55, + readTime: '15m', + intro: 'A polished PR makes it easy for maintainers to say yes.', + concept: 'Summary plus tests is the minimum useful review context.', + body: 'Open the PR with a focused title and clear verification notes.', + code: 'git status\n\ngit push -u origin feature/interactive-learn-page\n\ngh pr create', + }, + ], + }, +]; + +function moduleKey(dayIndex: number, moduleIndex: number) { + return `${dayIndex}-${moduleIndex}`; +} + +function isBrowserStorageAvailable() { + return typeof window !== 'undefined' && Boolean(window.localStorage); +} + +export default function LearnPage() { + const [activeDayIndex, setActiveDayIndex] = useState(0); + const [activeModuleIndex, setActiveModuleIndex] = useState(0); + const [completed, setCompleted] = useState>({}); + const [copiedKey, setCopiedKey] = useState(null); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + if (!isBrowserStorageAvailable()) return; + + const saved = window.localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved) as { + completed?: Record; + day?: number; + module?: number; + }; + const savedDay = Math.min(Math.max(parsed.day ?? 0, 0), curriculum.length - 1); + const savedModuleCount = curriculum[savedDay]?.modules.length ?? 1; + setCompleted(parsed.completed ?? {}); + setActiveDayIndex(savedDay); + setActiveModuleIndex(Math.min(Math.max(parsed.module ?? 0, 0), savedModuleCount - 1)); + } catch { + window.localStorage.removeItem(STORAGE_KEY); + } + } + setLoaded(true); + }, []); + + useEffect(() => { + if (!loaded || !isBrowserStorageAvailable()) return; + + window.localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + completed, + day: activeDayIndex, + module: activeModuleIndex, + }), + ); + }, [activeDayIndex, activeModuleIndex, completed, loaded]); + + const totalXp = useMemo( + () => + curriculum.reduce( + (total, day, dayIndex) => + total + + day.modules.reduce( + (dayTotal, module, moduleIndex) => + dayTotal + (completed[moduleKey(dayIndex, moduleIndex)] ? module.xp : 0), + 0, + ), + 0, + ), + [completed], + ); + + const firstDay = curriculum[0]!; + const currentDay = curriculum[activeDayIndex] ?? firstDay; + const currentModule = currentDay.modules[activeModuleIndex] ?? currentDay.modules[0]!; + const currentKey = moduleKey(activeDayIndex, activeModuleIndex); + const completedCount = Object.values(completed).filter(Boolean).length; + const totalModules = curriculum.reduce((sum, day) => sum + day.modules.length, 0); + const level = Math.floor(totalXp / XP_TO_LEVEL_ONE); + const xpWithinLevel = totalXp % XP_TO_LEVEL_ONE; + const profileScore = 24 + Math.floor(totalXp / 10); + + const isDayComplete = (dayIndex: number) => + curriculum[dayIndex]?.modules.every( + (_, moduleIndex) => completed[moduleKey(dayIndex, moduleIndex)], + ) ?? false; + + const isDayUnlocked = (dayIndex: number) => dayIndex === 0 || isDayComplete(dayIndex - 1); + + const isModuleUnlocked = (dayIndex: number, moduleIndex: number) => + isDayUnlocked(dayIndex) && + (moduleIndex === 0 || Boolean(completed[moduleKey(dayIndex, moduleIndex - 1)])); + + const selectDay = (dayIndex: number) => { + if (!isDayUnlocked(dayIndex)) return; + setActiveDayIndex(dayIndex); + setActiveModuleIndex(0); + }; + + const selectModule = (moduleIndex: number) => { + if (!isModuleUnlocked(activeDayIndex, moduleIndex)) return; + setActiveModuleIndex(moduleIndex); + }; + + const completeModule = () => { + setCompleted((progress) => ({ + ...progress, + [currentKey]: true, + })); + + const nextModuleIndex = activeModuleIndex + 1; + if (nextModuleIndex < currentDay.modules.length) { + setActiveModuleIndex(nextModuleIndex); + return; + } + + const nextDayIndex = activeDayIndex + 1; + if (nextDayIndex < curriculum.length) { + setActiveDayIndex(nextDayIndex); + setActiveModuleIndex(0); + } + }; + + const navigateModule = (direction: -1 | 1) => { + const target = activeModuleIndex + direction; + if ( + target < 0 || + target >= currentDay.modules.length || + !isModuleUnlocked(activeDayIndex, target) + ) { + return; + } + setActiveModuleIndex(target); + }; + + const copyCode = async () => { + const copyKey = currentKey; + try { + await navigator.clipboard.writeText(currentModule.code); + setCopiedKey(copyKey); + window.setTimeout(() => setCopiedKey(null), 1400); + } catch { + setCopiedKey(null); + } + }; + + return ( +
+
+
+
+
+ + MergeShip + + + + +
+
+ {xpWithinLevel} / {XP_TO_LEVEL_ONE} XP TO L{level + 1} +
+
+
+
+ + +
+ L{level} Newcomer +
+ Profile avatar +
+
+
+ +
+ + +
+
+ Course / Day {activeDayIndex + 1}{' '} + /{' '} + Module {activeModuleIndex + 1} +
+ + +
+
+
+
+
+
+
+
+
+
+ Day {activeDayIndex + 1} of {curriculum.length} +
+

{currentDay.title}

+

{currentDay.subtitle}

+
+ + + +
+
+

{currentModule.title}

+

+ Module {activeModuleIndex + 1} / Estimated Time: {currentModule.readTime} +

+
+ + {completed[currentKey] ? 'Completed' : 'Active'} + +
+ +
+

{currentModule.intro}

+
+
+ +
+

+ Key Concept +

+

{currentModule.concept}

+
+
+
+

{currentModule.body}

+
+ +
+
+ bash + +
+
+                  {currentModule.code}
+                
+
+ +
+ +
+
+ +
+ + +
+ {currentDay.modules.map((_, moduleIndex) => { + const unlocked = isModuleUnlocked(activeDayIndex, moduleIndex); + return ( +
+ + +
+
+ + +
+ +
+
+ + + +
+

Daily Login Bonus

+

+20 XP Earned

+
+
+
+ +
+ + {completedCount} / {totalModules} modules complete +
+
+
+ ); +} + +function Panel({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return ( +
+ {children} +
+ ); +} + +function Stat({ value }: { value: string }) { + return ( +
{value}
+ ); +} + +function Reward({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +}