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/
index.astro # challenge listing + hero
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/
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
layouts/Layout.astro # root layout wrapping header/nav/footer
styles/global.css # global styles

View file

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

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.
</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.

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.
</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

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>
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>
<!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 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.
</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:

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 →
</a> </div> </div> </div> </main> </div> <script client:load>
(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();
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.');">

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();
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">

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>
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>
<!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 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.
</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:

View file

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

View file

@ -1,129 +1,546 @@
---
import { runHighlighterWithAstro } from '../../node_modules/@astrojs/prism/dist/highlighter.js';
interface Props {
code: 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 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-[#161b22] px-4 py-3 border-b border-[#30363d]">
<span class="text-sm text-[#8b949e]">{Astro.url.pathname.split('/').pop() || 'file.js'}</span>
<!-- Toolbar -->
<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 class="overflow-x-auto">
<table class="w-full font-mono text-sm">
<tbody>
{lines.map((line, index) => {
const lineNum = index + 1;
const hasHint = hints.some(h => h.includes(`Line ${lineNum}`));
const hintText = hints.find(h => h.includes(`Line ${lineNum}`));
return (
<tr
data-line={lineNum}
class="group hover:bg-[#161b22] cursor-pointer transition-colors"
title={`Mark line ${lineNum} as buggy`}
>
<td class="w-16 text-right pr-4 select-none">
<span class={`text-[#8b949e] line-number ${hasHint ? 'has-hint-color' : ''}`}>
<!-- Side-by-side viewer -->
<div class="flex flex-col md:flex-row min-h-[400px]">
<!-- Left panel: Original code -->
<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}
</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 && (
<span class="ml-2 hint text-[#79c0ff] text-xs hidden">
• {hintText?.replace(`Line ${lineNum}: `, '')}
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
})}
</div>
<!-- Code content -->
<div class="left-code-area flex-1 py-2 overflow-hidden">
{originalLines.map((line, index) => {
const lineNum = index + 1;
const hasHint = hints.some(h => h.includes(`Line ${lineNum}`));
const hintText = hints.find(h => h.includes(`Line ${lineNum}`));
const content = highlightedMap.get(String(lineNum)) || line || ' ';
return (
<div
data-line={lineNum}
data-side="left"
class={`code-row flex items-start gap-0 px-4 text-sm leading-[1.6] transition-colors ${hasHint ? 'has-hint' : ''}`}
>
<span class={`text-[#484f58] w-0 select-none ${hasHint ? 'has-hint-num' : ''}`}></span>
<span class={`code-text font-mono text-[#e6edf3] whitespace-pre ${hasHint ? 'has-hint-bg' : ''}`} set:html={content}></span>
{hasHint && (
<span class="hint-text font-mono text-[#79c0ff] text-xs ml-2 hidden">
{hintText?.replace(`Line ${lineNum}: `, '')}
</span>
)}
</div>
);
})}
</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>
<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;
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;
}
tr.bugged .line-number {
color: #f85149;
}
tr.bugged {
.diff-row.removed {
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;
font-weight: normal !important;
.code-row.diff-added {
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;
}
.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>
<script client:load>
(() => {
const el = document.querySelector('[data-dv-id]');
if (!el) return;
const container = document.querySelector('[data-dv-id]');
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();
function updateBuggedState() {
const rows = el.querySelectorAll('tr[data-line]');
rows.forEach(row => {
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
row.classList.toggle('bugged', buggedLines.has(lineNum));
// Synchronized scrolling
function syncScroll(source, targetGutter, targetCode) {
if (targetCode) {
targetCode.scrollTop = source.scrollTop;
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) {
const target = e.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest('tr[data-line]');
if (!row) return;
// Right side scroll sync (also scrolls left side)
if (rightEditorArea) {
rightEditorArea.addEventListener('scroll', () => {
syncScroll(rightEditorArea, leftGutter, leftCodeArea);
});
}
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
if (buggedLines.has(lineNum)) {
buggedLines.delete(lineNum);
} else {
buggedLines.add(lineNum);
// Diff computation using LCS-based line diff
function computeDiff(oldText, newText) {
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
// 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]);
}
}
}
updateBuggedState();
// 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;
}
el.dispatchEvent(new CustomEvent('bugged-line-toggle', {
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,
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>

View file

@ -53,10 +53,10 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
<div class="flex justify-between items-start mb-4">
<div>
<h1 class="text-2xl font-semibold text-[#c9d1d9] mb-2 no-underline">{challenge.title}</h1>
<div class="mb-4">
<span class="text-[#79c0ff] text-xl no-underline">{stars}</span>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm">
<div class="mb-4">
<span class="text-[#79c0ff] text-xl no-underline">{stars}</span>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm">
<span class="text-[#8b949e]">📁 <span class="text-[#a5d6ff]">{challenge.repository}</span></span>
<span class="text-[#8b949e]">🔢 <code class="bg-[#21262d] px-2 py-0.5 rounded text-xs">{challenge.baseSha}</code></span>
<span class="px-2 py-0.5 bg-[#0a3064] rounded text-xs text-[#79c0ff]">{challenge.bugType}</span>
@ -76,46 +76,35 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
</div>
<!-- Code Viewer -->
<DiffViewer code={challenge.code} hints={challenge.hints} />
<DiffViewer code={challenge.code} hints={challenge.hints} file={challenge.file} />
<!-- Fix Submission -->
<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">
<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>
</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="mb-4">
<label class="block text-sm font-medium text-[#c9d1d9] mb-2">
Line-by-line fix:
</label>
<p class="text-xs text-[#8b949e] mb-3">Click a marked line number below to edit its fix.</p>
<div id="fix-editor-container" class="space-y-2">
<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>
</div>
<p class="text-sm text-[#8b949e] mb-1">
Edit the code on the <span class="text-[#3fb950] font-medium">right panel</span> above to fix the bugs.
Changes are highlighted in real-time.
</p>
<p class="text-xs text-[#484f58]">
<span class="text-[#f85149]">■</span> Removed &nbsp;
<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>
<!-- Unified diff textarea -->
<div class="mb-4">
<label class="block text-sm font-medium text-[#c9d1d9] mb-2">
Or provide as unified diff:
</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>
<!-- Bugged lines summary -->
<div id="marked-lines-summary" class="mb-4 hidden">
<p class="text-sm font-medium text-[#c9d1d9] mb-2">Marked buggy lines:</p>
<div id="marked-lines-tags" class="flex flex-wrap gap-2"></div>
</div>
<div class="flex gap-3">
@ -137,15 +126,30 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
<script client:load>
(() => {
const buggedLines = new Set();
const fixEditorContainer = document.getElementById('fix-editor-container');
const markedLinesTags = document.getElementById('marked-lines-tags');
const markedLinesSummary = document.getElementById('marked-lines-summary');
const buggedCountNum = document.getElementById('bugged-count-num');
const submitBtn = document.getElementById('submit-fix-btn');
const clearBtn = document.getElementById('clear-all-btn');
const diffTextarea = document.getElementById('diff-textarea');
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() {
const arr = [...buggedLines].sort((a, b) => a - b);
@ -153,13 +157,11 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
if (markedLinesTags) {
markedLinesTags.innerHTML = '';
arr.forEach(lineNum => {
const ranges = toRanges(arr);
ranges.forEach(([start, end]) => {
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.textContent = `Line ${lineNum}`;
tag.addEventListener('click', () => {
showLineFixEditor(lineNum);
});
tag.textContent = start === end ? `Line ${start}` : `Lines ${start}-${end}`;
markedLinesTags.appendChild(tag);
});
}
@ -168,58 +170,10 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
markedLinesSummary.classList.toggle('hidden', arr.length === 0);
}
if (fixEditorPlaceholder) {
fixEditorPlaceholder.classList.toggle('hidden', arr.length > 0);
}
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);
// Check if the editor has any changes
const textarea = document.getElementById('edit-textarea');
const editorChanged = textarea ? textarea.value !== textarea.defaultValue : false;
submitBtn.disabled = arr.length === 0 && !editorChanged;
}
// 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;
if (detail.bugged) {
buggedLines.add(detail.line);
showLineFixEditor(detail.line);
} else {
buggedLines.delete(detail.line);
const editor = document.getElementById(`fix-editor-${detail.line}`);
if (editor) editor.remove();
}
updateUI();
});
// Diff textarea input
if (diffTextarea) {
diffTextarea.addEventListener('input', updateUI);
}
// Listen for bugged-line-reset events from DiffViewer
document.addEventListener('bugged-line-reset', (e) => {
buggedLines.clear();
updateUI();
});
// Listen for textarea changes
document.getElementById('edit-textarea')?.addEventListener('input', () => {
updateUI();
});
// Submit
if (submitBtn) {
submitBtn.addEventListener('click', () => {
const lineFixes = {};
buggedLines.forEach(lineNum => {
const editor = document.getElementById(`fix-editor-${lineNum}`);
if (editor) {
const ta = editor.querySelector('textarea');
if (ta?.value.trim()) {
lineFixes[lineNum] = ta.value.trim();
}
}
});
const textarea = document.getElementById('edit-textarea');
const fixedCode = textarea?.value.trim() || '';
const originalCode = textarea?.defaultValue || '';
const unifiedDiff = diffTextarea?.value.trim() || '';
if (Object.keys(lineFixes).length === 0 && !unifiedDiff) {
if (fixedCode === originalCode) {
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.classList.remove('hidden');
}
return;
}
// Reveal bugged lines and hints in the diff viewer
const dvContainer = document.querySelector('.diff-viewer-container');
if (dvContainer) {
dvContainer.classList.add('revealed');
}
// Collect marked lines
const markedArr = [...buggedLines].sort((a, b) => a - b);
// Show success message
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.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) {
clearBtn.addEventListener('click', () => {
buggedLines.clear();
const editors = fixEditorContainer?.querySelectorAll('[id^="fix-editor-"]');
editors?.forEach(e => e.remove());
if (diffTextarea) diffTextarea.value = '';
if (submissionMessage) submissionMessage.classList.add('hidden');
updateUI();
// Reset bugged state and revealed state in DiffViewer
const dvContainer = document.querySelector('.diff-viewer-container');
if (dvContainer) {
dvContainer.classList.remove('revealed');
}
document.querySelectorAll('tr.bugged').forEach(row => {
row.classList.remove('bugged');
});
// Trigger reset on DiffViewer
const resetBtn = document.getElementById('reset-btn');
if (resetBtn) resetBtn.click();
});
}

View file

@ -52,7 +52,7 @@ import Layout from '../layouts/Layout.astro';
<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 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"
id="solved-ring" class="transition-all duration-700 ease-out"/>
</svg>
@ -69,7 +69,7 @@ import Layout from '../layouts/Layout.astro';
<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 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"
id="streak-ring" class="transition-all duration-700 ease-out"/>
</svg>

View file

@ -37,3 +37,37 @@ a {
a:hover {
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; }