Zertifiziert
Selbstgesteuertes Projekt
Frontend Engineering with Prediction Game
Junior Front-End Engineer · Softwareentwicklung · Juni 2026
86/ 100
A verified, self-directed Junior Front-End Engineer project — completed and passed a real industry rubric at 86/100 on ProoV.
Bewertet nach
- Scoring engine correctness30%
- Prediction model genuinely from real data25%
- Game completeness and leaderboard soundness25%
- Shipped and shareable: live deployed URL20%
Bestanden · Bestehensgrenze 60/100
Was herausstach6
- Implemented scorePick with correct handling of exact scores, result-only matches, and draws
- Computed team attack/defense strengths from real results sample rather than hard-coding values
- Built a botPredict model that uses both teams' strengths and Poisson-style win probabilities
- Persisted and reloaded leaderboard data through localStorage with descending ranking
- Algorithmic Implementation
- Data-Driven Decision Making
Meine eingereichte Arbeit11 Aufgaben
Lab Orientation
Meine Lösung
{"code":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Predict the Cup</title>\n <style>\n body {\n font-family: -apple-system, sans-serif;\n background: #FDFCF7;\n color: #0E1726;\n padding: 32px;\n margin: 0;\n }\n h1 {\n color: #00C65A;\n font-size: 32px;\n margin: 0 0 12px 0;\n }\n p { color: #3F4B5F; margin: 0; }\n </style>\n </head>\n <body>\n <!-- TODO: change this title to something of your own -->\n <h1>Predict the world cup</h1>\n <p>Make your predictions, beat friends.</p>\n </body>\n</html>","ranAt":"2026-06-25T14:59:04.357Z"}Act1 Fixtures Form
Meine Lösung
{"code":"<!doctype html>\n<html><head>\n <meta charset=\"utf-8\" />\n <title>Predict the Cup · my picks</title>\n <style>\n body { font: 15px/1.45 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n padding: 24px; background: #F7F8FA; color: #0E1726; }\n h1 { margin: 0 0 4px; font-size: 22px; }\n .sub { color: #6A7589; font-size: 13px; margin-bottom: 16px; }\n #board { display: grid; gap: 12px; max-width: 640px; }\n .match { background: #fff; border: 1px solid #E4E8EE; border-radius: 12px;\n padding: 14px; }\n .meta { color: #6A7589; font-size: 11px; text-transform: uppercase;\n letter-spacing: 0.14em; margin-bottom: 6px; }\n .teams { font-weight: 800; font-size: 15.5px; margin-bottom: 8px; }\n .picks { display: grid; grid-template-columns: 1fr auto 1fr; gap: 8px; align-items: center; }\n .picks input { padding: 9px; border: 1px solid #E4E8EE; border-radius: 8px;\n text-align: center; font: inherit; font-weight: 700; font-size: 15px; }\n .vs { color: #6A7589; font-weight: 700; font-size: 12px; }\n footer { color: #6A7589; font-size: 11px; margin-top: 24px; max-width: 640px; line-height: 1.5; }\n </style>\n</head><body>\n\n <h1>My picks</h1>\n <div class=\"sub\">2026 World Cup · Round of 32</div>\n <div id=\"board\"></div>\n\n <footer>\n Independent learning project. Not affiliated with or endorsed by FIFA.\n Fixtures snapshot: openfootball/worldcup.json (CC0-1.0).\n </footer>\n\n <script>\n /* The real 2026 fixtures (sample slice of the bundled snapshot). */\n const FIXTURES = [\n { home: \"Germany\", away: \"Mexico\", date: \"2026-06-28\", round: \"R32\" },\n { home: \"Argentina\", away: \"Switzerland\", date: \"2026-06-29\", round: \"R32\" },\n { home: \"France\", away: \"Portugal\", date: \"2026-06-30\", round: \"R32\" },\n { home: \"Spain\", away: \"Uruguay\", date: \"2026-07-01\", round: \"R32\" },\n { home: \"Brazil\", away: \"Japan\", date: \"2026-07-02\", round: \"R32\" },\n { home: \"England\", away: \"Netherlands\", date: \"2026-07-03\", round: \"R32\" },\n ];\n\n /* TODO — Your task.\n * Loop FIXTURES and, for each match, build a card and append it to #board.\n * Each card MUST:\n * - be tagged with the attribute data-match (any value is fine)\n * - contain TWO <input type=\"number\"> (one for each team's predicted score)\n * - show both team names somewhere visible\n *\n * Tip: the worked example is the same shape on a 2-match sample.\n */\n function renderFixtures() {\n const board = document.getElementById(\"board\");\n\n FIXTURES.forEach(match => {\n const card = document.createElement(\"div\");\n\n // Required attribute\n card.setAttribute(\"data-match\", `${match.home}-${match.away}`);\n\n // Team names\n const title = document.createElement(\"p\");\n title.textContent = `${match.home} vs ${match.away}`;\n\n // Score inputs\n const homeScore = document.createElement(\"input\");\n homeScore.type = \"number\";\n\n const awayScore = document.createElement(\"input\");\n awayScore.type = \"number\";\n\n card.appendChild(title);\n card.appendChild(homeScore);\n card.appendChild(awayScore);\n\n board.appendChild(card);\n });\n}\n renderFixtures();\n </script>\n\n</body></html>","ranAt":"2026-06-25T15:03:53.244Z","passed":false,"attempt":1}Example Score Pick
Meine Lösung
{"code":"<!doctype html>\n<html lang=\"en\">\n <head><meta charset=\"utf-8\"><title>scorePick · worked example</title></head>\n <body>\n <h1>scorePick · one match</h1>\n <pre id=\"out\" style=\"font-family:ui-monospace,Menlo,monospace;font-size:13px\"></pre>\n\n <script>\n // The rule, in one function.\n function scorePick(predicted, actual) {\n // 1. Work out the result of each scoreline.\n // A \"result\" is just one of three labels: 'home', 'away', or 'draw'.\n function resultOf(s) {\n if (s.home > s.away) return 'home';\n if (s.home < s.away) return 'away';\n return 'draw';\n }\n\n const myResult = resultOf(predicted);\n const trueResult = resultOf(actual);\n const exactScore = (predicted.home === actual.home)\n && (predicted.away === actual.away);\n\n // 2. Compare and hand out points.\n if (exactScore) return 5; // nailed it\n if (myResult === trueResult) return 2; // right result, wrong score\n return 0; // wrong result\n }\n\n // Score four canonical cases out loud so you can see the rule work.\n const cases = [\n { pred: { home: 2, away: 1 }, actual: { home: 2, away: 1 }, why: 'exact 2-1' },\n { pred: { home: 3, away: 0 }, actual: { home: 1, away: 0 }, why: 'home win, wrong score' },\n { pred: { home: 0, away: 1 }, actual: { home: 2, away: 0 }, why: 'wrong result entirely' },\n { pred: { home: 1, away: 1 }, actual: { home: 2, away: 2 }, why: 'both draws, scores differ' },\n ];\n\n const out = document.getElementById('out');\n out.textContent = cases\n .map(c => `predicted ${c.pred.home}-${c.pred.away} actual ${c.actual.home}-${c.actual.away} -> ${scorePick(c.pred, c.actual)} pts // ${c.why}`)\n .join('\\n');\n </script>\n </body>\n</html>","ranAt":"2026-06-25T15:06:22.773Z"}Act2 Scoring Function
Meine Lösung
{"code":"<!doctype html>\n<html lang=\"en\">\n <head><meta charset=\"utf-8\"><title>scorePick · your turn</title></head>\n <body>\n <h1>scorePick · your turn</h1>\n <pre id=\"out\" style=\"font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:pre-wrap\"></pre>\n\n <script>\n // ─────────────────────────────────────────────────────────────────────\n // YOUR TASK: write the body of scorePick.\n //\n // It takes two scorelines and returns the points by the rule:\n // exact score -> 5\n // right result, wrong score -> 2\n // wrong result -> 0\n //\n // (A \"draw vs draw with different numbers\" still counts as the same\n // result, so 1-1 vs 2-2 should return 2, not 0.)\n // ─────────────────────────────────────────────────────────────────────\n function scorePick(predicted, actual) {\n if (\n predicted.home === actual.home &&\n predicted.away === actual.away\n ) {\n return 5;\n }\n\n const predResult =\n predicted.home > predicted.away ? \"H\" :\n predicted.home < predicted.away ? \"A\" : \"D\";\n\n const actualResult =\n actual.home > actual.away ? \"H\" :\n actual.home < actual.away ? \"A\" : \"D\";\n\n // Same result, different score\n if (predResult === actualResult) {\n return 2;\n }\n\n // Wrong result\n return 0;\n}\n\n // Local sanity-check, scored on screen so you can iterate without the\n // self-check button. The grader (Self-check) runs the same four cases.\n const cases = [\n { pred: { home: 2, away: 1 }, actual: { home: 2, away: 1 }, why: 'exact 2-1' },\n { pred: { home: 3, away: 0 }, actual: { home: 1, away: 0 }, why: 'home win, wrong score' },\n { pred: { home: 0, away: 1 }, actual: { home: 2, away: 0 }, why: 'wrong result' },\n { pred: { home: 1, away: 1 }, actual: { home: 2, away: 2 }, why: 'both draws' },\n ];\n\n const out = document.getElementById('out');\n out.textContent = cases\n .map(c => `${c.pred.home}-${c.pred.away} vs ${c.actual.home}-${c.actual.away} -> ${scorePick(c.pred, c.actual)} // ${c.why}`)\n .join('\\n');\n </script>\n </body>\n</html>","ranAt":"2026-06-25T15:08:17.596Z","passed":true,"attempt":1,"selfCheck":{"cases":[{"name":"exact 2-1 vs 2-1 returns 5","pass":true},{"name":"home win 3-0 vs 1-0 returns 2","pass":true},{"name":"wrong result 0-1 vs 2-0 returns 0","pass":true},{"name":"draw 1-1 vs 2-2 returns 2","pass":true}],"ranAt":"2026-06-25T15:08:17.596Z"}}Example Compute Strengths
Meine Lösung
{"code":"<!doctype html>\n<html>\n<head><meta charset=\"utf-8\"><title>Worked example · computeStrengths</title></head>\n<body>\n\n<script>\n// ── A tiny sample ────────────────────────────────────────────────────────────\n// (same SHAPE as your real results; different teams + numbers on purpose).\nconst SAMPLE = [\n { home: 'Japan', away: 'Senegal', homeScore: 2, awayScore: 1 },\n { home: 'Morocco', away: 'Japan', homeScore: 0, awayScore: 2 },\n { home: 'Senegal', away: 'Morocco', homeScore: 1, awayScore: 1 },\n];\n\n// ── computeStrengths ─────────────────────────────────────────────────────────\n// Two numbers per team: ATTACK = avg goals scored, DEFENSE = avg goals conceded.\nfunction computeStrengths(matches) {\n // Step 1 — a running tally for every team: scored, conceded, games.\n const tally = {};\n const seed = (t) => { if (!tally[t]) tally[t] = { scored: 0, conceded: 0, games: 0 }; };\n\n // Step 2 — every row touches BOTH teams. Home and away are mirror images.\n for (const m of matches) {\n seed(m.home); seed(m.away);\n // home team: SCORED its own goals, CONCEDED the away goals.\n tally[m.home].scored += m.homeScore;\n tally[m.home].conceded += m.awayScore;\n tally[m.home].games += 1;\n // away team: the other way round.\n tally[m.away].scored += m.awayScore;\n tally[m.away].conceded += m.homeScore;\n tally[m.away].games += 1;\n }\n\n // Step 3 — divide each total by games to get the two averages.\n const table = {};\n for (const team of Object.keys(tally)) {\n const r = tally[team];\n table[team] = { attack: r.scored / r.games, defense: r.conceded / r.games };\n }\n return table;\n}\n\n// ── The result ───────────────────────────────────────────────────────────────\nconst strengths = computeStrengths(SAMPLE);\nconsole.log(strengths.Japan); // → { attack: 2, defense: 0.5 }\n</script>\n\n</body>\n</html>\n","ranAt":"2026-06-25T15:13:20.120Z"}Act3 Compute Strength
Meine Lösung
{"passed":true,"attempt":1,"selfCheck":{"allPass":true,"failedNames":[]},"ranAt":"2026-06-25T15:14:43.450Z"}Act3 House Bot
Meine Lösung
{"passed":true,"attempt":1,"selfCheck":{"allPass":true,"failedNames":[]},"ranAt":"2026-06-25T15:15:53.798Z"}Example Save Rank Readonly
Meine Lösung
{"code":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>Worked example: save + rank one entry</title>\n <style>\n body { font-family: ui-sans-serif, system-ui, sans-serif; padding: 20px; max-width: 480px; margin: 0 auto; }\n h1 { font-size: 18px; margin: 0 0 4px 0; }\n .sub { color: #6B7280; font-size: 12px; margin: 0 0 14px 0; }\n table { width: 100%; border-collapse: collapse; }\n th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #E5E7EB; font-size: 14px; }\n th { color: #6B7280; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }\n .rank { width: 36px; color: #9CA3AF; font-variant-numeric: tabular-nums; }\n .total { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }\n </style>\n </head>\n <body>\n <h1>Pub quiz · leaderboard</h1>\n <p class=\"sub\">A two-game demo, saved + reloaded + sorted + painted.</p>\n <table>\n <thead><tr><th class=\"rank\">#</th><th>Player</th><th class=\"total\">Total</th></tr></thead>\n <tbody id=\"board\"></tbody>\n </table>\n\n <script>\n // ────────────────────────────────────────────────────────────────────\n // 1) ONE key for the whole leaderboard. One name per box, remember.\n // ────────────────────────────────────────────────────────────────────\n var KEY = 'demo_quiz_v1';\n\n // ────────────────────────────────────────────────────────────────────\n // 2) SAVE one entry. We stringify the array so the shape survives.\n // ────────────────────────────────────────────────────────────────────\n var entry = { name: 'Leo', total: 14 };\n\n var existing = JSON.parse(localStorage.getItem(KEY) || '[]'); // [] on first run\n existing.push(entry);\n localStorage.setItem(KEY, JSON.stringify(existing)); // stringify on save\n\n // (Demo: add one more entry so the table isn't a single row.)\n var existing2 = JSON.parse(localStorage.getItem(KEY) || '[]');\n if (!existing2.find(function (r) { return r.name === 'Sam'; })) {\n existing2.push({ name: 'Sam', total: 19 });\n localStorage.setItem(KEY, JSON.stringify(existing2));\n }\n\n // ────────────────────────────────────────────────────────────────────\n // 3) READ the box back. JSON.parse so we get the array, not a string.\n // ────────────────────────────────────────────────────────────────────\n var raw = localStorage.getItem(KEY);\n var rows = JSON.parse(raw || '[]');\n\n // ────────────────────────────────────────────────────────────────────\n // 4) SORT highest first. b minus a flips the order to descending.\n // ────────────────────────────────────────────────────────────────────\n rows.sort(function (a, b) { return b.total - a.total; });\n\n // ────────────────────────────────────────────────────────────────────\n // 5) PAINT the rows. One template string per entry, joined into HTML.\n // ────────────────────────────────────────────────────────────────────\n var html = rows.map(function (r, i) {\n return ''\n + '<tr>'\n + '<td class=\"rank\">' + (i + 1) + '</td>'\n + '<td>' + r.name + '</td>'\n + '<td class=\"total\">' + r.total + '</td>'\n + '</tr>';\n }).join('');\n\n document.getElementById('board').innerHTML = html;\n </script>\n </body>\n</html>\n","ranAt":"2026-06-25T15:16:19.555Z"}Act4 Leaderboard
Meine Lösung
{"code":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>Section 4 · the leaderboard</title>\n <style>\n body { font-family: ui-sans-serif, system-ui, sans-serif; padding: 20px; max-width: 520px; margin: 0 auto; }\n h1 { font-size: 18px; margin: 0 0 4px 0; }\n .sub { color: #6B7280; font-size: 12px; margin: 0 0 14px 0; }\n table { width: 100%; border-collapse: collapse; }\n th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #E5E7EB; font-size: 14px; }\n th { color: #6B7280; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }\n .rank { width: 36px; color: #9CA3AF; font-variant-numeric: tabular-nums; }\n .total { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }\n button { font: inherit; padding: 8px 14px; border-radius: 999px; border: 0; background: #00C65A; color: #fff; font-weight: 700; cursor: pointer; }\n </style>\n </head>\n <body>\n <h1>Predict the Cup · leaderboard</h1>\n <p class=\"sub\">One box, one ranked table.</p>\n <table>\n <thead><tr><th class=\"rank\">#</th><th>Player</th><th class=\"total\">Total</th></tr></thead>\n <tbody id=\"board\"></tbody>\n </table>\n <p style=\"margin-top:14px\"><button id=\"run\">Run saveAndRank()</button></p>\n\n <script>\n // ────────────────────────────────────────────────────────────────────\n // One key for the whole leaderboard. Don't change the name, the\n // self-check reads from this exact key.\n // ────────────────────────────────────────────────────────────────────\n var KEY = 'proov_predict_v1';\n\n // These three totals are what your file produces from scorePick + bot.\n // For this slide they're hard-coded so you focus on persistence + rank.\n var rowsThisRound = [\n { name: 'You', total: 17 },\n { name: 'House bot', total: 11 },\n { name: 'Alex', total: 22 }\n ];\n\n // ────────────────────────────────────────────────────────────────────\n // Helper: paint an array of rows into the #board tbody.\n // (No need to touch this. Your job is to feed it the right rows.)\n // ────────────────────────────────────────────────────────────────────\n function render(rows) {\n var html = rows.map(function (r, i) {\n return ''\n + '<tr>'\n + '<td class=\"rank\">' + (i + 1) + '</td>'\n + '<td>' + r.name + '</td>'\n + '<td class=\"total\">' + r.total + '</td>'\n + '</tr>';\n }).join('');\n document.getElementById('board').innerHTML = html;\n }\n\n // ════════════════════════════════════════════════════════════════════\n // YOUR TURN.\n //\n // Write the body of saveAndRank(entries). Three moves, like the worked\n // example:\n // 1) SAVE entries to localStorage under KEY (stringify on the way in).\n // 2) READ them back from KEY (parse on the way out).\n // 3) SORT highest total first, then render(rows) to paint the table.\n //\n // The self-check will:\n // - call saveAndRank(rowsThisRound)\n // - check the box at KEY holds the array with all three totals\n // - check the rendered rows are ordered highest -> lowest\n // - simulate a reload (re-read KEY, re-sort) and check the order again\n // ════════════════════════════════════════════════════════════════════\n function saveAndRank(entries) {\n localStorage.setItem(KEY, JSON.stringify(entries));\n\n // 2. Read back\n var rows = JSON.parse(localStorage.getItem(KEY));\n\n // 3. Sort highest total first\n rows.sort(function (a, b) {\n return b.total - a.total;\n });\n\n // Draw leaderboard\n render(rows);\n}\n\n // Wire the button so you can run your function from the page.\n document.getElementById('run').addEventListener('click', function () {\n s
…Act5 Assemble
Meine Lösung
{"code":"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>Predict the Cup — my World Cup prediction game</title>\n<style>\n :root { --green:#00C65A; --ink:#0E1726; --soft:#3F4B5F; --faint:#6A7589; --line:#E4E8EE; --paper:#fff; --bg:#F4F7F6; }\n * { box-sizing: border-box; }\n body { font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n margin:0; background:var(--bg); color:var(--ink); }\n .wrap { max-width: 760px; margin: 0 auto; padding: 22px 18px 40px; }\n header { display:flex; align-items:center; gap:12px; margin-bottom:6px; }\n .badge { width:42px; height:42px; border-radius:12px; background:var(--green); color:#fff;\n display:flex; align-items:center; justify-content:center; font-weight:900; font-size:18px; }\n h1 { font-size:21px; margin:0; letter-spacing:-0.01em; }\n .tag { color:var(--faint); font-size:13px; margin:2px 0 18px; }\n h2 { font-size:13px; text-transform:uppercase; letter-spacing:0.12em; color:var(--faint); margin:24px 0 12px; }\n #board { display:grid; grid-template-columns:1fr; gap:12px; }\n @media (min-width:560px){ #board { grid-template-columns:1fr 1fr; } }\n .match { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:14px; }\n .meta { color:var(--faint); font-size:10.5px; text-transform:uppercase; letter-spacing:0.12em; margin-bottom:8px; }\n .teams { display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; font-weight:800; font-size:15px; }\n .teams .vs { color:var(--faint); font-weight:700; font-size:11px; }\n .teams .away { text-align:right; }\n .picks { display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; margin-top:10px; }\n .picks input { padding:10px; border:1px solid var(--line); border-radius:9px; text-align:center;\n font:inherit; font-weight:800; font-size:16px; width:100%; }\n .picks input:focus { outline:2px solid var(--green); border-color:var(--green); }\n .picks .dash { color:var(--faint); font-weight:800; text-align:center; }\n .botline { margin-top:10px; font-size:12px; color:var(--soft); }\n .botline b { color:var(--ink); font-variant-numeric:tabular-nums; }\n .botline .prob { color:var(--faint); font-weight:600; }\n .hint { color:var(--faint); font-size:12px; line-height:1.5; margin:8px 0 2px; max-width:560px; }\n .challenge-banner { background:rgba(0,198,90,0.10); border:1px solid var(--green); border-radius:12px; padding:12px 14px; font-size:14px; font-weight:700; color:var(--ink); margin:14px 0 4px; }\n .vs-friend { font-size:15px; font-weight:800; color:var(--green); margin-top:12px; }\n .share-row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:14px; }\n .namebox { flex:1; min-width:150px; padding:11px 12px; border:1px solid var(--line); border-radius:10px; font:inherit; }\n .namebox:focus { outline:2px solid var(--green); border-color:var(--green); }\n .sharebtn { font:inherit; font-weight:800; color:#fff; background:var(--ink); border:0; border-radius:999px; padding:11px 18px; cursor:pointer; }\n .sharebtn:hover { filter:brightness(1.15); }\n .shareurl { width:100%; margin-top:8px; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font-family:ui-monospace,Menlo,monospace; font-size:12px; color:var(--soft); }\n .proov-credit { margin-top:12px; font-size:11.5px; }\n .proov-credit a { color:var(--green); font-weight:800; text-decoration:none; }\n .result { margin-top:10px; font-size:12px; font-weight:700; color:var(--green);\n border-top:1px dashed var(--line); padding-top:9px; }\n .scorebtn { margin-top:18px; font:inherit; font-weight:800; font-size:15px; color:#fff;\n background:var(--green); border:0; border-radius:999px; padding:13px 22px; cursor:pointer; width:100%; }\n @media (min-width:560px){ .scorebtn { width:auto; } }\n .
…Final Submission
Meine Lösung
{"url":"https://wondrous-llama-44c472.netlify.app/","submittedAt":"2026-06-25T15:21:49.868Z"}