diff --git a/.astro/content.d.ts b/.astro/content.d.ts new file mode 100644 index 0000000..d9eaab4 --- /dev/null +++ b/.astro/content.d.ts @@ -0,0 +1,154 @@ +declare module 'astro:content' { + export interface RenderResult { + Content: import('astro/runtime/server/index.js').AstroComponentFactory; + headings: import('astro').MarkdownHeading[]; + remarkPluginFrontmatter: Record; + } + interface Render { + '.md': Promise; + } + + export interface RenderedContent { + html: string; + metadata?: { + imagePaths: Array; + [key: string]: unknown; + }; + } + + type Flatten = T extends { [K: string]: infer U } ? U : never; + + export type CollectionKey = keyof DataEntryMap; + export type CollectionEntry = Flatten; + + type AllValuesOf = T extends any ? T[keyof T] : never; + + export type ReferenceDataEntry< + C extends CollectionKey, + E extends keyof DataEntryMap[C] = string, + > = { + collection: C; + id: E; + }; + + export type ReferenceLiveEntry = { + collection: C; + id: string; + }; + + export function getCollection>( + collection: C, + filter?: (entry: CollectionEntry) => entry is E, + ): Promise; + export function getCollection( + collection: C, + filter?: (entry: CollectionEntry) => unknown, + ): Promise[]>; + + export function getLiveCollection( + collection: C, + filter?: LiveLoaderCollectionFilterType, + ): Promise< + import('astro').LiveDataCollectionResult, LiveLoaderErrorType> + >; + + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + entry: ReferenceDataEntry, + ): E extends keyof DataEntryMap[C] + ? Promise + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + collection: C, + id: E, + ): E extends keyof DataEntryMap[C] + ? string extends keyof DataEntryMap[C] + ? Promise | undefined + : Promise + : Promise | undefined>; + export function getLiveEntry( + collection: C, + filter: string | LiveLoaderEntryFilterType, + ): Promise, LiveLoaderErrorType>>; + + /** Resolve an array of entry references from the same collection */ + export function getEntries( + entries: ReferenceDataEntry[], + ): Promise[]>; + + export function render( + entry: DataEntryMap[C][string], + ): Promise; + + export function reference< + C extends + | keyof DataEntryMap + // Allow generic `string` to avoid excessive type errors in the config + // if `dev` is not running to update as you edit. + // Invalid collection names will be caught at build time. + | (string & {}), + >( + collection: C, + ): import('astro/zod').ZodPipe< + import('astro/zod').ZodString, + import('astro/zod').ZodTransform< + C extends keyof DataEntryMap + ? { + collection: C; + id: string; + } + : never, + string + > + >; + + type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; + type InferEntrySchema = import('astro/zod').infer< + ReturnTypeOrOriginal['schema']> + >; + type ExtractLoaderConfig = T extends { loader: infer L } ? L : never; + type InferLoaderSchema< + C extends keyof DataEntryMap, + L = ExtractLoaderConfig, + > = L extends { schema: import('astro/zod').ZodSchema } + ? import('astro/zod').infer + : any; + + type DataEntryMap = { + + }; + + type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< + infer TData, + infer TEntryFilter, + infer TCollectionFilter, + infer TError + > + ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } + : { data: never; entryFilter: never; collectionFilter: never; error: never }; + type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; + type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; + type ExtractErrorType = ExtractLoaderTypes['error']; + + type LiveLoaderDataType = + LiveContentConfig['collections'][C]['schema'] extends undefined + ? ExtractDataType + : import('astro/zod').infer< + Exclude + >; + type LiveLoaderEntryFilterType = + ExtractEntryFilterType; + type LiveLoaderCollectionFilterType = + ExtractCollectionFilterType; + type LiveLoaderErrorType = ExtractErrorType< + LiveContentConfig['collections'][C]['loader'] + >; + + export type ContentConfig = never; + export type LiveContentConfig = never; +} diff --git a/.astro/types.d.ts b/.astro/types.d.ts index f964fe0..03d7cc4 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1 +1,2 @@ /// +/// \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..87ba802 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# PR Dojo — Agent Guide + +## Project +**Code Review Hunter** ("PR Dojo") — an Astro site where users practice finding bugs in rejected PR code, earn XP, and submit fixes. Currently a static Astro frontend; backend (Quarkus or Node.js+SQLite) is planned per `full_plan.md`. + +## Developer Commands +``` +bun install # install deps +bun dev # start dev server at localhost:4321 +bun build # static build → dist/ +bun preview # preview the build locally +bun astro ... # run Astro CLI (add, check, etc.) +``` +**Use `bun`, not npm or npx.** + +## Tech Stack +- **Astro 6** with **Tailwind CSS v4** via `@tailwindcss/vite` plugin (not the PostCSS pipeline) +- **TypeScript** strict mode via `astro/tsconfigs/strict` +- **Node >= 22.12.0** (enforced in `package.json` engines) +- No test framework, linter, or formatter configured yet + +## Architecture +``` +src/ + data/challenges.json # static challenge data (id, code, hints, expectedLines, expectedPatch) + pages/ + index.astro # challenge listing + hero + profile.astro # user profile / XP dashboard + challenges/[slug].astro # single challenge view with DiffViewer + components/ + DiffViewer.astro # side-by-side code diff UI + Welcome.astro # legacy starter component + layouts/Layout.astro # root layout wrapping header/nav/footer + styles/global.css # global styles +``` + +## Challenge Data Schema +Each entry in `src/data/challenges.json`: +- `id`, `title`, `repository`, `baseSha`, `difficulty` (1-5), `xpValue`, `bugType` +- `file`, `code` (buggy source), `hints` (array of "Line N: description") +- `expectedLines` (array of line numbers), `expectedPatch` (unified diff string) + +## Key Constraints +- Tailwind v4 uses the Vite plugin, not PostCSS. Do not add `tailwind.config.js` or `@layer` directives — they won't work. +- Astro SSR renders everything client-side by default. Add `client:*` directives only when interactivity is needed. +- `full_plan.md` documents the planned backend architecture and MVP roadmap. It is a reference, not a spec to implement blindly. +- No CI, no pre-commit hooks, no test runner. If you add tests, document the command in this file. diff --git a/dist/_astro/Layout.D3ovbguN.css b/dist/_astro/Layout.D3ovbguN.css new file mode 100644 index 0000000..144913b --- /dev/null +++ b/dist/_astro/Layout.D3ovbguN.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.static{position:static}.start{inset-inline-start:var(--spacing)}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-12{height:calc(var(--spacing) * 12)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-0{width:calc(var(--spacing) * 0)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-\[\#30363d\]{border-color:#30363d}.bg-\[\#0a3064\]{background-color:#0a3064}.bg-\[\#0d1117\]{background-color:#0d1117}.bg-\[\#1f6feb\]{background-color:#1f6feb}.bg-\[\#58a6ff\]{background-color:#58a6ff}.bg-\[\#161b22\]{background-color:#161b22}.bg-\[\#21262d\]{background-color:#21262d}.bg-\[\#a5d6ff\]{background-color:#a5d6ff}.bg-\[\#f8514920\]{background-color:#f8514920}.bg-\[\#ff7b7233\]{background-color:#ff7b7233}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.pr-4{padding-right:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-pre{white-space:pre}.text-\[\#8b949e\]{color:#8b949e}.text-\[\#58a6ff\]{color:#58a6ff}.text-\[\#79c0ff\]{color:#79c0ff}.text-\[\#484f58\]{color:#484f58}.text-\[\#30363d\]{color:#30363d}.text-\[\#a5d6ff\]{color:#a5d6ff}.text-\[\#c9d1d9\]{color:#c9d1d9}.text-\[\#e6edf3\]{color:#e6edf3}.text-\[\#f85149\]{color:#f85149}.text-\[\#ff7b72\]{color:#ff7b72}.text-\[3ac840\]{color:3ac840}.text-white{color:var(--color-white)}.italic{font-style:italic}.no-underline{text-decoration-line:none}.opacity-0{opacity:0}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:bg-\[\#161b22\]:hover{background-color:#161b22}.hover\:bg-\[\#388bfd\]:hover{background-color:#388bfd}.hover\:bg-\[\#30363d\]:hover{background-color:#30363d}.hover\:bg-\[\#f8514930\]:hover{background-color:#f8514930}.hover\:text-\[\#c9d1d9\]:hover{color:#c9d1d9}.hover\:text-\[\#f85149\]:hover{color:#f85149}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-\[\#58a6ff\]:focus{--tw-ring-color:#58a6ff}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-\[\#21262d\]:disabled{background-color:#21262d}.disabled\:text-\[\#484f58\]:disabled{color:#484f58}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}:root{--bg-primary:#0d1117;--bg-secondary:#161b22;--bg-tertiary:#21262d;--border-color:#30363d;--text-primary:#c9d1d9;--text-secondary:#8b949e;--text-muted:#6e7681;--blue-900:#0a3064;--blue-800:#1a549d;--blue-700:#1f6feb;--blue-600:#388bfd;--blue-500:#58a6ff;--blue-400:#79c0ff;--blue-300:#a5d6ff;--accent-purple:#a371f7;--accent-orange:#f2cc60;--accent-red:#f85149;--btn-primary:#1f6feb;--btn-primary-hover:#388bfd;--btn-secondary:#21262d;--btn-secondary-hover:#30363d}body{background-color:var(--bg-primary);color:var(--text-primary);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif}a,a:hover{text-decoration:none}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}html,body{margin:0;width:100%;height:100%} diff --git a/dist/challenges/1/index.html b/dist/challenges/1/index.html new file mode 100644 index 0000000..db9db37 --- /dev/null +++ b/dist/challenges/1/index.html @@ -0,0 +1,212 @@ + PR Dojo - Code Review Practice +
← Back to Challenges / Challenge #1

