aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/App.tsx72
-rw-r--r--src/components/FuncExpectedAnswer.tsx59
-rw-r--r--src/components/FuncMyAnswer.tsx32
-rw-r--r--src/components/QuizGroupSection.tsx19
-rw-r--r--src/components/QuizSection.tsx19
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;