Add proper diff viewer with syntax highlighting

This commit is contained in:
Oracle 2026-05-03 15:36:14 +02:00
parent d9c27b4353
commit 60eaae5371
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
23 changed files with 2358 additions and 870 deletions

View file

@ -26,9 +26,9 @@ src/
pages/ pages/
index.astro # challenge listing + hero index.astro # challenge listing + hero
profile.astro # user profile / XP dashboard profile.astro # user profile / XP dashboard
challenges/[slug].astro # single challenge view with DiffViewer challenges/[slug].astro # single challenge view with side-by-side DiffViewer + submit fix panel
components/ components/
DiffViewer.astro # side-by-side code diff UI DiffViewer.astro # side-by-side diff: left=read-only buggy code, right=editable fix with real-time diff highlighting
Welcome.astro # legacy starter component Welcome.astro # legacy starter component
layouts/Layout.astro # root layout wrapping header/nav/footer layouts/Layout.astro # root layout wrapping header/nav/footer
styles/global.css # global styles styles/global.css # global styles

View file

@ -5,6 +5,7 @@
"": { "": {
"name": "pr-dojo", "name": "pr-dojo",
"dependencies": { "dependencies": {
"@astrojs/prism": "^4.0.1",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"astro": "^6.1.7", "astro": "^6.1.7",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",

File diff suppressed because one or more lines are too long

1
dist/_astro/Layout.CjyFoliM.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">About PR Dojo</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <p> <strong class="text-[#c9d1d9]">PR Dojo</strong> is a practice platform for developers who want to sharpen their code review skills. We believe that the best way to become a better reviewer is by practicing on real-world code that didn't make it through review. <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">About PR Dojo</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <p> <strong class="text-[#c9d1d9]">PR Dojo</strong> is a practice platform for developers who want to sharpen their code review skills. We believe that the best way to become a better reviewer is by practicing on real-world code that didn't make it through review.
</p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Our Mission</h2> <p> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Our Mission</h2> <p>
Every day, thousands of pull requests are rejected, revised, or merged with bugs. These rejected PRs contain valuable learning opportunities — real bugs, real edge cases, real design mistakes. PR Dojo curates these mistakes into structured challenges where you can practice identifying issues and writing fixes. Every day, thousands of pull requests are rejected, revised, or merged with bugs. These rejected PRs contain valuable learning opportunities — real bugs, real edge cases, real design mistakes. PR Dojo curates these mistakes into structured challenges where you can practice identifying issues and writing fixes.
</p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">How It Works</h2> <div class="grid gap-4 mt-4"> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6"> <h3 class="text-[#58a6ff] font-semibold mb-2">1. Pick a Challenge</h3> <p class="text-sm">Browse our collection of buggy code snippets sourced from real rejected PRs. Each challenge includes a description, difficulty rating, and bug type categorization.</p> </div> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6"> <h3 class="text-[#58a6ff] font-semibold mb-2">2. Find the Bugs</h3> <p class="text-sm">Review the code carefully, looking for logic errors, security vulnerabilities, performance issues, and style problems. Use hints if you get stuck.</p> </div> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6"> <h3 class="text-[#58a6ff] font-semibold mb-2">3. Submit Your Fix</h3> <p class="text-sm">Write a patch that resolves the identified issues. Submit your fix and earn XP based on the difficulty and quality of your solution.</p> </div> </div> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Why Practice Code Review?</h2> <p> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">How It Works</h2> <div class="grid gap-4 mt-4"> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6"> <h3 class="text-[#58a6ff] font-semibold mb-2">1. Pick a Challenge</h3> <p class="text-sm">Browse our collection of buggy code snippets sourced from real rejected PRs. Each challenge includes a description, difficulty rating, and bug type categorization.</p> </div> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6"> <h3 class="text-[#58a6ff] font-semibold mb-2">2. Find the Bugs</h3> <p class="text-sm">Review the code carefully, looking for logic errors, security vulnerabilities, performance issues, and style problems. Use hints if you get stuck.</p> </div> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6"> <h3 class="text-[#58a6ff] font-semibold mb-2">3. Submit Your Fix</h3> <p class="text-sm">Write a patch that resolves the identified issues. Submit your fix and earn XP based on the difficulty and quality of your solution.</p> </div> </div> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Why Practice Code Review?</h2> <p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/faq/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">Imprint</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Information according to § 5 TMG</h2> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6 space-y-3"> <p><strong class="text-[#c9d1d9]">PR Dojo</strong></p> <p>Project Lead: [Your Name]</p> <p>Address: [Your Address]</p> <p>[Postal Code] [City]</p> <p>[Country]</p> </div> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Contact</h2> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6 space-y-2"> <p>Email: [contact@prdojo.example.com](mailto:contact@prdojo.example.com)</p> <p>GitHub: [github.com/prdojo](https://github.com/prdojo)</p> </div> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Disclaimer</h2> <p> <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">Imprint</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Information according to § 5 TMG</h2> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6 space-y-3"> <p><strong class="text-[#c9d1d9]">PR Dojo</strong></p> <p>Project Lead: [Your Name]</p> <p>Address: [Your Address]</p> <p>[Postal Code] [City]</p> <p>[Country]</p> </div> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Contact</h2> <div class="bg-[#161b22] border border-[#30363d] rounded-lg p-6 space-y-2"> <p>Email: [contact@prdojo.example.com](mailto:contact@prdojo.example.com)</p> <p>GitHub: [github.com/prdojo](https://github.com/prdojo)</p> </div> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Disclaimer</h2> <p>
The content on this site is provided with the utmost care. However, we do not guarantee the accuracy, completeness, or timeliness of any content on this website. The content on this site is provided with the utmost care. However, we do not guarantee the accuracy, completeness, or timeliness of any content on this website.
</p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Links and References</h2> <p> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-8 mb-4">Links and References</h2> <p>
This website contains links to external websites of third parties, over whose content we have no influence. Therefore, we cannot assume any liability for these external contents. The respective providers or operators of the pages are always responsible for the content of the linked pages. This website contains links to external websites of third parties, over whose content we have no influence. Therefore, we cannot assume any liability for these external contents. The respective providers or operators of the pages are always responsible for the content of the linked pages.

2
dist/index.html vendored
View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> <div class="flex items-center gap-3"> <svg class="w-8 h-8 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> </svg> <span class="text-xl font-semibold text-[#c9d1d9]">PR Dojo</span> </div> <nav class="flex items-center gap-4"> <a href="/" class="text-[#58a6ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">Challenges</a> <a href="/profile" class="text-[#8b949e] hover:text-[#c9d1d9] text-sm font-medium no-underline focus:outline-none focus:ring-0">Profile</a> <a href="/signin" class="text-[#58a6ff] hover:text-[#79c0ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">Sign In</a> </nav> </div> </header> <main class="max-w-6xl mx-auto px-4 py-8"> <!-- Hero Section --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-8 mb-8"> <h1 class="text-3xl font-bold text-[#c9d1d9] mb-3">Code Review Hunter</h1> <p class="text-[#8b949e] mb-6 text-lg"> <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> <div class="flex items-center gap-3"> <svg class="w-8 h-8 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> </svg> <span class="text-xl font-semibold text-[#c9d1d9]">PR Dojo</span> </div> <nav class="flex items-center gap-4"> <a href="/" class="text-[#58a6ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">Challenges</a> <a href="/profile" class="text-[#8b949e] hover:text-[#c9d1d9] text-sm font-medium no-underline focus:outline-none focus:ring-0">Profile</a> <a href="/signin" class="text-[#58a6ff] hover:text-[#79c0ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">Sign In</a> </nav> </div> </header> <main class="max-w-6xl mx-auto px-4 py-8"> <!-- Hero Section --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-8 mb-8"> <h1 class="text-3xl font-bold text-[#c9d1d9] mb-3">Code Review Hunter</h1> <p class="text-[#8b949e] mb-6 text-lg">
Master code review skills by fixing rejected PRs. Earn XP, climb ranks, and become a bug detection expert. Master code review skills by fixing rejected PRs. Earn XP, climb ranks, and become a bug detection expert.
</p> <div class="flex gap-4"> <a href="/challenges/1" class="bg-[#1f6feb] hover:bg-[#388bfd] text-white px-6 py-2 rounded-md text-sm font-semibold transition-colors no-underline focus:outline-none focus:ring-0"> </p> <div class="flex gap-4"> <a href="/challenges/1" class="bg-[#1f6feb] hover:bg-[#388bfd] text-white px-6 py-2 rounded-md text-sm font-semibold transition-colors no-underline focus:outline-none focus:ring-0">
Start Challenge Start Challenge

View file

@ -1,5 +1,5 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">Privacy Policy</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <p> <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">Privacy Policy</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <p>
Last updated: <strong class="text-[#c9d1d9]">May 2, 2026</strong> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Introduction</h2> <p> Last updated: <strong class="text-[#c9d1d9]">May 3, 2026</strong> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Introduction</h2> <p>
PR Dojo ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how your personal information is collected, used, and disclosed by PR Dojo. PR Dojo ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how your personal information is collected, used, and disclosed by PR Dojo.
</p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Information We Collect</h2> <p> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Information We Collect</h2> <p>
PR Dojo is currently in its early stages and does not collect personal data beyond what is necessary for functionality. As we grow, we may collect: PR Dojo is currently in its early stages and does not collect personal data beyond what is necessary for functionality. As we grow, we may collect:

View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3"> <a href="/" class="text-[#58a6ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">← Back to Challenges</a> </div> </header> <main class="max-w-6xl mx-auto px-4 py-6"> <!-- Profile Header --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <div class="flex items-center gap-6"> <div class="w-20 h-20 bg-[#21262d] rounded-full flex items-center justify-center"> <svg id="avatar-placeholder" class="w-12 h-12 text-[#8b949e]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path> </svg> </div> <div> <h1 id="profile-name" class="text-2xl font-semibold text-[#c9d1d9]">Loading...</h1> <p id="profile-title" class="text-[#8b949e] text-sm mt-1">Code Review Practitioner</p> </div> </div> </div> <!-- Circular Progress Bars --> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> <!-- XP Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Total XP</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="xp-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-xp" class="text-2xl font-bold text-[#58a6ff]">?/?</span> <span class="text-[#8b949e] text-xs mt-1">XP</span> </div> </div> </div> <!-- Challenges Solved --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Challenges Solved</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#a5d6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="solved-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-solved" class="text-2xl font-bold text-[#c9d1d9]">?/<span class="text-[#8b949e] text-lg">?</span></span> <span class="text-[#8b949e] text-xs mt-1">Solved</span> </div> </div> </div> <!-- Streak Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Current Streak</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#79c0ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="streak-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-streak" class="text-3xl font-bold text-[#79c0ff]">?<span class="text-[#8b949e] text-lg">d</span></span> <span class="text-[#8b949e] text-xs mt-1">days</span> </div> </div> </div> </div> <!-- Badges --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Badges</h2> <div id="badge-list" class="flex flex-wrap gap-3"> <div class="text-center text-[#8b949e] py-4"> <p>Loading badges...</p> </div> </div> </div> <!-- Recent Activity --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Recent Activity</h2> <div id="activity-list" class="space-y-3"> <div class="text-center text-[#8b949e] py-8"> <svg class="w-12 h-12 mx-auto mb-3 text-[#30363d]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path> </svg> <p>No challenges completed yet</p> <a href="/challenges/1" class="text-[#58a6ff] mt-2 inline-block no-underline"> <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3"> <a href="/" class="text-[#58a6ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">← Back to Challenges</a> </div> </header> <main class="max-w-6xl mx-auto px-4 py-6"> <!-- Profile Header --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <div class="flex items-center gap-6"> <div class="w-20 h-20 bg-[#21262d] rounded-full flex items-center justify-center"> <svg id="avatar-placeholder" class="w-12 h-12 text-[#8b949e]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path> </svg> </div> <div> <h1 id="profile-name" class="text-2xl font-semibold text-[#c9d1d9]">Loading...</h1> <p id="profile-title" class="text-[#8b949e] text-sm mt-1">Code Review Practitioner</p> </div> </div> </div> <!-- Circular Progress Bars --> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> <!-- XP Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Total XP</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="xp-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-xp" class="text-2xl font-bold text-[#58a6ff]">?/?</span> <span class="text-[#8b949e] text-xs mt-1">XP</span> </div> </div> </div> <!-- Challenges Solved --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Challenges Solved</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="solved-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-solved" class="text-2xl font-bold text-[#c9d1d9]">?/<span class="text-[#8b949e] text-lg">?</span></span> <span class="text-[#8b949e] text-xs mt-1">Solved</span> </div> </div> </div> <!-- Streak Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Current Streak</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="streak-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-streak" class="text-3xl font-bold text-[#79c0ff]">?<span class="text-[#8b949e] text-lg">d</span></span> <span class="text-[#8b949e] text-xs mt-1">days</span> </div> </div> </div> </div> <!-- Badges --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Badges</h2> <div id="badge-list" class="flex flex-wrap gap-3"> <div class="text-center text-[#8b949e] py-4"> <p>Loading badges...</p> </div> </div> </div> <!-- Recent Activity --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Recent Activity</h2> <div id="activity-list" class="space-y-3"> <div class="text-center text-[#8b949e] py-8"> <svg class="w-12 h-12 mx-auto mb-3 text-[#30363d]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path> </svg> <p>No challenges completed yet</p> <a href="/challenges/1" class="text-[#58a6ff] mt-2 inline-block no-underline">
Start your first challenge → Start your first challenge →
</a> </div> </div> </div> </main> </div> <script client:load> </a> </div> </div> </div> </main> </div> <script client:load>
(function() { (function() {

View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen bg-[#0d1117]"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> <a href="/" class="flex items-center gap-3 no-underline focus:outline-none focus:ring-0"> <svg class="w-8 h-8 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> </svg> <span class="text-xl font-semibold text-[#c9d1d9]">PR Dojo</span> </a> <nav class="flex items-center gap-4"> <a href="/" class="text-[#8b949e] hover:text-[#c9d1d9] text-sm font-medium no-underline focus:outline-none focus:ring-0">Home</a> </nav> </div> </header> <main class="flex items-center justify-center px-4 py-16"> <div class="w-full max-w-md"> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-8"> <div class="text-center mb-8"> <h1 class="text-2xl font-semibold text-[#c9d1d9] mb-2">Sign in to PR Dojo</h1> <p class="text-[#8b949e] text-sm">Continue your code review practice</p> </div> <form on:submit|preventDefault="(e) => { <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen bg-[#0d1117]"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> <a href="/" class="flex items-center gap-3 no-underline focus:outline-none focus:ring-0"> <svg class="w-8 h-8 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> </svg> <span class="text-xl font-semibold text-[#c9d1d9]">PR Dojo</span> </a> <nav class="flex items-center gap-4"> <a href="/" class="text-[#8b949e] hover:text-[#c9d1d9] text-sm font-medium no-underline focus:outline-none focus:ring-0">Home</a> </nav> </div> </header> <main class="flex items-center justify-center px-4 py-16"> <div class="w-full max-w-md"> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-8"> <div class="text-center mb-8"> <h1 class="text-2xl font-semibold text-[#c9d1d9] mb-2">Sign in to PR Dojo</h1> <p class="text-[#8b949e] text-sm">Continue your code review practice</p> </div> <form on:submit|preventDefault="(e) => {
e.preventDefault(); e.preventDefault();
alert(&#34;Sign in is not available yet. Backend API is not implemented.&#34;); alert(&#34;Sign in is not available yet. Backend API is not implemented.&#34;);
}"> <!-- Email --> <div class="mb-4"> <label for="email" class="block text-[#c9d1d9] text-sm font-medium mb-2">Email</label> <input type="email" id="email" name="email" required placeholder="you@example.com" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Password --> <div class="mb-4"> <label for="password" class="block text-[#c9d1d9] text-sm font-medium mb-2">Password</label> <input type="password" id="password" name="password" required placeholder="Enter your password" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Remember me --> <div class="flex items-center justify-between mb-6"> <label class="flex items-center gap-2"> <input type="checkbox" name="remember" class="w-4 h-4 rounded bg-[#0d1117] border-[#30363d] text-[#58a6ff] focus:ring-[#58a6ff] focus:ring-offset-0"> <span class="text-[#8b949e] text-sm">Remember me</span> </label> <a href="#" class="text-[#58a6ff] hover:text-[#79c0ff] text-sm no-underline focus:outline-none focus:ring-0" onclick="event.preventDefault(); alert('Password reset is not available yet. Backend API is not implemented.');"> }"> <!-- Email --> <div class="mb-4"> <label for="email" class="block text-[#c9d1d9] text-sm font-medium mb-2">Email</label> <input type="email" id="email" name="email" required placeholder="you@example.com" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Password --> <div class="mb-4"> <label for="password" class="block text-[#c9d1d9] text-sm font-medium mb-2">Password</label> <input type="password" id="password" name="password" required placeholder="Enter your password" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Remember me --> <div class="flex items-center justify-between mb-6"> <label class="flex items-center gap-2"> <input type="checkbox" name="remember" class="w-4 h-4 rounded bg-[#0d1117] border-[#30363d] text-[#58a6ff] focus:ring-[#58a6ff] focus:ring-offset-0"> <span class="text-[#8b949e] text-sm">Remember me</span> </label> <a href="#" class="text-[#58a6ff] hover:text-[#79c0ff] text-sm no-underline focus:outline-none focus:ring-0" onclick="event.preventDefault(); alert('Password reset is not available yet. Backend API is not implemented.');">

View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen bg-[#0d1117]"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> <a href="/" class="flex items-center gap-3 no-underline focus:outline-none focus:ring-0"> <svg class="w-8 h-8 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> </svg> <span class="text-xl font-semibold text-[#c9d1d9]">PR Dojo</span> </a> <nav class="flex items-center gap-4"> <a href="/" class="text-[#8b949e] hover:text-[#c9d1d9] text-sm font-medium no-underline focus:outline-none focus:ring-0">Home</a> </nav> </div> </header> <main class="flex items-center justify-center px-4 py-16"> <div class="w-full max-w-md"> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-8"> <div class="text-center mb-8"> <h1 class="text-2xl font-semibold text-[#c9d1d9] mb-2">Create your account</h1> <p class="text-[#8b949e] text-sm">Join PR Dojo to track your progress and earn XP</p> </div> <form on:submit|preventDefault="(e) => { <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen bg-[#0d1117]"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> <a href="/" class="flex items-center gap-3 no-underline focus:outline-none focus:ring-0"> <svg class="w-8 h-8 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> </svg> <span class="text-xl font-semibold text-[#c9d1d9]">PR Dojo</span> </a> <nav class="flex items-center gap-4"> <a href="/" class="text-[#8b949e] hover:text-[#c9d1d9] text-sm font-medium no-underline focus:outline-none focus:ring-0">Home</a> </nav> </div> </header> <main class="flex items-center justify-center px-4 py-16"> <div class="w-full max-w-md"> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-8"> <div class="text-center mb-8"> <h1 class="text-2xl font-semibold text-[#c9d1d9] mb-2">Create your account</h1> <p class="text-[#8b949e] text-sm">Join PR Dojo to track your progress and earn XP</p> </div> <form on:submit|preventDefault="(e) => {
e.preventDefault(); e.preventDefault();
alert(&#34;Sign up is not available yet. Backend API is not implemented.&#34;); alert(&#34;Sign up is not available yet. Backend API is not implemented.&#34;);
}"> <!-- Username --> <div class="mb-4"> <label for="username" class="block text-[#c9d1d9] text-sm font-medium mb-2">Username</label> <input type="text" id="username" name="username" required placeholder="Enter your username" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Email --> <div class="mb-4"> <label for="email" class="block text-[#c9d1d9] text-sm font-medium mb-2">Email</label> <input type="email" id="email" name="email" required placeholder="you@example.com" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Password --> <div class="mb-4"> <label for="signup-password" class="block text-[#c9d1d9] text-sm font-medium mb-2">Password</label> <input type="password" id="signup-password" name="password" required minlength="8" placeholder="At least 8 characters" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Confirm Password --> <div class="mb-6"> <label for="confirm-password" class="block text-[#c9d1d9] text-sm font-medium mb-2">Confirm Password</label> <input type="password" id="confirm-password" name="confirm_password" required minlength="8" placeholder="Re-enter your password" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Submit --> <button type="submit" class="w-full bg-[#1f6feb] hover:bg-[#388bfd] text-white px-4 py-2 rounded-md text-sm font-semibold transition-colors focus:outline-none focus:ring-0"> }"> <!-- Username --> <div class="mb-4"> <label for="username" class="block text-[#c9d1d9] text-sm font-medium mb-2">Username</label> <input type="text" id="username" name="username" required placeholder="Enter your username" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Email --> <div class="mb-4"> <label for="email" class="block text-[#c9d1d9] text-sm font-medium mb-2">Email</label> <input type="email" id="email" name="email" required placeholder="you@example.com" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Password --> <div class="mb-4"> <label for="signup-password" class="block text-[#c9d1d9] text-sm font-medium mb-2">Password</label> <input type="password" id="signup-password" name="password" required minlength="8" placeholder="At least 8 characters" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Confirm Password --> <div class="mb-6"> <label for="confirm-password" class="block text-[#c9d1d9] text-sm font-medium mb-2">Confirm Password</label> <input type="password" id="confirm-password" name="confirm_password" required minlength="8" placeholder="Re-enter your password" class="w-full bg-[#0d1117] border border-[#30363d] rounded-md px-3 py-2 text-[#c9d1d9] placeholder-[#484f58] focus:outline-none focus:border-[#58a6ff] focus:ring-1 focus:ring-[#58a6ff] transition-colors"> </div> <!-- Submit --> <button type="submit" class="w-full bg-[#1f6feb] hover:bg-[#388bfd] text-white px-4 py-2 rounded-md text-sm font-semibold transition-colors focus:outline-none focus:ring-0">

View file

@ -1,5 +1,5 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">Terms of Service</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <p> <!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CjyFoliM.css"></head> <body data-astro-cid-sckkx6r4> <div class="max-w-4xl mx-auto px-4 py-16"> <h1 class="text-4xl font-bold text-[#c9d1d9] mb-8">Terms of Service</h1> <div class="space-y-6 text-[#8b949e] leading-relaxed"> <p>
Last updated: <strong class="text-[#c9d1d9]">May 2, 2026</strong> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Acceptance of Terms</h2> <p> Last updated: <strong class="text-[#c9d1d9]">May 3, 2026</strong> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Acceptance of Terms</h2> <p>
By accessing and using PR Dojo, you accept and agree to be bound by these Terms of Service. If you do not agree to these terms, do not use this service. By accessing and using PR Dojo, you accept and agree to be bound by these Terms of Service. If you do not agree to these terms, do not use this service.
</p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Description of Service</h2> <p> </p> <h2 class="text-2xl font-semibold text-[#c9d1d9] mt-10 mb-4">Description of Service</h2> <p>
PR Dojo provides an educational platform for practicing code review skills through interactive challenges. The service includes: PR Dojo provides an educational platform for practicing code review skills through interactive challenges. The service includes:

View file

@ -12,6 +12,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/prism": "^4.0.1",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"astro": "^6.1.7", "astro": "^6.1.7",
"tailwindcss": "^4.2.2" "tailwindcss": "^4.2.2"

View file

@ -1,129 +1,546 @@
--- ---
import { runHighlighterWithAstro } from '../../node_modules/@astrojs/prism/dist/highlighter.js';
interface Props { interface Props {
code: string; code: string;
hints: string[]; hints: string[];
file?: string;
} }
const { code, hints } = Astro.props; const { code, hints, file } = Astro.props;
const id = `dv-${Math.random().toString(36).substring(2, 9)}`; const id = `dv-${Math.random().toString(36).substring(2, 9)}`;
const lines = code.split('\n'); const originalLines = code.split('\n');
const fileExtension = (file?.split('/').pop() || 'file').split('.').pop() || '';
const langMap: Record<string, string> = {
js: 'javascript', jsx: 'jsx', ts: 'typescript', tsx: 'tsx',
html: 'html', css: 'css', json: 'json', py: 'python',
rb: 'ruby', java: 'java', c: 'c', cpp: 'cpp', cs: 'csharp',
go: 'go', rs: 'rust', php: 'php', swift: 'swift', kt: 'kotlin',
sh: 'bash', yaml: 'yaml', yml: 'yaml', md: 'markdown', sql: 'sql',
xml: 'xml', dockerfile: 'dockerfile', gitignore: 'bash', astro: 'astro',
};
const language = langMap[fileExtension] || undefined;
let highlightedLines: string[] = [];
if (language) {
try {
const { html } = runHighlighterWithAstro(language, code);
const parsed = html.split('\n');
if (parsed.length > originalLines.length && parsed[parsed.length - 1] === '') {
parsed.pop();
}
highlightedLines = parsed.length === originalLines.length ? parsed : [];
} catch {
// Fallback: no highlighting
}
}
const highlightedMap = new Map<string, string>();
highlightedLines.forEach((hl, i) => {
highlightedMap.set(String(i + 1), hl);
});
const lineContentMap = new Map<string, string>();
originalLines.forEach((line, i) => {
const lineNum = String(i + 1);
lineContentMap.set(lineNum, highlightedMap.get(lineNum) || line);
});
--- ---
<div id={id} class="diff-viewer-container" data-dv-id={id}> <div id={id} class="diff-viewer-container" data-dv-id={id} data-original-code={code} data-file-extension={fileExtension}>
<div class="bg-[#0d1117] border border-[#30363d] rounded-md overflow-hidden"> <div class="bg-[#0d1117] border border-[#30363d] rounded-md overflow-hidden">
<div class="bg-[#161b22] px-4 py-3 border-b border-[#30363d]"> <!-- Toolbar -->
<span class="text-sm text-[#8b949e]">{Astro.url.pathname.split('/').pop() || 'file.js'}</span> <div class="bg-[#161b22] px-4 py-3 border-b border-[#30363d] flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-sm text-[#8b949e]">{file?.split('/').pop() || 'file'}.{fileExtension}</span>
<span class="text-xs text-[#484f58]">|</span>
<span class="text-xs text-[#8b949e]">
<span id="left-lines-count">{originalLines.length}</span> lines
</span>
</div>
<div class="flex items-center gap-2">
<button id="reveal-hints-btn" class="text-xs px-2 py-1 rounded bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] hover:bg-[#30363d] transition-colors focus:outline-none focus:ring-0">
Reveal Hints
</button>
<button id="reset-btn" class="text-xs px-2 py-1 rounded bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] hover:bg-[#30363d] transition-colors focus:outline-none focus:ring-0">
Reset
</button>
</div>
</div> </div>
<div class="overflow-x-auto"> <!-- Side-by-side viewer -->
<table class="w-full font-mono text-sm"> <div class="flex flex-col md:flex-row min-h-[400px]">
<tbody> <!-- Left panel: Original code -->
{lines.map((line, index) => { <div class="flex-1 flex flex-col border-b md:border-b-0 md:border-r border-[#30363d]">
<div class="bg-[#161b22] px-4 py-2 border-b border-[#30363d] flex items-center justify-between">
<span class="text-xs font-semibold text-[#f85149] uppercase tracking-wider">Original (Buggy)</span>
<span class="text-xs text-[#484f58]">Read-only</span>
</div>
<div class="flex-1 overflow-auto relative">
<div class="flex min-h-full">
<!-- Line numbers -->
<div class="left-gutter select-none bg-[#0d1117] border-r border-[#30363d] py-2 sticky left-0 z-10">
{originalLines.map((_, index) => {
const lineNum = index + 1;
const hasHint = hints.some(h => h.includes(`Line ${lineNum}`));
return (
<div
data-line={lineNum}
data-side="left"
class={`line-gutter-row font-mono text-right pr-3 pl-3 text-[#484f58] text-sm cursor-pointer hover:text-[#8b949e] transition-colors leading-[1.6] ${hasHint ? 'has-hint' : ''}`}
title={`Mark line ${lineNum} as buggy`}
>
{lineNum}
</div>
);
})}
</div>
<!-- Code content -->
<div class="left-code-area flex-1 py-2 overflow-hidden">
{originalLines.map((line, index) => {
const lineNum = index + 1; const lineNum = index + 1;
const hasHint = hints.some(h => h.includes(`Line ${lineNum}`)); const hasHint = hints.some(h => h.includes(`Line ${lineNum}`));
const hintText = hints.find(h => h.includes(`Line ${lineNum}`)); const hintText = hints.find(h => h.includes(`Line ${lineNum}`));
const content = highlightedMap.get(String(lineNum)) || line || ' ';
return ( return (
<tr <div
data-line={lineNum} data-line={lineNum}
class="group hover:bg-[#161b22] cursor-pointer transition-colors" data-side="left"
title={`Mark line ${lineNum} as buggy`} class={`code-row flex items-start gap-0 px-4 text-sm leading-[1.6] transition-colors ${hasHint ? 'has-hint' : ''}`}
> >
<td class="w-16 text-right pr-4 select-none"> <span class={`text-[#484f58] w-0 select-none ${hasHint ? 'has-hint-num' : ''}`}></span>
<span class={`text-[#8b949e] line-number ${hasHint ? 'has-hint-color' : ''}`}> <span class={`code-text font-mono text-[#e6edf3] whitespace-pre ${hasHint ? 'has-hint-bg' : ''}`} set:html={content}></span>
{lineNum}
</span>
</td>
<td class="px-4 py-0.5 whitespace-pre">
<span class={`text-[#e6edf3] ${hasHint ? 'has-hint-bg bg-[#ff7b7233] px-2 -mx-2' : ''}`}>
{line || ' '}
</span>
{hasHint && ( {hasHint && (
<span class="ml-2 hint text-[#79c0ff] text-xs hidden"> <span class="hint-text font-mono text-[#79c0ff] text-xs ml-2 hidden">
• {hintText?.replace(`Line ${lineNum}: `, '')} {hintText?.replace(`Line ${lineNum}: `, '')}
</span> </span>
)} )}
</td> </div>
</tr>
); );
})} })}
</tbody> </div>
</table> </div>
</div>
</div> </div>
<!-- Right panel: Editable code -->
<div class="flex-1 flex flex-col">
<div class="bg-[#161b22] px-4 py-2 border-b border-[#30363d] flex items-center justify-between">
<span class="text-xs font-semibold text-[#3fb950] uppercase tracking-wider">Your Fix</span>
<span class="text-xs text-[#484f58]">Editable</span>
</div>
<div class="flex-1 overflow-auto relative">
<div class="flex min-h-full">
<!-- Line numbers -->
<div class="right-gutter select-none bg-[#0d1117] border-r border-[#30363d] py-2 sticky left-0 z-10">
{originalLines.map((_, index) => (
<div
data-line={index + 1}
data-side="right"
class="right-gutter-row font-mono text-right pr-3 pl-3 text-[#484f58] text-sm leading-[1.6] bg-transparent"
>
{index + 1}
</div>
))}
</div>
<!-- Editable content with diff overlay -->
<div class="right-editor-area flex-1 relative py-0">
<!-- Diff highlight overlay -->
<div id="diff-highlight" class="absolute inset-0 py-2 pointer-events-none">
{originalLines.map((_, index) => {
const lineNum = index + 1;
return (
<div
data-diff-line={lineNum}
class="diff-row h-[1.6em] px-4 transition-colors"
></div>
);
})}
</div>
<!-- Textarea for editing -->
<textarea
id="edit-textarea"
class="editor-textarea absolute inset-0 w-full h-full bg-transparent text-[#e6edf3] font-mono text-sm leading-[1.6] resize-none focus:outline-none pl-4 pr-4 py-2"
spellcheck="false"
>{code}</textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Status bar -->
<div class="bg-[#161b22] px-4 py-2 border-t border-[#30363d] flex items-center justify-between text-xs">
<div class="flex items-center gap-4">
<span class="text-[#484f58]">
<span id="diff-added" class="text-[#3fb950]">0</span> added
</span>
<span class="text-[#484f58]">
<span id="diff-removed" class="text-[#f85149]">0</span> removed
</span>
<span class="text-[#484f58]">
<span id="diff-changed" class="text-[#d29922]">0</span> changed
</span>
</div>
<div class="text-[#484f58]">
Click lines on the left to mark as buggy
</div>
</div>
</div> </div>
</div> </div>
<style> <style>
tr { .left-gutter-row,
.right-gutter-row {
height: 1.6em;
}
.code-row {
height: 1.6em;
min-height: 1.6em;
}
.editor-textarea {
line-height: 1.6; line-height: 1.6;
tab-size: 2;
-moz-tab-size: 2;
} }
tr:hover {
background-color: #161b22; .diff-row {
height: 1.6em;
min-height: 1.6em;
} }
td:first-child {
border-right: 1px solid #30363d; .diff-row.added {
background-color: #3fb95018;
} }
tr.bugged td:first-child {
border-left: 3px solid #f85149; .diff-row.removed {
}
tr.bugged .line-number {
color: #f85149;
}
tr.bugged {
background-color: #f8514918; background-color: #f8514918;
} }
.diff-viewer-container:not(.revealed) tr.bugged .line-number.has-hint-color {
color: #f85149 !important; .code-row.diff-removed {
background-color: #f8514918;
} }
.diff-viewer-container:not(.revealed) tr:not(.bugged) span.line-number.has-hint-color {
color: #8b949e !important; .code-row.diff-added {
font-weight: normal !important; background-color: #3fb95018;
} }
.diff-viewer-container.revealed tr.bugged .hint {
.diff-viewer-container:not(.revealed) .has-hint .code-text {
background-color: transparent !important;
}
.diff-viewer-container.revealed .has-hint .hint-text {
display: inline; display: inline;
} }
.diff-viewer-container:not(.revealed) td > span.has-hint-bg {
background-color: transparent !important; .has-hint .code-text {
background-color: #f8514918;
}
.has-hint.has-hint-bg .code-text {
background-color: #f8514918;
}
.has-hint .has-hint-num {
color: #f85149 !important;
}
.has-hint:not(.revealed) .has-hint-num {
color: #484f58 !important;
}
.diff-viewer-container.revealed .has-hint .has-hint-num {
color: #f85149 !important;
}
/* Scrollbar styling */
.overflow-auto::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.overflow-auto::-webkit-scrollbar-track {
background: #0d1117;
}
.overflow-auto::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 4px;
}
.overflow-auto::-webkit-scrollbar-thumb:hover {
background: #484f58;
} }
</style> </style>
<script client:load> <script client:load>
(() => { (() => {
const el = document.querySelector('[data-dv-id]'); const container = document.querySelector('[data-dv-id]');
if (!el) return; if (!container) return;
const originalCode = container.getAttribute('data-original-code') || '';
const originalLines = originalCode.split('\n');
const leftGutter = container.querySelector('.left-gutter');
const leftCodeArea = container.querySelector('.left-code-area');
const rightGutter = container.querySelector('.right-gutter');
const rightEditorArea = container.querySelector('.right-editor-area');
const textarea = document.getElementById('edit-textarea');
const diffHighlight = document.getElementById('diff-highlight');
const revealBtn = document.getElementById('reveal-hints-btn');
const resetBtn = document.getElementById('reset-btn');
const addedEl = document.getElementById('diff-added');
const removedEl = document.getElementById('diff-removed');
const changedEl = document.getElementById('diff-changed');
const buggedLines = new Set(); const buggedLines = new Set();
function updateBuggedState() { // Synchronized scrolling
const rows = el.querySelectorAll('tr[data-line]'); function syncScroll(source, targetGutter, targetCode) {
rows.forEach(row => { if (targetCode) {
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10); targetCode.scrollTop = source.scrollTop;
row.classList.toggle('bugged', buggedLines.has(lineNum)); targetCode.scrollLeft = source.scrollLeft;
}
if (targetGutter) {
targetGutter.scrollTop = source.scrollTop;
}
}
// Left side scroll sync (also scrolls right side)
if (leftCodeArea) {
leftCodeArea.addEventListener('scroll', () => {
syncScroll(leftCodeArea, rightGutter, rightEditorArea);
}); });
} }
el.addEventListener('click', function(e) { // Right side scroll sync (also scrolls left side)
const target = e.target; if (rightEditorArea) {
if (!(target instanceof HTMLElement)) return; rightEditorArea.addEventListener('scroll', () => {
const row = target.closest('tr[data-line]'); syncScroll(rightEditorArea, leftGutter, leftCodeArea);
if (!row) return; });
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
if (buggedLines.has(lineNum)) {
buggedLines.delete(lineNum);
} else {
buggedLines.add(lineNum);
} }
updateBuggedState(); // Diff computation using LCS-based line diff
function computeDiff(oldText, newText) {
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
el.dispatchEvent(new CustomEvent('bugged-line-toggle', { // LCS to compute diff
const m = oldLines.length;
const n = newLines.length;
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (oldLines[i - 1] === newLines[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// Backtrack to get diff ops
const ops = [];
let i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
ops.push({ type: 'equal', oldLine: i, newLine: j });
i--; j--;
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
ops.push({ type: 'added', newLine: j });
j--;
} else {
ops.push({ type: 'removed', oldLine: i });
i--;
}
}
ops.reverse();
return ops;
}
function updateDiffHighlight() {
if (!textarea || !diffHighlight) return;
const newText = textarea.value;
const allOld = originalLines;
const allNew = newText.split('\n');
const totalLines = Math.max(allOld.length, allNew.length);
let added = 0, removed = 0, changed = 0;
// Rebuild diff rows to match textarea line count
diffHighlight.innerHTML = '';
for (let i = 0; i < totalLines; i++) {
const row = document.createElement('div');
row.setAttribute('data-diff-line', String(i + 1));
row.className = 'diff-row h-[1.6em] px-4 transition-colors';
diffHighlight.appendChild(row);
}
// Compare line by line using LCS-based approach
let oi = 0, ni = 0;
const statusMap = new Map(); // old line index -> 'equal' | 'removed' | 'changed'
while (oi < allOld.length || ni < allNew.length) {
if (oi < allOld.length && ni < allNew.length && allOld[oi] === allNew[ni]) {
statusMap.set(oi, 'equal');
oi++;
ni++;
} else if (oi < allOld.length && ni < allNew.length) {
statusMap.set(oi, 'changed');
added++;
changed++;
oi++;
ni++;
} else if (oi < allOld.length) {
statusMap.set(oi, 'removed');
removed++;
oi++;
} else {
added++;
ni++;
}
}
// Apply to right side diff overlay
statusMap.forEach((status, idx) => {
const lineNum = idx + 1;
const row = diffHighlight.querySelector(`[data-diff-line="${lineNum}"]`);
if (row) {
if (status === 'removed') {
row.classList.add('removed');
} else if (status === 'changed') {
row.classList.add('added');
}
}
});
// Apply to left panel code rows (only removed = red, no yellow)
statusMap.forEach((status, idx) => {
const lineNum = idx + 1;
const row = leftCodeArea?.querySelector(`.code-row[data-line="${lineNum}"]`);
if (row) {
row.classList.remove('diff-removed', 'diff-added');
if (status === 'removed') {
row.classList.add('diff-removed');
}
}
});
// Highlight added lines beyond original length
for (let i = allOld.length; i < allNew.length; i++) {
const row = diffHighlight.querySelector(`[data-diff-line="${i + 1}"]`);
if (row) {
row.classList.add('added');
}
}
addedEl.textContent = added;
removedEl.textContent = removed;
changedEl.textContent = changed;
}
// Left panel: click to mark bugged lines (gutter + code area)
function toggleBuggedLine(lineNum) {
if (isNaN(lineNum) || lineNum === 0) return;
const gutterRow = leftGutter?.querySelector(`[data-line="${lineNum}"][data-side="left"]`);
const codeRow = leftCodeArea?.querySelector(`.code-row[data-line="${lineNum}"]`);
const isBugged = buggedLines.has(lineNum);
if (isBugged) {
buggedLines.delete(lineNum);
if (gutterRow) { gutterRow.style.backgroundColor = ''; gutterRow.style.borderLeft = ''; }
if (codeRow) { codeRow.style.backgroundColor = ''; }
} else {
buggedLines.add(lineNum);
if (gutterRow) { gutterRow.style.backgroundColor = '#f8514920'; gutterRow.style.borderLeft = '3px solid #f85149'; }
if (codeRow) { codeRow.style.backgroundColor = '#f8514920'; }
}
container.dispatchEvent(new CustomEvent('bugged-line-toggle', {
bubbles: true, bubbles: true,
detail: { line: lineNum, bugged: buggedLines.has(lineNum), allBugged: [...buggedLines] } detail: { line: lineNum, bugged: buggedLines.has(lineNum), allBugged: [...buggedLines] }
})); }));
}
leftGutter?.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest('[data-line][data-side="left"]');
if (!row) return;
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
toggleBuggedLine(lineNum);
}); });
leftCodeArea?.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest('.code-row');
if (!row) return;
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
toggleBuggedLine(lineNum);
});
// Reveal hints
revealBtn?.addEventListener('click', () => {
const isRevealed = container.classList.toggle('revealed');
revealBtn.textContent = isRevealed ? 'Hide Hints' : 'Reveal Hints';
revealBtn.classList.toggle('text-[#79c0ff]', isRevealed);
});
// Reset
resetBtn?.addEventListener('click', () => {
textarea.value = originalLines.join('\n');
buggedLines.clear();
container.classList.remove('revealed');
revealBtn.textContent = 'Reveal Hints';
revealBtn.classList.remove('text-[#79c0ff]');
const gutterRows = leftGutter?.querySelectorAll('[data-side="left"]');
gutterRows?.forEach(row => {
row.style.backgroundColor = '';
row.style.borderLeft = '';
});
const codeRows = leftCodeArea?.querySelectorAll('.code-row');
codeRows?.forEach(row => {
row.style.backgroundColor = '';
row.classList.remove('diff-removed', 'diff-added');
});
updateDiffHighlight();
container.dispatchEvent(new CustomEvent('bugged-line-reset', {
bubbles: true,
detail: { allBugged: [] }
}));
});
// Textarea input -> update diff
textarea?.addEventListener('input', () => {
updateDiffHighlight();
});
// Tab key support in textarea
textarea?.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + 2;
updateDiffHighlight();
}
});
// Initial diff
updateDiffHighlight();
})(); })();
</script> </script>

View file

@ -76,46 +76,35 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
</div> </div>
<!-- Code Viewer --> <!-- Code Viewer -->
<DiffViewer code={challenge.code} hints={challenge.hints} /> <DiffViewer code={challenge.code} hints={challenge.hints} file={challenge.file} />
<!-- Fix Submission --> <!-- Fix Submission -->
<div id="fix-panel" class="mt-6 bg-[#161b22] border border-[#30363d] rounded-md overflow-hidden"> <div id="fix-panel" class="mt-6 bg-[#161b22] border border-[#30363d] rounded-md overflow-hidden">
<div class="bg-[#161b22] px-4 py-3 border-b border-[#30363d] flex items-center justify-between"> <div class="bg-[#161b22] px-4 py-3 border-b border-[#30363d] flex items-center justify-between">
<h2 class="text-lg font-semibold text-[#c9d1d9]">Submit Your Fix</h2> <h2 class="text-lg font-semibold text-[#c9d1d9]">Submit Your Fix</h2>
<div id="bugged-count" class="text-sm text-[#8b949e]"> <div class="text-sm text-[#8b949e]">
Lines marked: <span id="bugged-count-num" class="text-[#f85149] font-semibold">0</span> Lines marked: <span id="bugged-count-num" class="text-[#f85149] font-semibold">0</span>
</div> </div>
</div> </div>
<!-- Marked lines summary -->
<div id="marked-lines-summary" class="px-4 py-3 border-b border-[#30363d] hidden">
<p class="text-sm text-[#8b949e] mb-2">Marked lines:</p>
<div id="marked-lines-tags" class="flex flex-wrap gap-2"></div>
</div>
<!-- Inline fix editor -->
<div class="p-4"> <div class="p-4">
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-[#c9d1d9] mb-2"> <p class="text-sm text-[#8b949e] mb-1">
Line-by-line fix: Edit the code on the <span class="text-[#3fb950] font-medium">right panel</span> above to fix the bugs.
</label> Changes are highlighted in real-time.
<p class="text-xs text-[#8b949e] mb-3">Click a marked line number below to edit its fix.</p> </p>
<div id="fix-editor-container" class="space-y-2"> <p class="text-xs text-[#484f58]">
<p id="fix-editor-placeholder" class="text-sm text-[#484f58] italic">Click on lines in the diff above to mark them, then add your fixes here.</p> <span class="text-[#f85149]">■</span> Removed &nbsp;
</div> <span class="text-[#d29922]">■</span> Changed &nbsp;
<span class="text-[#3fb950]">■</span> Added &nbsp;
&nbsp;Click lines on the left to mark buggy lines
</p>
</div> </div>
<!-- Unified diff textarea --> <!-- Bugged lines summary -->
<div class="mb-4"> <div id="marked-lines-summary" class="mb-4 hidden">
<label class="block text-sm font-medium text-[#c9d1d9] mb-2"> <p class="text-sm font-medium text-[#c9d1d9] mb-2">Marked buggy lines:</p>
Or provide as unified diff: <div id="marked-lines-tags" class="flex flex-wrap gap-2"></div>
</label>
<textarea
id="diff-textarea"
placeholder="- old line&#10;+ new line"
rows={6}
class="w-full bg-[#0d1117] border border-[#30363d] text-[#c9d1d9] rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#58a6ff] focus:border-transparent text-sm font-mono"
></textarea>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
@ -137,15 +126,30 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
<script client:load> <script client:load>
(() => { (() => {
const buggedLines = new Set(); const buggedLines = new Set();
const fixEditorContainer = document.getElementById('fix-editor-container');
const markedLinesTags = document.getElementById('marked-lines-tags'); const markedLinesTags = document.getElementById('marked-lines-tags');
const markedLinesSummary = document.getElementById('marked-lines-summary'); const markedLinesSummary = document.getElementById('marked-lines-summary');
const buggedCountNum = document.getElementById('bugged-count-num'); const buggedCountNum = document.getElementById('bugged-count-num');
const submitBtn = document.getElementById('submit-fix-btn'); const submitBtn = document.getElementById('submit-fix-btn');
const clearBtn = document.getElementById('clear-all-btn'); const clearBtn = document.getElementById('clear-all-btn');
const diffTextarea = document.getElementById('diff-textarea');
const submissionMessage = document.getElementById('submission-message'); const submissionMessage = document.getElementById('submission-message');
const fixEditorPlaceholder = document.getElementById('fix-editor-placeholder');
function toRanges(sorted) {
const ranges = [];
if (sorted.length === 0) return ranges;
let start = sorted[0];
let end = sorted[0];
for (let i = 1; i < sorted.length; i++) {
if (sorted[i] === end + 1) {
end = sorted[i];
} else {
ranges.push([start, end]);
start = sorted[i];
end = sorted[i];
}
}
ranges.push([start, end]);
return ranges;
}
function updateUI() { function updateUI() {
const arr = [...buggedLines].sort((a, b) => a - b); const arr = [...buggedLines].sort((a, b) => a - b);
@ -153,13 +157,11 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
if (markedLinesTags) { if (markedLinesTags) {
markedLinesTags.innerHTML = ''; markedLinesTags.innerHTML = '';
arr.forEach(lineNum => { const ranges = toRanges(arr);
ranges.forEach(([start, end]) => {
const tag = document.createElement('span'); const tag = document.createElement('span');
tag.className = 'inline-flex items-center gap-1 bg-[#f8514920] text-[#f85149] text-xs px-2 py-0.5 rounded cursor-pointer hover:bg-[#f8514930] transition-colors'; tag.className = 'inline-flex items-center gap-1 bg-[#f8514920] text-[#f85149] text-xs px-2 py-0.5 rounded cursor-pointer hover:bg-[#f8514930] transition-colors';
tag.textContent = `Line ${lineNum}`; tag.textContent = start === end ? `Line ${start}` : `Lines ${start}-${end}`;
tag.addEventListener('click', () => {
showLineFixEditor(lineNum);
});
markedLinesTags.appendChild(tag); markedLinesTags.appendChild(tag);
}); });
} }
@ -168,58 +170,10 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
markedLinesSummary.classList.toggle('hidden', arr.length === 0); markedLinesSummary.classList.toggle('hidden', arr.length === 0);
} }
if (fixEditorPlaceholder) { // Check if the editor has any changes
fixEditorPlaceholder.classList.toggle('hidden', arr.length > 0); const textarea = document.getElementById('edit-textarea');
} const editorChanged = textarea ? textarea.value !== textarea.defaultValue : false;
submitBtn.disabled = arr.length === 0 && !editorChanged;
if (submitBtn) {
submitBtn.disabled = arr.length === 0 && !diffTextarea?.value.trim();
}
}
function showLineFixEditor(lineNum) {
if (!fixEditorContainer) return;
const existing = document.getElementById(`fix-editor-${lineNum}`);
if (existing) {
existing.scrollIntoView({ behavior: 'smooth', block: 'center' });
existing.querySelector('textarea')?.focus();
return;
}
const editorDiv = document.createElement('div');
editorDiv.id = `fix-editor-${lineNum}`;
editorDiv.className = 'bg-[#0d1117] border border-[#30363d] rounded-md p-3';
const header = document.createElement('div');
header.className = 'flex items-center justify-between mb-2';
const label = document.createElement('span');
label.className = 'text-sm font-medium text-[#f85149]';
label.textContent = `Line ${lineNum} — Your fix:`;
const removeBtn = document.createElement('button');
removeBtn.className = 'text-xs text-[#484f58] hover:text-[#f85149] transition-colors';
removeBtn.textContent = '✕';
removeBtn.addEventListener('click', () => {
buggedLines.delete(lineNum);
editorDiv.remove();
updateUI();
});
header.appendChild(label);
header.appendChild(removeBtn);
const textarea = document.createElement('textarea');
textarea.rows = 3;
textarea.className = 'w-full bg-[#0d1117] border border-[#30363d] text-[#c9d1d9] rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-[#58a6ff] focus:border-transparent text-sm font-mono';
textarea.placeholder = 'Enter the corrected line...';
textarea.addEventListener('input', updateUI);
editorDiv.appendChild(header);
editorDiv.appendChild(textarea);
fixEditorContainer.appendChild(editorDiv);
} }
// Listen for bugged-line-toggle events from DiffViewer // Listen for bugged-line-toggle events from DiffViewer
@ -227,57 +181,57 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
const detail = e.detail; const detail = e.detail;
if (detail.bugged) { if (detail.bugged) {
buggedLines.add(detail.line); buggedLines.add(detail.line);
showLineFixEditor(detail.line);
} else { } else {
buggedLines.delete(detail.line); buggedLines.delete(detail.line);
const editor = document.getElementById(`fix-editor-${detail.line}`);
if (editor) editor.remove();
} }
updateUI(); updateUI();
}); });
// Diff textarea input // Listen for bugged-line-reset events from DiffViewer
if (diffTextarea) { document.addEventListener('bugged-line-reset', (e) => {
diffTextarea.addEventListener('input', updateUI); buggedLines.clear();
} updateUI();
});
// Listen for textarea changes
document.getElementById('edit-textarea')?.addEventListener('input', () => {
updateUI();
});
// Submit // Submit
if (submitBtn) { if (submitBtn) {
submitBtn.addEventListener('click', () => { submitBtn.addEventListener('click', () => {
const lineFixes = {}; const textarea = document.getElementById('edit-textarea');
buggedLines.forEach(lineNum => { const fixedCode = textarea?.value.trim() || '';
const editor = document.getElementById(`fix-editor-${lineNum}`); const originalCode = textarea?.defaultValue || '';
if (editor) {
const ta = editor.querySelector('textarea');
if (ta?.value.trim()) {
lineFixes[lineNum] = ta.value.trim();
}
}
});
const unifiedDiff = diffTextarea?.value.trim() || ''; if (fixedCode === originalCode) {
if (Object.keys(lineFixes).length === 0 && !unifiedDiff) {
if (submissionMessage) { if (submissionMessage) {
submissionMessage.textContent = 'Please mark some lines or provide a diff.'; submissionMessage.textContent = 'Please make some changes to the code before submitting.';
submissionMessage.className = 'mt-3 text-sm text-[#f85149]'; submissionMessage.className = 'mt-3 text-sm text-[#f85149]';
submissionMessage.classList.remove('hidden'); submissionMessage.classList.remove('hidden');
} }
return; return;
} }
// Reveal bugged lines and hints in the diff viewer // Collect marked lines
const dvContainer = document.querySelector('.diff-viewer-container'); const markedArr = [...buggedLines].sort((a, b) => a - b);
if (dvContainer) {
dvContainer.classList.add('revealed');
}
// Show success message // Show success message
if (submissionMessage) { if (submissionMessage) {
submissionMessage.textContent = 'Fix submitted! (Backend integration coming soon)'; const lineInfo = markedArr.length > 0
? ` Marked ${markedArr.length} buggy line(s).`
: ' No lines were specifically marked as buggy.';
submissionMessage.textContent = `Fix submitted!${lineInfo} (Backend integration coming soon)`;
submissionMessage.className = 'mt-3 text-sm text-[#3ac840]'; submissionMessage.className = 'mt-3 text-sm text-[#3ac840]';
submissionMessage.classList.remove('hidden'); submissionMessage.classList.remove('hidden');
} }
// Log the fix for debugging
console.log('=== Challenge Fix Submitted ===');
console.log('Marked buggy lines:', markedArr);
console.log('Fixed code:\n', fixedCode);
console.log('===============================');
}); });
} }
@ -285,20 +239,12 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
if (clearBtn) { if (clearBtn) {
clearBtn.addEventListener('click', () => { clearBtn.addEventListener('click', () => {
buggedLines.clear(); buggedLines.clear();
const editors = fixEditorContainer?.querySelectorAll('[id^="fix-editor-"]');
editors?.forEach(e => e.remove());
if (diffTextarea) diffTextarea.value = '';
if (submissionMessage) submissionMessage.classList.add('hidden'); if (submissionMessage) submissionMessage.classList.add('hidden');
updateUI(); updateUI();
// Reset bugged state and revealed state in DiffViewer // Trigger reset on DiffViewer
const dvContainer = document.querySelector('.diff-viewer-container'); const resetBtn = document.getElementById('reset-btn');
if (dvContainer) { if (resetBtn) resetBtn.click();
dvContainer.classList.remove('revealed');
}
document.querySelectorAll('tr.bugged').forEach(row => {
row.classList.remove('bugged');
});
}); });
} }

View file

@ -52,7 +52,7 @@ import Layout from '../layouts/Layout.astro';
<div class="relative w-36 h-36"> <div class="relative w-36 h-36">
<svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120">
<circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"/> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#a5d6ff" stroke-width="10" <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10"
stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round"
id="solved-ring" class="transition-all duration-700 ease-out"/> id="solved-ring" class="transition-all duration-700 ease-out"/>
</svg> </svg>
@ -69,7 +69,7 @@ import Layout from '../layouts/Layout.astro';
<div class="relative w-36 h-36"> <div class="relative w-36 h-36">
<svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120">
<circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"/> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#79c0ff" stroke-width="10" <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10"
stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round"
id="streak-ring" class="transition-all duration-700 ease-out"/> id="streak-ring" class="transition-all duration-700 ease-out"/>
</svg> </svg>

View file

@ -37,3 +37,37 @@ a {
a:hover { a:hover {
text-decoration: none; text-decoration: none;
} }
/* Prism syntax highlighting - GitHub Dark theme */
.code-text .token.comment,
.code-text .token.prolog,
.code-text .token.doctype,
.code-text .token.cdata { color: #8b949e; }
.code-text .token.punctuation { color: #e6edf3; }
.code-text .token.property,
.code-text .token.tag,
.code-text .token.boolean,
.code-text .token.number,
.code-text .token.constant,
.code-text .token.symbol,
.code-text .token.deleted { color: #79c0ff; }
.code-text .token.selector,
.code-text .token.attr-name,
.code-text .token.string,
.code-text .token.char,
.code-text .token.builtin,
.code-text .token.inserted { color: #a5d6ff; }
.code-text .token.operator,
.code-text .token.entity,
.code-text .token.url,
.language-css .token.string,
.style .token.string,
.code-text .token.variable { color: #ffa657; }
.code-text .token.atrule,
.code-text .token.attr-value,
.code-text .token.keyword { color: #ff7b72; }
.code-text .token.function,
.code-text .token.class-name { color: #d2a8ff; }
.code-text .token.regex,
.code-text .token.important,
.code-text .token.entity { color: #ff7b72; }