diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/App.tsx | 72 | ||||
| -rw-r--r-- | src/components/FuncExpectedAnswer.tsx | 59 | ||||
| -rw-r--r-- | src/components/FuncMyAnswer.tsx | 32 | ||||
| -rw-r--r-- | src/components/QuizGroupSection.tsx | 19 | ||||
| -rw-r--r-- | src/components/QuizSection.tsx | 19 |
5 files changed, 201 insertions, 0 deletions
diff --git a/src/components/App.tsx b/src/components/App.tsx new file mode 100644 index 0000000..aa2a386 --- /dev/null +++ b/src/components/App.tsx @@ -0,0 +1,72 @@ +import type { QuizGroup } from "../quiz"; +import QuizGroupSection from "./QuizGroupSection"; + +type Props = { + quizGroups: QuizGroup[]; +}; + +function App({ quizGroups }: Props) { + return ( + <table id="layout"> + <tr> + <td id="header" colSpan={2} className="marquee"> + <span>PHPerKaigi 2025 デジタルサーカス株式会社トークン問題</span> + </td> + </tr> + <tr> + <td id="sidebar"> + <h2>メニュー</h2> + <ul> + <li> + <a href="">ホーム</a> + </li> + <li> + <a + href="https://github.com/nsfisis/PHPerKaigi2024-tokens" + target="_blank" + > + トークン問題2024 + </a> + </li> + <li> + <a + href="https://github.com/nsfisis/PHPerKaigi2023-tokens" + target="_blank" + > + トークン問題2023 + </a> + </li> + <li> + <a + href="https://github.com/nsfisis/PHPerKaigi2022-tokens" + target="_blank" + > + トークン問題2022 + </a> + </li> + <li className="hidden">ここにトークンはないよ</li> + </ul> + </td> + <td id="content"> + <main> + <p> + PHPerKaigi 2025 の PHPer チャレンジ企画において、 + <a href="https://www.dgcircus.com/">デジタルサーカス株式会社</a> + から出題するトークン問題です (作問{" "} + <a href="https://x.com/nsfisis">@nsfisis</a>)。 + </p> + <p> + それぞれの問題に、PHP の標準関数がひとつ設定されています。 + 好きな引数を渡すと実行されます。その実行結果を見て、何の関数かを当ててください。 + </p> + {quizGroups.map((group) => ( + <QuizGroupSection key={group.label} quizGroup={group} /> + ))} + </main> + </td> + </tr> + </table> + ); +} + +export default App; diff --git a/src/components/FuncExpectedAnswer.tsx b/src/components/FuncExpectedAnswer.tsx new file mode 100644 index 0000000..f412ba5 --- /dev/null +++ b/src/components/FuncExpectedAnswer.tsx @@ -0,0 +1,59 @@ +import type { Quiz } from "../quiz"; +import { execPHP } from "../exec_php"; +import React, { useState, useEffect } from "react"; +import { useDebounce } from "use-debounce"; + +type Props = { + quiz: Quiz; +}; + +function FuncExpectedAnswer({ quiz }: Props) { + const [argument, setArgument] = useState<string>("123"); + const [debouncedArgument] = useDebounce(argument, 1000); + const [result, setResult] = useState<string>(""); + const [loading, setLoading] = useState<boolean>(true); + + const handleArgumentChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setArgument(e.target.value); + }; + + useEffect(() => { + if (debouncedArgument === "") { + setResult("<empty>"); + return; + } + + setLoading(true); + setResult(""); + + const code = ` + function f($x) { + return ${quiz.func}($x); + } + try { + var_dump(f(${debouncedArgument})); + } catch (\\Throwable $e) { + echo $e->getMessage(), PHP_EOL; + } + `; + + execPHP(code).then((result) => { + const output = result.stdout + result.stderr; + setResult(output.replaceAll(quiz.func, "<answer is masked>")); + setLoading(false); + }); + }, [debouncedArgument, quiz.func]); + + return ( + <div> + <code> + {`f(`} + <input type="text" value={argument} onChange={handleArgumentChange} /> + {`)`} + </code> + は <code>{loading ? "running..." : result}</code> を返す。 + </div> + ); +} + +export default FuncExpectedAnswer; diff --git a/src/components/FuncMyAnswer.tsx b/src/components/FuncMyAnswer.tsx new file mode 100644 index 0000000..63cd427 --- /dev/null +++ b/src/components/FuncMyAnswer.tsx @@ -0,0 +1,32 @@ +import type { Quiz } from "../quiz"; +import React, { useState } from "react"; +import { useDebounce } from "use-debounce"; + +type Props = { + quiz: Quiz; +}; + +const INITIAL_ANSWER = "your_answer"; + +function FuncMyAnswer({ quiz }: Props) { + const [answer, setAnswer] = useState<string>(INITIAL_ANSWER); + const [debouncedAnswer] = useDebounce(answer, 500); + const hasAnyAnswer = debouncedAnswer !== INITIAL_ANSWER; + const isCorrectAnswer = debouncedAnswer === quiz.func; + + const handleAnswerChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setAnswer(e.target.value); + }; + + return ( + <div> + この関数は? + <input type="text" value={answer} onChange={handleAnswerChange} /> + <p> + {hasAnyAnswer && (isCorrectAnswer ? `正解!${quiz.message}` : "不正解")} + </p> + </div> + ); +} + +export default FuncMyAnswer; diff --git a/src/components/QuizGroupSection.tsx b/src/components/QuizGroupSection.tsx new file mode 100644 index 0000000..b2bbee7 --- /dev/null +++ b/src/components/QuizGroupSection.tsx @@ -0,0 +1,19 @@ +import type { QuizGroup } from "../quiz"; +import QuizSection from "./QuizSection"; + +type Props = { + quizGroup: QuizGroup; +}; + +function QuizGroupSection({ quizGroup }: Props) { + return ( + <section> + <h2>{quizGroup.label}</h2> + {quizGroup.quizzes.map((quiz) => ( + <QuizSection key={quiz.label} quiz={quiz} /> + ))} + </section> + ); +} + +export default QuizGroupSection; diff --git a/src/components/QuizSection.tsx b/src/components/QuizSection.tsx new file mode 100644 index 0000000..27dc384 --- /dev/null +++ b/src/components/QuizSection.tsx @@ -0,0 +1,19 @@ +import type { Quiz } from "../quiz"; +import FuncExpectedAnswer from "./FuncExpectedAnswer"; +import FuncMyAnswer from "./FuncMyAnswer"; + +type Props = { + quiz: Quiz; +}; + +function QuizSection({ quiz }: Props) { + return ( + <section> + <h3>{quiz.label}</h3> + <FuncExpectedAnswer quiz={quiz} /> + <FuncMyAnswer quiz={quiz} /> + </section> + ); +} + +export default QuizSection; |
