├── .node-version ├── .eslintrc.json ├── public ├── logo.png ├── favicon.ico ├── github-mark.png ├── screenshot_game.png ├── screenshot_title.png └── vercel.svg ├── styles ├── globals.css └── Home.module.css ├── postcss.config.js ├── next.config.js ├── README.md ├── package.json~ ├── tailwind.config.js ├── pages ├── _app.tsx ├── api │ ├── hello.ts │ ├── game.ts │ ├── start_game.ts │ ├── send_answer.ts │ ├── question.ts │ └── og.tsx ├── about.tsx ├── index.tsx └── games │ └── [gameId] │ ├── result.tsx │ └── index.tsx ├── data ├── repo_with_snippets.ts └── github_repos.js ├── utils └── firebaseAdmin.ts ├── .gitignore ├── tsconfig.json ├── components ├── analytics.tsx └── Layout.tsx ├── LICENSE ├── package.json ├── types └── index.ts └── scripts └── fetch_code_snippets.ts /.node-version: -------------------------------------------------------------------------------- 1 | 18.8.0 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tan-z-tan/GitHubGuessr/HEAD/public/logo.png -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tan-z-tan/GitHubGuessr/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tan-z-tan/GitHubGuessr/HEAD/public/github-mark.png -------------------------------------------------------------------------------- /public/screenshot_game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tan-z-tan/GitHubGuessr/HEAD/public/screenshot_game.png -------------------------------------------------------------------------------- /public/screenshot_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tan-z-tan/GitHubGuessr/HEAD/public/screenshot_title.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitHubGuessr 2 | A simple game to guess the GitHub repository from a code snippet. 3 | 4 | [Play Game](https://github-guessr.vercel.app/) 5 | 6 | 7 | -------------------------------------------------------------------------------- /package.json~: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-guessr", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "../components/analytics"; 2 | import "../styles/globals.css"; 3 | import type { AppProps } from "next/app"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /data/repo_with_snippets.ts: -------------------------------------------------------------------------------- 1 | export const githubPopularRepos = [ 2 | { 3 | org: "facebook", 4 | name: "facebook/react", 5 | lang: "JavaScript", 6 | desc: "A declarative, efficient, and flexible JavaScript library for building user interfaces.", 7 | star_num: 0, 8 | fork_num: 0, 9 | avatarURL: "", 10 | url: "", 11 | snippets: [ 12 | "dummy data", 13 | "test data", 14 | "sample data", 15 | ] 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /utils/firebaseAdmin.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin'; 2 | 3 | if (!admin.apps.length) { 4 | const serviceAccount = { 5 | privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), 6 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 7 | projectId: process.env.FIREBASE_PROJECT_ID 8 | }; 9 | 10 | admin.initializeApp({ 11 | credential: admin.credential.cert(serviceAccount), 12 | }); 13 | } 14 | const firestore = admin.firestore(); 15 | 16 | export default firestore; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # .env 39 | .env 40 | .env.local 41 | 42 | # credentials 43 | firebase*.json 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "CommonJS", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /pages/api/game.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { GameData } from "../../types"; 3 | import firestore from "../../utils/firebaseAdmin"; 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse) => { 6 | const gameId = req.query.gameId as string; 7 | if (!gameId) { 8 | res.status(404).json({ message: "Game not found" }); 9 | return; 10 | } 11 | const game = await fetchGame(gameId); 12 | if (!game) { 13 | res.status(404).json({ message: "Game not found" }); 14 | return; 15 | } 16 | res.status(200).json(game); 17 | }; 18 | 19 | async function fetchGame(gameId: string): Promise { 20 | const game = await firestore.collection("games").doc(gameId).get(); 21 | if (!game.exists) { 22 | throw new Error("Game not found"); 23 | } 24 | return game.data() as GameData; 25 | } 26 | -------------------------------------------------------------------------------- /components/analytics.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Script from "next/script"; 3 | 4 | const ANALYTICS_ID = process.env.NEXT_PUBLIC_ANALYTICS_ID; 5 | 6 | export const Analytics: FC = () => { 7 | if (process.env.NODE_ENV !== "production") { 8 | return <>; 9 | } 10 | if (!ANALYTICS_ID) { 11 | return <>; 12 | } 13 | 14 | return ( 15 | <> 16 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /pages/api/start_game.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | import { GameData } from "../../types"; 4 | import firestore from "../../utils/firebaseAdmin"; 5 | 6 | export default async (req: NextApiRequest, res: NextApiResponse) => { 7 | const gameId = uuidv4().replace(/-/g, ""); 8 | const game: GameData = { 9 | id: gameId, 10 | username: req.body.username, 11 | theme: req.body.theme, 12 | roundNum: req.body.round_num || 10, 13 | rounds: [], 14 | correct_num: 0, 15 | correct_rate: 0, 16 | score: 0, 17 | finished_at: null, 18 | }; 19 | 20 | // store game data to firestore 21 | await storeGame(game); 22 | res.status(200).json({ game_id: game.id }); 23 | }; 24 | 25 | async function storeGame(game: GameData) { 26 | await firestore.collection("games").doc(game.id).set(game); 27 | } 28 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Makoto Tanji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-guessr", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "fetch-code-snippets": "ts-node scripts/fetch_code_snippets.ts" 11 | }, 12 | "dependencies": { 13 | "@nextui-org/react": "^2.1.12", 14 | "@vercel/og": "^0.5.13", 15 | "axios": "^1.5.0", 16 | "dotenv": "^16.3.1", 17 | "firebase-admin": "^11.10.1", 18 | "framer-motion": "^10.16.4", 19 | "lodash": "^4.17.21", 20 | "next": "13.4.16", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "react-select": "^5.7.4", 24 | "recharts": "^2.8.0", 25 | "ts-node": "^10.9.1", 26 | "uuid": "^9.0.0" 27 | }, 28 | "devDependencies": { 29 | "@types/lodash": "^4.14.198", 30 | "@types/node": "18.8.0", 31 | "@types/react": "18.2.21", 32 | "@types/react-dom": "18.2.7", 33 | "@types/uuid": "^9.0.3", 34 | "autoprefixer": "^10.4.15", 35 | "eslint": "8.48.0", 36 | "eslint-config-next": "13.4.16", 37 | "postcss": "^8.4.29", 38 | "tailwindcss": "^3.3.3", 39 | "typescript": "5.2.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export type Repository = { 2 | updatedAt: Date; 3 | org: string; 4 | avatarURL: string; 5 | name: string; 6 | url: string; 7 | lang: string; 8 | desc: string; 9 | star_num: number; 10 | fork_num: number; 11 | snippets: string[]; 12 | }; 13 | 14 | export type Theme = { 15 | id: number; 16 | title: string; 17 | difficulty: "easy" | "medium" | "hard"; 18 | }; 19 | 20 | export type ThemeRepository = { 21 | theme_id: number; 22 | repository_id: string; 23 | }; 24 | 25 | export type QuestionData = { 26 | repository: Repository; 27 | candidates: string[]; 28 | }; 29 | 30 | export type Answer = { 31 | user_id: string; 32 | user_answer: string; 33 | time_remaining: number; 34 | correct_answer: string; 35 | repo_image_url: string; 36 | repo_url: string; 37 | is_correct: boolean; 38 | }; 39 | 40 | export type GameData = { 41 | id: string; 42 | username: string; 43 | theme: string; 44 | roundNum: number; 45 | rounds: { 46 | repoName: string; 47 | userAnswer: string | null; 48 | timeRemaining: number; 49 | isCorrect: boolean; 50 | }[]; 51 | correct_num: number; 52 | correct_rate: number; 53 | score: number; 54 | finished_at: Date | null; 55 | }; 56 | -------------------------------------------------------------------------------- /pages/api/send_answer.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import firestore from "../../utils/firebaseAdmin"; 3 | import { GameData } from "../../types"; 4 | 5 | type UserAnswer = { 6 | user_id: string; 7 | game_id: string; 8 | user_answer: string; 9 | round: number; 10 | correct_answer: string; 11 | time_remaining: number; 12 | is_correct: boolean; 13 | }; 14 | 15 | export default async (req: NextApiRequest, res: NextApiResponse) => { 16 | const { user_id, game_id, user_answer, time_remaining } = req.body; 17 | const game = await fetchGame(game_id); 18 | if (!game) { 19 | res.status(404).json({ message: "Game not found" }); 20 | return; 21 | } 22 | const round = game.rounds.length - 1; // starts from 0 23 | const correctRepo = game.rounds[round].repoName; 24 | 25 | const isCorrect = user_answer === correctRepo; 26 | const answer: UserAnswer = { 27 | user_id: user_id, 28 | game_id: game_id, 29 | user_answer: user_answer, 30 | round: round, 31 | correct_answer: correctRepo, 32 | time_remaining: time_remaining, 33 | is_correct: isCorrect, 34 | }; 35 | await storeAnswer(answer); 36 | 37 | game.rounds[round].userAnswer = user_answer; 38 | game.rounds[round].timeRemaining = time_remaining; 39 | game.rounds[round].isCorrect = isCorrect; 40 | 41 | if (isCorrect) { 42 | game.correct_num++; 43 | game.correct_rate = game.correct_num / game.roundNum; 44 | game.score += 100 + time_remaining; 45 | } 46 | 47 | if (game.rounds.length >= game.roundNum) { 48 | game.finished_at = new Date(); 49 | } 50 | await updateGame(game_id, game); 51 | 52 | res.status(200).json({ 53 | isCorrect: isCorrect, 54 | finished: game.finished_at ? true : false, 55 | }); 56 | }; 57 | 58 | async function storeAnswer(answer: UserAnswer) { 59 | firestore.collection("answers").add(answer); 60 | } 61 | 62 | async function fetchGame(gameId: string): Promise { 63 | const game = await firestore.collection("games").doc(gameId).get(); 64 | if (!game.exists) { 65 | throw new Error("Game not found"); 66 | } 67 | return game.data() as GameData; 68 | } 69 | 70 | async function updateGame(gameId: string, game: any) { 71 | await firestore.collection("games").doc(gameId).set(game); 72 | } 73 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /pages/api/question.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { GameData, QuestionData, Repository } from "../../types"; 3 | import { githubPopularRepos } from "../../data/repo_with_snippets"; 4 | import lodash from "lodash"; 5 | import firestore from "../../utils/firebaseAdmin"; 6 | 7 | const QUIZ_NUM = 12; 8 | export default async (req: NextApiRequest, res: NextApiResponse) => { 9 | const gameId = req.query.game_id as string; 10 | const game = await fetchGame(gameId); 11 | if (!game) { 12 | res.status(404).json({ message: "Game not found" }); 13 | return; 14 | } 15 | 16 | if (game.rounds.length > 0 && game.rounds[game.rounds.length - 1].userAnswer === null) { 17 | // already fetched question but not answered yet. Remove the last round. 18 | game.rounds.pop(); 19 | } 20 | 21 | // Pick random repositories from githubPopularRepos 22 | const allRepos = filterUsedRepos(githubPopularRepos, game.rounds.map((round: any) => round.repoName)); 23 | const randomRepos: Repository[] = lodash.sampleSize( 24 | allRepos.filter((repo) => repo.snippets.length > 0), 25 | QUIZ_NUM 26 | ); 27 | const targetRepo = randomRepos[0]; 28 | 29 | // snippet must be more than 32 characters and shuffled. Return only 5 snippets at most. 30 | var snippets = targetRepo.snippets.filter((snippet) => snippet.length > 32); 31 | snippets.sort(() => Math.random() - 0.5); 32 | snippets = snippets.slice(0, 5); 33 | 34 | game.rounds.push({ 35 | repoName: targetRepo.name, 36 | userAnswer: null, 37 | isCorrect: false, 38 | timeRemaining: -1, 39 | }); 40 | await updateGame(gameId, game); 41 | 42 | const questionData: QuestionData = { 43 | repository: { 44 | org: targetRepo.org, 45 | updatedAt: new Date(), // we don't use updatedAt for now 46 | avatarURL: targetRepo.avatarURL, 47 | name: targetRepo.name, 48 | url: targetRepo.url, 49 | lang: targetRepo.lang, 50 | desc: targetRepo.desc, 51 | star_num: targetRepo.star_num, 52 | fork_num: targetRepo.fork_num, 53 | snippets: snippets, 54 | }, 55 | candidates: lodash.shuffle(randomRepos).map((repo) => repo.name), 56 | }; 57 | 58 | res.status(200).json(questionData); 59 | }; 60 | 61 | function filterUsedRepos( 62 | repos: any[], 63 | usedRepoNames: string[] 64 | ) { 65 | return repos.filter((repo) => { 66 | return !usedRepoNames.includes(repo.name); 67 | }); 68 | } 69 | 70 | async function fetchGame(gameId: string): Promise { 71 | const game = await firestore.collection("games").doc(gameId).get(); 72 | if (!game.exists) { 73 | throw new Error("Game not found"); 74 | } 75 | return game.data() as GameData; 76 | } 77 | 78 | async function updateGame(gameId: string, game: any) { 79 | await firestore.collection("games").doc(gameId).set(game); 80 | } 81 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | import { PropsWithChildren } from "react"; 6 | 7 | export default function Layout({ 8 | children, 9 | className, 10 | title, 11 | ogUrl, 12 | ogImageUrl, 13 | description, 14 | }: PropsWithChildren<{ 15 | className?: string; 16 | title?: string; 17 | ogUrl?: string; 18 | ogImageUrl?: string; 19 | description?: string; 20 | }>) { 21 | const router = useRouter(); 22 | const { locales, defaultLocale, asPath } = router; 23 | const isTopPage = router.pathname === "/"; 24 | const desc = 25 | description || 26 | "Guess GitHub repositories by codes. GitHubGuessr is a game for geeks. Can you guess the GitHub repository from the code?"; 27 | 28 | return ( 29 | 48 | 49 | {title || "GitHub-Guessr"} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {children} 67 |
68 | 69 | GitHub-Guessr 70 | 71 | 77 | 82 | Code 83 | 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import Layout from "../components/Layout"; 3 | 4 | export default function About() { 5 | return ( 6 | 7 |
10 |

15 | About GitHub-Guessr 16 |

17 |
18 |

19 | What is GitHub-Guessr? 20 |

21 |

22 | GitHub-Guessr is a game designed for engineers where the challenge 23 | is to identify GitHub repositories based on code snippets. 24 |
25 | In today's coding world, we often use many open-source 26 | libraries without looking at their code. With GitHub-Guessr, you can 27 | play a game to learn how popular libraries are made and which 28 | languages they use. Ity's also a fun way to test your skills. 29 |

30 |

31 | How to play 32 |

33 |

34 | The game is simple. You will be presented with a code snippet from a 35 | random popular GitHub repository. Your task is to guess the 36 | repository from the code. There are 10 rounds in a game. For each 37 | round you need to select the correct repository within 60 seconds. 38 |
39 | You can guess by programming language, what the code does, or any 40 | information you can get from the code. The final score is calculated 41 | based on the number of correct answers and the time remaining. 42 |

43 |

44 | Contact and Feedback 45 |

46 |

47 | If you have any suggestions or comments regarding the game, they are 48 | very welcome. 49 |
50 | 56 | Contact form 57 | 58 |

59 |

Wishing you a delightful engineering journey!

60 |

61 | GitHub-Guessr is created by{" "} 62 | @tan_z_tan. 63 |

64 |
65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /pages/api/og.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/server"; 2 | import { GameData } from "../../types"; 3 | import { NextRequest } from "next/server"; 4 | import { sys } from "typescript"; 5 | 6 | export const config = { 7 | runtime: "edge", 8 | }; 9 | 10 | export default async function handler(req: NextRequest) { 11 | const { searchParams } = new URL(req.url); 12 | const gameId = searchParams.get("gameId"); 13 | if (!gameId) { 14 | return defaultOGImage(); 15 | } 16 | const game = await fetch( 17 | `${process.env.SERVERHOST}/api/game?gameId=${gameId}` 18 | ).then(async (res) => { 19 | if (!res.ok) { 20 | return null; 21 | } 22 | 23 | if (res.status === 404) { 24 | return null; 25 | } 26 | // GameData 27 | return (await res.json()) as GameData; 28 | }); 29 | if (!game) { 30 | return defaultOGImage(); 31 | } 32 | 33 | return new ImageResponse( 34 | ( 35 |
47 |
59 | 63 |

72 | 80 | {game.username} 81 | 82 | 83 | guessed {game.correct_num} repos correctly!😍 84 | 85 |

86 |

95 | Score: {game.score} 96 |

97 |

98 | https://github-guessr.vercel.app 99 |

100 |
101 |
102 | ) 103 | ); 104 | } 105 | 106 | function defaultOGImage() { 107 | return new ImageResponse( 108 | ( 109 |
122 |
134 | 138 |

147 | Can you guess the GitHub repository from the code? 148 |

149 |

150 | https://github-guessr.vercel.app 151 |

152 |
153 |
154 | ) 155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { Modal, ModalContent } from "@nextui-org/react"; 3 | 4 | import { useEffect, useRef, useState } from "react"; 5 | import { motion } from "framer-motion"; 6 | import { Answer } from "../types"; 7 | import Layout from "../components/Layout"; 8 | 9 | const Home: NextPage = () => { 10 | const [answerLog, setAnswerLog] = useState([]); 11 | const [username, setUsername] = useState(""); 12 | const [theme, setTheme] = useState("all"); 13 | const [nameModalOpen, setNameModalOpen] = useState(false); 14 | const usernameRef = useRef(null); 15 | 16 | useEffect(() => { 17 | const answerLog = JSON.parse(localStorage.getItem("answerLog") || "[]"); 18 | if (answerLog.length) { 19 | setAnswerLog(answerLog); 20 | } 21 | const savedUsername = localStorage.getItem("username"); 22 | if (savedUsername) { 23 | setUsername(savedUsername); 24 | } 25 | }, []); 26 | 27 | async function startGame(username?: string) { 28 | // ゲーム開始時にanswerLogをリセット 29 | if (username === undefined || username === "") { 30 | setNameModalOpen(true); 31 | return; 32 | } 33 | 34 | const res = await fetch("/api/start_game", { 35 | method: "POST", 36 | headers: { 37 | "Content-Type": "application/json", 38 | }, 39 | body: JSON.stringify({ username, theme }), 40 | }); 41 | if (!res.ok) { 42 | alert("Failed to start game"); 43 | return; 44 | } 45 | 46 | const { game_id } = await res.json(); 47 | localStorage.removeItem("answerLog"); 48 | localStorage.setItem("game_id", game_id); 49 | window.location.href = `/games/${game_id}`; 50 | } 51 | 52 | return ( 53 | 54 |
55 |

60 | GitHub-Guessr 61 |

62 |
63 | Can you guess the GitHub repository from the code? 64 | {username.length > 0 && ( 65 |

66 | Hello {username}! 67 |

68 | )} 69 |
70 | startGame(username)} 75 | > 76 | Start Game 77 | 78 | {answerLog.length > 0 && ( 79 |
80 |

81 | Your previous score 82 |

83 | {answerLog.map((answer, index) => ( 84 |
85 | {index + 1}: 86 | 91 | {answer.is_correct ? " Correct! " : " Wrong. "} 92 | 93 | 98 | [{answer.correct_answer}] 99 |
100 | ))} 101 |
102 | )} 103 | 111 |
112 | setNameModalOpen(false)} 115 | backdrop="opaque" 116 | radius="sm" 117 | classNames={{ 118 | base: "text-gray-400 fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80", 119 | closeButton: "hover:bg-white/5 active:bg-white/10", 120 | }} 121 | > 122 | 123 | <> 124 |

125 | Please enter your name 126 |

127 | 131 | { 136 | localStorage.setItem( 137 | "username", 138 | usernameRef.current?.value || "" 139 | ); 140 | setUsername(usernameRef.current?.value || ""); 141 | setNameModalOpen(false); 142 | startGame(usernameRef.current?.value || ""); 143 | }} 144 | > 145 | Start Game 146 | 147 | 148 |
149 |
150 |
151 | ); 152 | }; 153 | 154 | export default Home; 155 | -------------------------------------------------------------------------------- /scripts/fetch_code_snippets.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import axios from "axios"; 3 | import { Repository } from "../types"; 4 | import { githubPopularRepos } from "../data/github_repos"; 5 | import dotenv from "dotenv"; 6 | import * as admin from "firebase-admin"; 7 | import firestore from "../utils/firebaseAdmin"; 8 | 9 | dotenv.config(); // .envの内容をprocess.envに反映 10 | 11 | const EXT_BY_LANG: { [key: string]: string } = { 12 | TypeScript: ".ts", 13 | JavaScript: ".js", 14 | Python: ".py", 15 | Go: ".go", 16 | C: ".c", 17 | D: ".d", 18 | "C++": ".cpp", 19 | Java: ".java", 20 | Ruby: ".rb", 21 | PHP: ".php", 22 | Rust: ".rs", 23 | Swift: ".swift", 24 | Kotlin: ".kt", 25 | Scala: ".scala", 26 | Perl: ".pl", 27 | Haskell: ".hs", 28 | Clojure: ".clj", 29 | Elixir: ".ex", 30 | Erlang: ".erl", 31 | Dart: ".dart", 32 | Crystal: ".cr", 33 | Julia: ".jl", 34 | Lua: ".lua", 35 | OCaml: ".ml", 36 | R: ".r", 37 | Scheme: ".scm", 38 | "Vim script": ".vim", 39 | Shell: ".sh", 40 | PowerShell: ".ps1", 41 | HTML: ".html", 42 | CSS: ".css", 43 | SCSS: ".scss", 44 | Less: ".less", 45 | }; 46 | const GITHUB_API_URL = "https://api.github.com"; 47 | const SNIPPETS = 10; 48 | const MAX_LINE = 32; 49 | 50 | async function selectRandomSnippet(repoName: string, files: any[]) { 51 | for (let i = 0; i < SNIPPETS; i++) { 52 | // retry 5 times 53 | for (let retry = 0; retry < 5; retry++) { 54 | const randomIndex = Math.floor(Math.random() * files.length); 55 | const f = files[randomIndex]; 56 | 57 | if (!f) { 58 | continue; 59 | } 60 | // timeout 10s 61 | const response = await axios.get( 62 | `${GITHUB_API_URL}/repos/${repoName}/contents/${f.path}`, 63 | { 64 | headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` }, 65 | timeout: 10000, 66 | } 67 | ); 68 | // Base64デコードして内容を取得 69 | const content = Buffer.from(response.data.content, "base64").toString( 70 | "utf-8" 71 | ); 72 | // if there is no alphabet character, retry 73 | if (!content.match(/[a-z]/)) { 74 | console.log("a to z character found"); 75 | continue; 76 | } 77 | 78 | // if snippet contains repository name, retry 79 | if (content.includes(repoName)) { 80 | console.log("repository name found"); 81 | continue; 82 | } 83 | 84 | const lines = content.split("\n"); 85 | // line が10行未満の場合はリトライ 86 | if (lines.length < 10) { 87 | continue; 88 | } 89 | 90 | // ファイルのランダムな部分を取得 91 | const randomStart = Math.floor(Math.random() * (lines.length - MAX_LINE)); 92 | const snippet = content 93 | .split("\n") 94 | .slice(randomStart, randomStart + MAX_LINE) 95 | .join("\n"); 96 | return snippet; 97 | } 98 | } 99 | return undefined; 100 | } 101 | 102 | async function getRandomSnippetsFromRepo( 103 | repoName: string, 104 | defaultBranch: string, 105 | langs: string[] 106 | ): Promise { 107 | console.log( 108 | `${GITHUB_API_URL}/repos/${repoName}/git/trees/${defaultBranch}?recursive=1` 109 | ); 110 | console.log("langs", langs); 111 | const response = await axios.get( 112 | `${GITHUB_API_URL}/repos/${repoName}/git/trees/${defaultBranch}?recursive=1`, 113 | // githun access token 114 | { 115 | headers: { 116 | Authorization: `token ${process.env.GITHUB_TOKEN}`, 117 | timeout: 10000, 118 | }, 119 | } 120 | ); 121 | const tree = response.data.tree; 122 | const fileExts = langs.map((lang) => EXT_BY_LANG[lang]).filter((ext) => ext); 123 | const files = tree.filter( 124 | (item: any) => 125 | item.type === "blob" && fileExts.some((ext) => item.path.endsWith(ext)) 126 | ); 127 | 128 | const randomSnippets: string[] = []; 129 | for (let i = 0; i < SNIPPETS; i++) { 130 | const snippet = await selectRandomSnippet(repoName, files); 131 | if (!snippet) { 132 | continue; 133 | } 134 | randomSnippets.push(snippet); 135 | } 136 | return randomSnippets; 137 | } 138 | 139 | async function upsertRepository(repository: Repository) { 140 | const repoId = repository.name.replace("/", "-"); 141 | const docRef = firestore.collection("repositories").doc(repoId); 142 | const doc = await docRef.get(); 143 | if (doc.exists) { 144 | await docRef.update(repository); 145 | } else { 146 | await docRef.set(repository); 147 | } 148 | } 149 | 150 | async function fetchRepository( 151 | repoName: string 152 | ): Promise { 153 | const repoId = repoName.replace("/", "-"); 154 | const docRef = firestore.collection("repositories").doc(repoId); 155 | const doc = await docRef.get(); 156 | if (doc.exists) { 157 | const data = doc.data(); 158 | if (!data) { 159 | return undefined; 160 | } 161 | const updatedAt = data.updatedAt.toDate(); 162 | return { 163 | ...data, 164 | updatedAt: updatedAt, 165 | } as Repository; 166 | } else { 167 | return undefined; 168 | } 169 | } 170 | 171 | async function fetchSnippetsForRepos() { 172 | const output: Repository[] = []; 173 | 174 | for (const repoMeta of githubPopularRepos) { 175 | // fetch repo information 176 | console.log(`fetching ${repoMeta.name}`); 177 | const repoDoc = await fetchRepository(repoMeta.name); 178 | if ( 179 | repoDoc && 180 | repoDoc.updatedAt.getTime() > new Date().getTime() - 1000 * 60 * 60 * 12 181 | ) { 182 | console.log("skip"); 183 | output.push(repoDoc); 184 | continue; 185 | } 186 | 187 | const repositoryResponse = await fetch( 188 | `https://api.github.com/repos/${repoMeta.name}`, 189 | { 190 | referrerPolicy: "no-referrer", 191 | headers: { 192 | Accept: "application/vnd.github+json", 193 | "X-GitHub-Api-Version": "2022-11-28", 194 | Authorization: `token ${process.env.GITHUB_TOKEN}`, 195 | }, 196 | } 197 | ); 198 | const repositoryJSON = await repositoryResponse.json(); 199 | const starCount = repositoryJSON.stargazers_count; 200 | const flakCount = repositoryJSON.forks_count; 201 | const avatarURL = repositoryJSON.owner?.avatar_url; 202 | const githubURL = repositoryJSON.html_url; 203 | const defaultBranch: string = repositoryJSON.default_branch; 204 | const lang = repoMeta.lang; 205 | const snippets = await getRandomSnippetsFromRepo( 206 | repoMeta.name, 207 | defaultBranch, 208 | lang.split(",").map((l) => l.trim()) 209 | ); 210 | const repo: Repository = { 211 | org: repoMeta.name.split("/")[0], 212 | updatedAt: new Date(), 213 | avatarURL: avatarURL, 214 | name: repoMeta.name, 215 | url: githubURL, 216 | lang: lang, 217 | desc: repoMeta.desc, 218 | star_num: starCount, 219 | fork_num: flakCount, 220 | snippets: snippets, 221 | }; 222 | output.push(repo); 223 | await upsertRepository(repo); 224 | } 225 | 226 | // write to file 227 | fs.writeFileSync( 228 | "./data/repo_with_snippets.ts", 229 | `export const githubPopularRepos = ${JSON.stringify(output, null, 2)}` 230 | ); 231 | } 232 | 233 | // 関数を呼び出して結果を出力 234 | fetchSnippetsForRepos(); 235 | -------------------------------------------------------------------------------- /pages/games/[gameId]/result.tsx: -------------------------------------------------------------------------------- 1 | import { GameData } from "../../../types"; 2 | import { motion } from "framer-motion"; 3 | import Layout from "../../../components/Layout"; 4 | import React from "react"; 5 | import { BarChart, Bar, Cell, ResponsiveContainer } from "recharts"; 6 | 7 | export default function Result({ game }: { game: GameData | null }) { 8 | function shareResult() { 9 | const url = `https://github-guessr.vercel.app/games/${game?.id}/result`; 10 | const text = `My 😺GitHub-Guessr😺 score is ${game?.score}!\n`; 11 | const hashtags = "GitHubGuessr"; 12 | const encodedUrl = encodeURIComponent(url); 13 | const encodedText = encodeURIComponent(text); 14 | const encodedHashtags = encodeURIComponent(hashtags); 15 | const shareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedText}&hashtags=${encodedHashtags}`; 16 | window.open(shareUrl, "_blank"); 17 | } 18 | 19 | if (game === null) { 20 | return ( 21 | 22 |