Memory Leak in Database Connection

★★★☆☆
📁 facebook/react 🔢 abc1234 Memory Safety
★★★☆☆
Difficulty
+100 XP reward
file.js
1 class DatabaseConnector { +• Line 15: Connection handle not released after use
2 constructor(url) {
3 this.url = url;
4 this.connection = null;
5 }
6
7 connect() {
8 this.connection = createConnection(this.url);
9 }
10
11 query(sql) {
12 return this.connection.execute(sql);
13 }
14
15 disconnect() { +• Connection handle not released after use
16 // Memory leak: connection never closed +• Resource cleanup missing in disconnect method
17 }
18 }

Hints:

  • Line 15: Connection handle not released after use
  • Line 16: Resource cleanup missing in disconnect method

Submit Your Fix

+Lines marked: 0

Click a marked line number below to edit its fix.

Click on lines in the diff above to mark them, then add your fixes here.

\ No newline at end of file diff --git a/dist/challenges/2/index.html b/dist/challenges/2/index.html new file mode 100644 index 0000000..ab5a2dd --- /dev/null +++ b/dist/challenges/2/index.html @@ -0,0 +1,211 @@ + PR Dojo - Code Review Practice +
← Back to Challenges / Challenge #2

Null Pointer Dereference in User Service

★★☆☆☆
📁 expressjs/express 🔢 def5678 Null Pointer
★★☆☆☆
Difficulty
+75 XP reward
file.js
1 function getUserProfile(userId) {
2 const user = database.findUser(userId);
3 return { +• No null check before accessing user properties
4 name: user.name,
5 email: user.email,
6 profile: user.profile.displayName +• Potential null pointer on user.profile
7 };
8 }

Hints:

  • Line 3: No null check before accessing user properties
  • Line 6: Potential null pointer on user.profile

Submit Your Fix

+Lines marked: 0

Click a marked line number below to edit its fix.

Click on lines in the diff above to mark them, then add your fixes here.

\ No newline at end of file diff --git a/dist/challenges/3/index.html b/dist/challenges/3/index.html new file mode 100644 index 0000000..184369f --- /dev/null +++ b/dist/challenges/3/index.html @@ -0,0 +1,212 @@ + PR Dojo - Code Review Practice +
← Back to Challenges / Challenge #3

Race Condition in Counter Service

★★★★☆
📁 rails/rails 🔢 ghi9012 Concurrency
★★★★☆
Difficulty
+150 XP reward
file.js
1 class Counter { +• Line 12: Same issue with decrement operation
2 constructor() {
3 this.value = 0;
4 }
5
6 increment() {
7 this.value = this.value + 1; +• Race condition when increment called concurrently
8 return this.value;
9 }
10
11 decrement() {
12 this.value = this.value - 1; +• Same issue with decrement operation
13 return this.value;
14 }
15 }

Hints:

  • Line 7: Race condition when increment called concurrently
  • Line 12: Same issue with decrement operation
  • No synchronization mechanism for thread safety

Submit Your Fix

+Lines marked: 0

Click a marked line number below to edit its fix.

Click on lines in the diff above to mark them, then add your fixes here.

\ No newline at end of file diff --git a/dist/challenges/4/index.html b/dist/challenges/4/index.html new file mode 100644 index 0000000..2ca58d1 --- /dev/null +++ b/dist/challenges/4/index.html @@ -0,0 +1,211 @@ + PR Dojo - Code Review Practice +
← Back to Challenges / Challenge #4

SQL Injection Vulnerability

★★★★★
📁 django/django 🔢 jkl3456 Security
★★★★★
Difficulty
+200 XP reward
file.js
1 function findUserByUsername(username) {
2 const query = `SELECT * FROM users WHERE username = '${username}'`; +• Direct string interpolation allows SQL injection
3 return database.query(query);
4 }
5
6 function deleteUser(userId) {
7 const query = `DELETE FROM users WHERE id = ${userId}`; +• Second SQL injection vulnerability in delete operation
8 return database.query(query);
9 }

Hints:

  • Line 2: Direct string interpolation allows SQL injection
  • Line 7: Second SQL injection vulnerability in delete operation
  • Should use parameterized queries instead

Submit Your Fix

+Lines marked: 0

Click a marked line number below to edit its fix.

Click on lines in the diff above to mark them, then add your fixes here.