No result found 🧐

23 |

24 | GitHub-Guessr 25 |

26 | (window.location.href = "/")} 32 | > 33 | Go to Top 34 | 35 |
36 | ); 37 | } 38 | const hitNum = game.correct_num; 39 | const score = game.score; 40 | const scoreBin = Math.ceil((hitNum / game.rounds.length) * 10) / 10; 41 | const data = [ 42 | { 43 | name: "0", 44 | ratio: 0.01131221719, 45 | color: "gray", 46 | }, 47 | { 48 | name: "0.1", 49 | ratio: 0.03846153846, 50 | color: "gray", 51 | }, 52 | { 53 | name: "0.2", 54 | ratio: 0.04740406321, 55 | color: "gray", 56 | }, 57 | { 58 | name: "0.3", 59 | ratio: 0.06334841629, 60 | color: "gray", 61 | }, 62 | { 63 | name: "0.4", 64 | ratio: 0.1199095023, 65 | color: "gray", 66 | }, 67 | { 68 | name: "0.5", 69 | ratio: 0.1719457014, 70 | color: "gray", 71 | }, 72 | { 73 | name: "0.6", 74 | ratio: 0.149321267, 75 | color: "gray", 76 | }, 77 | { 78 | name: "0.7", 79 | ratio: 0.1606334842, 80 | color: "gray", 81 | }, 82 | { 83 | name: "0.8", 84 | ratio: 0.1199095023, 85 | color: "gray", 86 | }, 87 | { 88 | name: "0.9", 89 | ratio: 0.07239819005, 90 | color: "gray", 91 | }, 92 | { 93 | name: "1.0", 94 | ratio: 0.04524886878, 95 | color: "gray", 96 | }, 97 | ]; 98 | 99 | return ( 100 | 105 |
106 |

107 | Player{" "} 108 | 109 | {game.username} 110 | {"'s"} 111 | 112 |
113 | GitHub-Guessr score is{" "} 114 | 115 | 116 | {" " + score} 117 | {score > 1000 118 | ? "🎉🎉🎉" 119 | : score >= 700 120 | ? "🎉" 121 | : score >= 400 122 | ? "🙂" 123 | : "😢"} 124 | 125 | 126 |

127 |
128 | 129 | 130 | 131 | {data.map((entry, index) => ( 132 | 138 | ))} 139 | 140 | 141 | 142 |
143 |

144 | You guessed {hitNum} repositories correctly! 145 |

146 | 147 | 148 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | 158 | {game.rounds.map((round, index) => ( 159 | 160 | 163 | 172 | 181 | 189 | 190 | ))} 191 | 192 |
151 | Your Answer 152 | AnswerResult
161 | {index + 1} 162 | 164 | 169 | {round.userAnswer?.replace("/", "/ ")} 170 | 171 | 173 | 178 | {round.repoName.replace("/", "/ ")} 179 | 180 | 187 | {round.isCorrect ? "Correct" : "Wrong"} 188 |
193 | 199 | Share your score 200 | 201 | (window.location.href = "/")} 207 | > 208 | Go to Top 209 | 210 |
211 |
212 | ); 213 | } 214 | 215 | export const getServerSideProps = async (context: any) => { 216 | const gameId = context.params.gameId; 217 | const protocol = context.req.headers['x-forwarded-proto'] || 'http'; 218 | const host = context.req.headers.host; 219 | const baseUrl = `${protocol}://${host}`; 220 | 221 | if (!gameId) { 222 | return { 223 | props: { 224 | gameId: null, 225 | }, 226 | }; 227 | } 228 | 229 | const game: GameData = await fetch(`${baseUrl}/api/game?gameId=${gameId}`).then( 230 | async (res) => { 231 | if (!res.ok) { 232 | return null; 233 | } 234 | if (res.status === 404) { 235 | return null; 236 | } 237 | const game = await res.json(); 238 | return game; 239 | } 240 | ); 241 | 242 | return { 243 | props: { 244 | game: game, 245 | }, 246 | }; 247 | }; 248 | -------------------------------------------------------------------------------- /data/github_repos.js: -------------------------------------------------------------------------------- 1 | export const githubPopularRepos = [ 2 | { 3 | name: "torvalds/linux", 4 | lang: "C", 5 | desc: "The official Linux kernel source code, maintained by Linus Torvalds. It's the foundation for Linux operating systems, with global contributions.", 6 | }, 7 | { 8 | name: "microsoft/vscode", 9 | lang: "TypeScript", 10 | desc: "Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.", 11 | }, 12 | { 13 | name: "facebook/react", 14 | lang: "JavaScript", 15 | desc: "A declarative, efficient, and flexible JavaScript library for building user interfaces.", 16 | }, 17 | { 18 | name: "angular/angular", 19 | lang: "TypeScript", 20 | desc: "The core infrastructure backend (BaaS) framework for Angular.", 21 | }, 22 | { 23 | name: "tensorflow/tensorflow", 24 | lang: "Python", 25 | desc: "An open-source platform for machine learning.", 26 | }, 27 | { 28 | name: "twbs/bootstrap", 29 | lang: "HTML, CSS, JavaScript", 30 | desc: "The most popular HTML, CSS, and JavaScript framework for developing responsive, mobile-first projects on the web.", 31 | }, 32 | { 33 | name: "kubernetes/kubernetes", 34 | lang: "Go", 35 | desc: "Production-Grade Container Scheduling and Management.", 36 | }, 37 | { 38 | name: "docker/docker-ce", 39 | lang: "Go", 40 | desc: "Docker CE (Community Edition) is the open source container runtime.", 41 | }, 42 | { 43 | name: "nodejs/node", 44 | lang: "JavaScript", 45 | desc: "Node.js JavaScript runtime.", 46 | }, 47 | { 48 | name: "electron/electron", 49 | lang: "C++, JavaScript", 50 | desc: "Build cross-platform desktop apps with JavaScript, HTML, and CSS.", 51 | }, 52 | { 53 | name: "vuejs/vue", 54 | lang: "JavaScript", 55 | desc: "A progressive, incrementally-adoptable JavaScript framework for building UI on the web.", 56 | }, 57 | { 58 | name: "atom/atom", 59 | lang: "JavaScript", 60 | desc: "The hackable text editor built on Electron.", 61 | }, 62 | { name: "golang/go", lang: "Go", desc: "The Go programming language." }, 63 | { 64 | name: "facebook/jest", 65 | lang: "JavaScript", 66 | desc: "Delightful JavaScript testing.", 67 | }, 68 | { 69 | name: "reduxjs/redux", 70 | lang: "JavaScript", 71 | desc: "Predictable state container for JavaScript apps.", 72 | }, 73 | { 74 | name: "moby/moby", 75 | lang: "Go", 76 | desc: "Moby Project - a collaborative project for the container ecosystem to assemble container-based systems.", 77 | }, 78 | { 79 | name: "apple/swift", 80 | lang: "Swift", 81 | desc: "The Swift programming language.", 82 | }, 83 | { 84 | name: "openssl/openssl", 85 | lang: "C", 86 | desc: "Toolkit for the Transport Layer Security (TLS) and Secure Sockets Layer (SSL) protocols.", 87 | }, 88 | { 89 | name: "apache/spark", 90 | lang: "Scala", 91 | desc: "Unified analytics engine for big data and machine learning.", 92 | }, 93 | { 94 | name: "microsoft/TypeScript", 95 | lang: "TypeScript", 96 | desc: "Superset of JavaScript that compiles to clean JavaScript output.", 97 | }, 98 | { 99 | name: "spring-projects/spring-boot", 100 | lang: "Java", 101 | desc: "Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications.", 102 | }, 103 | { 104 | name: "rails/rails", 105 | lang: "Ruby", 106 | desc: "Ruby on Rails, the web-application framework.", 107 | }, 108 | { 109 | name: "django/django", 110 | lang: "Python", 111 | desc: "The web framework for perfectionists with deadlines.", 112 | }, 113 | { 114 | name: "flutter/flutter", 115 | lang: "Dart", 116 | desc: "Flutter is Google's UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase.", 117 | }, 118 | { 119 | name: "pandas-dev/pandas", 120 | lang: "Python", 121 | desc: "Flexible and powerful data analysis / manipulation library for Python.", 122 | }, 123 | { 124 | name: "hakimel/reveal.js", 125 | lang: "JavaScript", 126 | desc: "The HTML Presentation Framework.", 127 | }, 128 | { 129 | name: "yarnpkg/yarn", 130 | lang: "JavaScript", 131 | desc: "Fast, reliable, and secure dependency management.", 132 | }, 133 | { 134 | name: "mrdoob/three.js", 135 | lang: "JavaScript", 136 | desc: "JavaScript 3D library.", 137 | }, 138 | { 139 | name: "moment/moment", 140 | lang: "JavaScript", 141 | desc: "Parse, validate, manipulate, and display dates in JavaScript.", 142 | }, 143 | { 144 | name: "laravel/laravel", 145 | lang: "PHP", 146 | desc: "A PHP framework for web artisans.", 147 | }, 148 | { 149 | name: "expressjs/express", 150 | lang: "JavaScript", 151 | desc: "Fast, unopinionated, minimalist web framework for Node.js.", 152 | }, 153 | { 154 | name: "WordPress/WordPress", 155 | lang: "PHP", 156 | desc: "WordPress, open source software you can use to create a beautiful website, blog, or app.", 157 | }, 158 | { 159 | name: "rust-lang/rust", 160 | lang: "Rust", 161 | desc: "A language empowering everyone to build reliable and efficient software.", 162 | }, 163 | { 164 | name: "git/git", 165 | lang: "C", 166 | desc: "Git is a free and open source distributed version control system.", 167 | }, 168 | { 169 | name: "nvm-sh/nvm", 170 | lang: "Shell", 171 | desc: "Node Version Manager - POSIX-compliant bash script to manage multiple active Node.js versions.", 172 | }, 173 | { 174 | name: "bitcoin/bitcoin", 175 | lang: "C++", 176 | desc: "Bitcoin Core integration/staging tree.", 177 | }, 178 | { 179 | name: "home-assistant/core", 180 | lang: "Python", 181 | desc: "Open source home automation that puts local control and privacy first.", 182 | }, 183 | { 184 | name: "hashicorp/terraform", 185 | lang: "Go", 186 | desc: "Terraform enables you to safely and predictably create, change, and improve infrastructure.", 187 | }, 188 | { 189 | name: "jekyll/jekyll", 190 | lang: "Ruby", 191 | desc: "A simple, blog-aware static site generator.", 192 | }, 193 | { 194 | name: "freeCodeCamp/freeCodeCamp", 195 | lang: "JavaScript", 196 | desc: "The https://www.freecodecamp.org open source codebase and curriculum. Learn to code for free.", 197 | }, 198 | { 199 | name: "ansible/ansible", 200 | lang: "Python", 201 | desc: "Ansible is a radically simple IT automation platform.", 202 | }, 203 | { 204 | name: "elastic/elasticsearch", 205 | lang: "Java", 206 | desc: "Open Source, Distributed, RESTful Search Engine.", 207 | }, 208 | { name: "mongodb/mongo", lang: "C++", desc: "The MongoDB Database." }, 209 | { 210 | name: "grafana/grafana", 211 | lang: "Go, TypeScript", 212 | desc: "Open source platform for monitoring and observability.", 213 | }, 214 | { 215 | name: "prometheus/prometheus", 216 | lang: "Go", 217 | desc: "The Prometheus monitoring system and time series database.", 218 | }, 219 | { 220 | name: "netdata/netdata", 221 | lang: "C", 222 | desc: "Real-time performance monitoring, done right!", 223 | }, 224 | { 225 | name: "JetBrains/kotlin", 226 | lang: "Kotlin", 227 | desc: "The Kotlin Programming Language.", 228 | }, 229 | { 230 | name: "coreos/etcd", 231 | lang: "Go", 232 | desc: "Distributed reliable key-value store for the most critical data of a distributed system.", 233 | }, 234 | { 235 | name: "JuliaLang/julia", 236 | lang: "Julia", 237 | desc: "The Julia Language: A fresh approach to technical computing.", 238 | }, 239 | { 240 | name: "ionic-team/ionic-framework", 241 | lang: "TypeScript", 242 | desc: "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.", 243 | }, 244 | { 245 | name: "nestjs/nest", 246 | lang: "TypeScript", 247 | desc: "A progressive Node.js framework for building efficient and scalable server-side applications.", 248 | }, 249 | { 250 | name: "Tencent/wepy", 251 | lang: "JavaScript", 252 | desc: "A mini program framework to promote development efficiency.", 253 | }, 254 | { 255 | name: "vercel/next.js", 256 | lang: "JavaScript", 257 | desc: "The React Framework.", 258 | }, 259 | { 260 | name: "rollup/rollup", 261 | lang: "JavaScript, TypeScript", 262 | desc: "Next-generation ES module bundler.", 263 | }, 264 | { 265 | name: "webpack/webpack", 266 | lang: "JavaScript", 267 | desc: "A bundler for JavaScript and friends.", 268 | }, 269 | { 270 | name: "bazelbuild/bazel", 271 | lang: "Java", 272 | desc: "A fast, scalable, multi-language and extensible build system.", 273 | }, 274 | { 275 | name: "apache/cassandra", 276 | lang: "Java", 277 | desc: "The Apache Cassandra database.", 278 | }, 279 | { 280 | name: "redis/redis", 281 | lang: "C", 282 | desc: "Redis is an in-memory database that persists on disk. It also provides Lua scripting, replication, and more.", 283 | }, 284 | { 285 | name: "microsoft/terminal", 286 | lang: "C++", 287 | desc: "The new Windows Terminal and the original Windows console host, all in the same place!", 288 | }, 289 | { 290 | name: "fastai/fastai", 291 | lang: "Python", 292 | desc: "The fastai deep learning library, plus lessons and tutorials.", 293 | }, 294 | { 295 | name: "getsentry/sentry", 296 | lang: "Python", 297 | desc: "Application monitoring platform, helps developers see performance issues, fix errors, and optimize their code.", 298 | }, 299 | { 300 | name: "prettier/prettier", 301 | lang: "JavaScript", 302 | desc: "Prettier is an opinionated code formatter.", 303 | }, 304 | { 305 | name: "pytorch/pytorch", 306 | lang: "C++, Python", 307 | desc: "Tensors and Dynamic neural networks in Python with strong GPU acceleration.", 308 | }, 309 | { 310 | name: "SeleniumHQ/selenium", 311 | lang: "Java", 312 | desc: "A browser automation framework and ecosystem.", 313 | }, 314 | { 315 | name: "psf/requests", 316 | lang: "Python", 317 | desc: "Python HTTP Requests for Humans.", 318 | }, 319 | { 320 | name: "cockroachdb/cockroach", 321 | lang: "Go", 322 | desc: "CockroachDB - the open source, cloud-native distributed SQL database.", 323 | }, 324 | { 325 | name: "apollographql/apollo-client", 326 | lang: "TypeScript", 327 | desc: "A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.", 328 | }, 329 | { 330 | name: "huggingface/transformers", 331 | lang: "Python", 332 | desc: "Transformers: State-of-the-art Natural Language Processing for TensorFlow 2.0 and PyTorch.", 333 | }, 334 | { 335 | name: "google-research/bert", 336 | lang: "Python", 337 | desc: "TensorFlow code and pre-trained models for BERT.", 338 | }, 339 | { 340 | name: "openai/gpt-2", 341 | lang: "Python", 342 | desc: "Code for the paper \"Language Models are Unsupervised Multitask Learners\".", 343 | }, 344 | { 345 | name: "llvm/llvm-project", 346 | lang: "C++", 347 | desc: "The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.", 348 | }, 349 | { 350 | name: "dlang/dmd", 351 | lang: "D", 352 | desc: "The D Programming Language Compiler.", 353 | }, 354 | { 355 | name: "crystal-lang/crystal", 356 | lang: "Crystal", 357 | desc: "The Crystal Programming Language.", 358 | }, 359 | { 360 | name: "scikit-learn/scikit-learn", 361 | lang: "Python", 362 | desc: "Scikit-learn: Machine Learning in Python.", 363 | }, 364 | { 365 | name: "python/cpython", 366 | lang: "Python", 367 | desc: "The Python programming language.", 368 | }, 369 | { 370 | name: "numpy/numpy", 371 | lang: "Python", 372 | desc: "The fundamental package for scientific computing with Python.", 373 | }, 374 | { 375 | name: "openjdk/jdk", 376 | lang: "Java", 377 | desc: "The Java Development Kit.", 378 | }, 379 | { 380 | name: "tokio-rs/tokio", 381 | lang: "Rust", 382 | desc: "A runtime for writing reliable asynchronous applications with Rust. Provides I/O, networking, scheduling, timers, ...", 383 | }, 384 | { 385 | name: "jquery/jquery", 386 | lang: "JavaScript", 387 | desc: "jQuery JavaScript Library.", 388 | } 389 | ]; 390 | -------------------------------------------------------------------------------- /pages/games/[gameId]/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import { Answer, GameData, QuestionData } from "../../../types"; 3 | import { motion, useAnimation } from "framer-motion"; 4 | import { useRouter } from "next/router"; 5 | 6 | export default function Game() { 7 | const router = useRouter(); 8 | const { gameId } = router.query; 9 | const [game, setGame] = useState(null); 10 | 11 | const [currentQuestion, setCurrentQuestion] = useState( 12 | null 13 | ); 14 | const [_, setAnswer] = useState(""); // this is for re-rendering component 15 | const answerRef = useRef(""); // this is for checking answer synchronously 16 | const [username, setUsername] = useState(null); 17 | const [snippetIndex, setSnippetIndex] = useState(0); // 現在のスニペットのインデックス 18 | const [showModal, setShowModal] = useState(false); // モーダルの表示フラグ 19 | const [answerable, setAnswerable] = useState(false); // 回答可能かどうか 20 | const [secondsRemaining, setSecondsRemaining] = useState(60); // タイマーの残り時間 21 | const snipetControls = useAnimation(); 22 | const snipetDragControls = useAnimation(); 23 | const modalControls = useAnimation(); 24 | const gameRound = 10; // ゲームのラウンド数 25 | const dragParentRef = useRef(null); 26 | const dragAreaRef = useRef(null); 27 | 28 | useEffect(() => { 29 | if (!gameId) return; 30 | 31 | const existUsername = localStorage.getItem("username"); 32 | if (existUsername) { 33 | setUsername(existUsername); 34 | } else { 35 | setUsername(""); 36 | } 37 | 38 | fetch(`/api/game?gameId=${gameId}`).then(async (res) => { 39 | if (!res.ok) { 40 | alert("Failed to fetch game"); 41 | setGame(null); 42 | return; 43 | } 44 | 45 | if (res.status === 404) { 46 | setGame(null); 47 | return; 48 | } 49 | const game = await res.json(); 50 | setGame(game); 51 | fetchQuestion(); 52 | }); 53 | }, [gameId]); 54 | 55 | async function updateGame() { 56 | const response = await fetch(`/api/game?gameId=${gameId}`); 57 | if (!response.ok) { 58 | return; 59 | } 60 | const data: GameData = await response.json(); 61 | setGame(data); 62 | } 63 | 64 | async function fetchQuestion() { 65 | const response = await fetch(`/api/question?game_id=${gameId}`); 66 | const data: QuestionData = await response.json(); 67 | snipetControls.start({ scale: [0.1, 1] }); 68 | snipetDragControls.start({ x: 0, y: 0 }); 69 | setCurrentQuestion(data); 70 | setAnswerable(true); 71 | updateGame(); 72 | } 73 | 74 | useEffect(() => { 75 | if (currentQuestion) { 76 | setSecondsRemaining(60); 77 | const timerId = setInterval(() => { 78 | setSecondsRemaining((prevSeconds) => { 79 | if (prevSeconds <= 1) { 80 | clearInterval(timerId); 81 | checkAnswer(answerRef.current); 82 | return 60; 83 | } 84 | return prevSeconds - 1; 85 | }); 86 | }, 1000); 87 | 88 | return () => clearInterval(timerId); 89 | } 90 | }, [currentQuestion]); 91 | 92 | function nextSnippet(dir: number = 1) { 93 | return () => { 94 | if (!currentQuestion) return; 95 | 96 | const nextIndex = snippetIndex + dir; 97 | if (nextIndex < 0) { 98 | setSnippetIndex(currentQuestion.repository.snippets.length - 1); 99 | } else if (nextIndex < currentQuestion.repository.snippets.length) { 100 | setSnippetIndex(nextIndex); 101 | } else { 102 | setSnippetIndex(0); 103 | } 104 | snipetDragControls.start({ x: 0, y: 0 }); 105 | }; 106 | } 107 | 108 | if (!game) { 109 | return <>; 110 | } 111 | 112 | return ( 113 |
114 |

115 | What is this repository? {game.rounds.length}/{gameRound} 116 |

117 |
118 | 119 | {currentQuestion?.repository.star_num} 120 | 121 | {" stars, "} 122 | 123 | {currentQuestion?.repository.fork_num} 124 | 125 | {" forks"} 126 |
127 |
128 | {secondsRemaining < 30 && ( 129 |

130 | Hint:{" "} 131 | 132 | {currentQuestion?.repository.lang} 133 | 134 |

135 | )} 136 | {secondsRemaining < 15 && ( 137 |

138 | Hint:{" "} 139 | 143 |

144 | )} 145 |
146 |
147 |
148 |
149 |
156 | {secondsRemaining}s 157 |
158 |
159 |
160 | 169 |
181 | 188 | {currentQuestion?.repository.snippets[snippetIndex]} 189 | 190 |
191 |
192 |
193 |
194 | 200 | {"←"} 201 | 202 |
203 | {/* 次のSnippetを表示するためのコンポーネント */} 204 |
205 | 211 | {"→"} 212 | 213 |
214 | {/* 横並びに選択肢を表示する */} 215 |

216 | Choose the repository you guess 217 |

218 |
219 | {currentQuestion?.candidates.map((candidate) => ( 220 | 235 | ))} 236 |
237 | 244 |
245 | {game.rounds 246 | .filter((round) => round.userAnswer != null) 247 | .map((round, index) => { 248 | return ( 249 |
250 | {index + 1}: 251 | 256 | {round.isCorrect ? " Correct! " : " Wrong. "} 257 | 258 |
259 | Correct answer{" "} 260 | {/* */} 265 | [{round.repoName}] 266 |
267 | {(round.userAnswer || "").length > 0 268 | ? `Your answer is ${round.userAnswer}}` 269 | : "You selected nothing"} 270 |
271 | ); 272 | })} 273 |
274 | {showModal && ( 275 | 280 |

288 | {currentQuestion?.repository.name == answerRef.current 289 | ? "Correct!🎉" 290 | : "Oops!😢"} 291 |

292 |
293 | The repository is [ 294 | 299 | {currentQuestion?.repository.name}] 300 |

301 | {currentQuestion?.repository.desc} 302 |

303 |

304 | 305 | {currentQuestion?.repository.url} 306 | 307 |

308 |
309 |

310 | {answerRef.current 311 | ? `Your answer is ${answerRef.current}` 312 | : "You selected nothing"} 313 |

314 | 320 | {game.finished_at ? "Show Result" : "Next Round"} 321 | 322 |
323 | )} 324 |
325 | ); 326 | 327 | async function checkAnswer(userAnswer: string) { 328 | setAnswerable(false); 329 | const sendRes = await fetch("/api/send_answer", { 330 | method: "POST", 331 | body: JSON.stringify({ 332 | user_id: username, 333 | game_id: gameId, 334 | user_answer: userAnswer, 335 | time_remaining: secondsRemaining, 336 | }), 337 | headers: { 338 | "Content-Type": "application/json", 339 | }, 340 | }).then((res) => res.json()); 341 | setShowModal(true); 342 | 343 | const game = await fetch(`/api/game?gameId=${gameId}`).then((res) => 344 | res.json() 345 | ); 346 | setGame(game); 347 | 348 | // LocalStorageに回答履歴を保存 349 | const currentLog: Answer = { 350 | user_id: username || "", 351 | repo_image_url: currentQuestion?.repository.avatarURL || "", 352 | repo_url: currentQuestion?.repository.url || "", 353 | user_answer: userAnswer, 354 | time_remaining: secondsRemaining, 355 | correct_answer: currentQuestion?.repository.name || "", 356 | is_correct: sendRes.isCorrect, 357 | }; 358 | const existingLogs: Answer[] = JSON.parse( 359 | localStorage.getItem("answerLog") || "[]" 360 | ); 361 | localStorage.setItem( 362 | "answerLog", 363 | JSON.stringify([...existingLogs, currentLog]) 364 | ); 365 | } 366 | 367 | function goToNextQuestion() { 368 | if (game?.finished_at) { 369 | // スコアをLocalStorageに保存 370 | const existingLogs: Answer[] = JSON.parse( 371 | localStorage.getItem("answerLog") || "[]" 372 | ); 373 | const correctAnswers = existingLogs.filter( 374 | (answer) => answer.is_correct 375 | ).length; 376 | localStorage.setItem("score", correctAnswers.toString()); 377 | window.location.href = `/games/${gameId}/result`; 378 | } else { 379 | setShowModal(false); 380 | setAnswer(""); 381 | answerRef.current = ""; 382 | fetchQuestion(); 383 | } 384 | } 385 | } 386 | --------------------------------------------------------------------------------