\ No newline at end of file diff --git a/dist/challenges/5/index.html b/dist/challenges/5/index.html new file mode 100644 index 0000000..859842a --- /dev/null +++ b/dist/challenges/5/index.html @@ -0,0 +1,211 @@ + PR Dojo - Code Review Practice +
← Back to Challenges / Challenge #5

Infinite Loop in Data Processing

★★★☆☆
📁 angular/angular 🔢 mno7890 Logic Error
★★★☆☆
Difficulty
+100 XP reward
file.js
1 function processItems(items) {
2 let index = 0;
3 while (index < items.length) { +• Line 3-6: Infinite loop - index never increments
4 console.log(items[index]);
5 // Missing: index++ +• Missing increment statement causes endless iteration
6 }
7 return 'done';
8 }

Hints:

  • Line 3-6: Infinite loop - index never increments
  • Line 5: Missing increment statement causes endless iteration

Submit Your Fix

+Lines marked: 0

Click a marked line number below to edit its fix.

Click on lines in the diff above to mark them, then add your fixes here.

\ No newline at end of file diff --git a/dist/favicon.ico b/dist/favicon.ico new file mode 100644 index 0000000..7f48a94 Binary files /dev/null and b/dist/favicon.ico differ diff --git a/dist/favicon.svg b/dist/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/dist/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..526d3ae --- /dev/null +++ b/dist/index.html @@ -0,0 +1,21 @@ + PR Dojo - Code Review Practice
PR Dojo

Code Review Hunter

+Master code review skills by fixing rejected PRs. Earn XP, climb ranks, and become a bug detection expert. +

0
XP Earned
Novice
Current Rank
0
Challenges Solved

How It Works

+1 +

Find Bugs

+Analyze code and flag buggy sections with GitHub-style inline hints +

+2 +

Identify Lines

+Select exact line numbers where issues occur for bonus points +

+3 +

Submit Fix

+Write a unified diff patch and earn double XP on success +

Code Review Hunter - Practice fixing rejected PRs and earn XP

\ No newline at end of file diff --git a/dist/profile/index.html b/dist/profile/index.html new file mode 100644 index 0000000..0a19cd5 --- /dev/null +++ b/dist/profile/index.html @@ -0,0 +1,3 @@ + PR Dojo - Code Review Practice

Anonymous User

Code Review Practitioner

Total XP
0
Challenges Solved
0/5
Current Streak
0 days
Current Rank: Novice 0 / 500 XP
Next: Apprentice 500 / 1500 XP

Recent Activity

No challenges completed yet

+Start your first challenge → +
\ No newline at end of file diff --git a/src/components/DiffViewer.astro b/src/components/DiffViewer.astro index 782d014..774016c 100644 --- a/src/components/DiffViewer.astro +++ b/src/components/DiffViewer.astro @@ -5,68 +5,67 @@ interface Props { } const { code, hints } = Astro.props; +const id = `dv-${Math.random().toString(36).substring(2, 9)}`; const lines = code.split('\n'); --- -
-
-
-
-
-
+
+
+
+ {Astro.url.pathname.split('/').pop() || 'file.js'}
- {Astro.url.pathname.split('/').pop() || 'file.js'} -
-
- - - {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 ( - - - + + + ); + })} + +
- - {lineNum} - - - - {line || ' '} - - {hasHint && ( - - • {hintText?.replace(`Line ${lineNum}: `, '')} +
+ + + {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 ( + + - - ); - })} - -
+ + {lineNum} - )} -
-
- - {hints.length > 0 && ( -
-

Hints:

-
    - {hints.map((hint) => ( -
  • - - {hint} -
  • - ))} -
+
+ + {line || ' '} + + {hasHint && ( + + • {hintText?.replace(`Line ${lineNum}: `, '')} + + )} +
- )} + + {hints.length > 0 && ( +
+

Hints:

+
    + {hints.map((hint) => ( +
  • + + {hint} +
  • + ))} +
+
+ )} +
+ + diff --git a/src/pages/challenges/[slug].astro b/src/pages/challenges/[slug].astro index 6a5b62d..a79314c 100644 --- a/src/pages/challenges/[slug].astro +++ b/src/pages/challenges/[slug].astro @@ -75,41 +75,230 @@ const stars = '★'.repeat(challenge.difficulty) + '☆'.repeat(5 - challenge.di
-
- -
- + + + + +
+
+

Submit Your Fix

+
+ Lines marked: 0 +
- -
-
-

Analysis Tools

- -
-
-

Click on the left side of lines to mark them as buggy (like breakpoints).

-

Hover over marked lines to see hints.

-
+ + -
- - -
- - + +
+
+ +

Click a marked line number below to edit its fix.

+
+

Click on lines in the diff above to mark them, then add your fixes here.

+ + +
+ + +
+ +
+ + +
+ +
+ + + +