aboutsummaryrefslogtreecommitdiffhomepage
path: root/services/app/templates
diff options
context:
space:
mode:
Diffstat (limited to 'services/app/templates')
-rw-r--r--services/app/templates/_page.html.twig23
-rw-r--r--services/app/templates/admin_answer_edit.html.twig44
-rw-r--r--services/app/templates/admin_answer_list.html.twig41
-rw-r--r--services/app/templates/admin_overview.html.twig13
-rw-r--r--services/app/templates/admin_quiz_edit.html.twig11
-rw-r--r--services/app/templates/admin_quiz_list.html.twig21
-rw-r--r--services/app/templates/admin_quiz_new.html.twig8
-rw-r--r--services/app/templates/admin_testcase_edit.html.twig22
-rw-r--r--services/app/templates/admin_testcase_list.html.twig18
-rw-r--r--services/app/templates/admin_testcase_new.html.twig12
-rw-r--r--services/app/templates/admin_user_edit.html.twig8
-rw-r--r--services/app/templates/admin_user_list.html.twig14
-rw-r--r--services/app/templates/answer_list.html.twig45
-rw-r--r--services/app/templates/answer_new.html.twig12
-rw-r--r--services/app/templates/answer_view.html.twig38
-rw-r--r--services/app/templates/form/_form.html.twig24
-rw-r--r--services/app/templates/form/_form_item.html.twig20
-rw-r--r--services/app/templates/form/_form_item_checkbox.html.twig21
-rw-r--r--services/app/templates/form/_form_item_textarea.html.twig20
-rw-r--r--services/app/templates/login.html.twig5
-rw-r--r--services/app/templates/quiz_list.html.twig9
-rw-r--r--services/app/templates/quiz_view.html.twig53
22 files changed, 482 insertions, 0 deletions
diff --git a/services/app/templates/_page.html.twig b/services/app/templates/_page.html.twig
new file mode 100644
index 0000000..d2072ea
--- /dev/null
+++ b/services/app/templates/_page.html.twig
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{{ page_title }} | {{ site_name }}</title>
+ <link rel="stylesheet" href="{{ base_path() }}/assets/index.css">
+ <script type="module" src="{{ base_path() }}/assets/index.js"></script>
+ </head>
+ <body>
+ <header class="container">
+ <nav class="navbar">
+ <a href="{{ url_for('quiz_list') }}" class="navbar-brand">{{ site_name }}</a>
+ </nav>
+ </header>
+ <main class="container">
+ <h1>{{ page_title }}</h1>
+ {% block content %}{% endblock %}
+ </main>
+ <footer class="container">
+ {{ site_name }}
+ </footer>
+ </body>
+</html>
diff --git a/services/app/templates/admin_answer_edit.html.twig b/services/app/templates/admin_answer_edit.html.twig
new file mode 100644
index 0000000..31d007c
--- /dev/null
+++ b/services/app/templates/admin_answer_edit.html.twig
@@ -0,0 +1,44 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h2>回答 #{{ answer.answer_number }}</h2>
+ <p>
+ {{ answer.author_name }} が {{ answer.submitted_at|date('Y-m-d H:i:s', 'Asia/Tokyo') }} に投稿
+ </p>
+ <h2>コード</h2>
+ <p>
+ {{ answer.code_size }} byte
+ </p>
+ <pre><code class="hljs language-php">{{ answer.code }}</code></pre>
+ <h2>実行結果</h2>
+ <div>
+ ステータス: {{ answer.execution_status.label() }}
+ <form action="{{ url_for('admin_answer_rerun_all_testcases_post', { qslug: quiz.slug, anum: answer.answer_number }) }}" method=POST>
+ <input type="submit" class="btn btn-warning" value="すべてのテストケースを再実行">
+ <input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
+ <input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
+ </form>
+ </div>
+ {% for ex in testcase_executions %}
+ <h3>テストケース {{ loop.index }}</h3>
+ <div>
+ ステータス: {{ ex.status.label() }}
+ </div>
+ <form action="{{ url_for('admin_answer_rerun_single_testcase_post', { qslug: quiz.slug, anum: answer.answer_number, txid: ex.testcase_execution_id }) }}" method=POST>
+ <input type="submit" class="btn btn-warning" value="このテストケースを再実行">
+ <input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
+ <input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
+ </form>
+ <h4>標準出力</h4>
+ <pre><code class="hljs language-plaintext">{{ ex.stdout }}</code></pre>
+ <h4>標準エラー出力</h4>
+ <pre><code class="hljs language-plaintext">{{ ex.stderr }}</code></pre>
+ {% endfor %}
+{% endblock %}
diff --git a/services/app/templates/admin_answer_list.html.twig b/services/app/templates/admin_answer_list.html.twig
new file mode 100644
index 0000000..68f0089
--- /dev/null
+++ b/services/app/templates/admin_answer_list.html.twig
@@ -0,0 +1,41 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です。<a href="{{ url_for('answer_list', { qslug: quiz.slug }) }}">通常の回答一覧はこちらを参照してください</a>
+ </p>
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h2>回答一覧</h2>
+ <form action="{{ url_for('admin_answer_rerun_all_answers_post', { qslug: quiz.slug }) }}" method=POST>
+ <input type="submit" class="btn btn-warning" value="すべての回答に対して全テストケースを再実行">
+ <input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
+ <input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
+ </form>
+ <table>
+ <thead>
+ <tr>
+ <th>ランク</th>
+ <th>ID</th>
+ <th>作者</th>
+ <th>サイズ</th>
+ <th>投稿日時</th>
+ <th>ステータス</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for answer in answers %}
+ <tr>
+ <td>{{ loop.index }}</td>
+ <td><a href="{{ url_for('admin_answer_edit', { qslug: quiz.slug, anum: answer.answer_number }) }}">#{{ answer.answer_number }}</a></td>
+ <td>{{ answer.author_name }}{% if answer.author_is_admin %} (staff){% endif %}</td>
+ <td>{{ answer.code_size }} byte</td>
+ <td>{{ answer.submitted_at|date('Y-m-d H:i:s', 'Asia/Tokyo') }}</td>
+ <td>{{ answer.execution_status.value }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+{% endblock %}
diff --git a/services/app/templates/admin_overview.html.twig b/services/app/templates/admin_overview.html.twig
new file mode 100644
index 0000000..2103616
--- /dev/null
+++ b/services/app/templates/admin_overview.html.twig
@@ -0,0 +1,13 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ <div>
+ <a href="{{ url_for('admin_user_list') }}">ユーザ一覧</a>
+ </div>
+ <div>
+ <a href="{{ url_for('admin_quiz_list') }}">問題一覧</a>
+ </div>
+{% endblock %}
diff --git a/services/app/templates/admin_quiz_edit.html.twig b/services/app/templates/admin_quiz_edit.html.twig
new file mode 100644
index 0000000..114de02
--- /dev/null
+++ b/services/app/templates/admin_quiz_edit.html.twig
@@ -0,0 +1,11 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です。<a href="{{ url_for('quiz_view', { qslug: quiz.slug }) }}">通常の問題はこちらを参照してください</a>
+ </p>
+ {{ include('form/_form.html.twig') }}
+ <p>
+ <a href="{{ url_for('admin_testcase_list', { qslug: quiz.slug }) }}">テストケースを追加・削除・編集する</a>
+ </p>
+{% endblock %}
diff --git a/services/app/templates/admin_quiz_list.html.twig b/services/app/templates/admin_quiz_list.html.twig
new file mode 100644
index 0000000..4db6961
--- /dev/null
+++ b/services/app/templates/admin_quiz_list.html.twig
@@ -0,0 +1,21 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です。<a href="{{ url_for('quiz_list', { qslug: quiz.slug }) }}">通常の問題一覧はこちらを参照してください</a>
+ </p>
+ <ul>
+ {% for quiz in quizzes %}
+ <li>
+ <a href="{{ url_for('admin_quiz_edit', { qslug: quiz.slug }) }}">問題 #{{ quiz.quiz_id }}: {{ quiz.title }}</a>
+ <ul>
+ <li><a href="{{ url_for('admin_testcase_list', { qslug: quiz.slug }) }}">テストケース一覧</a></li>
+ <li><a href="{{ url_for('admin_answer_list', { qslug: quiz.slug }) }}">回答一覧</a></li>
+ </ul>
+ </li>
+ {% endfor %}
+ </ul>
+ <p>
+ <a href="{{ url_for('admin_quiz_new') }}">問題を作成する</a>
+ </p>
+{% endblock %}
diff --git a/services/app/templates/admin_quiz_new.html.twig b/services/app/templates/admin_quiz_new.html.twig
new file mode 100644
index 0000000..9da6a14
--- /dev/null
+++ b/services/app/templates/admin_quiz_new.html.twig
@@ -0,0 +1,8 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ {{ include('form/_form.html.twig') }}
+{% endblock %}
diff --git a/services/app/templates/admin_testcase_edit.html.twig b/services/app/templates/admin_testcase_edit.html.twig
new file mode 100644
index 0000000..e86d61b
--- /dev/null
+++ b/services/app/templates/admin_testcase_edit.html.twig
@@ -0,0 +1,22 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ {{ include('form/_form.html.twig') }}
+ <p>
+ 既存のテストケースを編集すると、この問題に対してすでに提出されている回答に対してもテストが再度実行されます。
+ その実行が終わるまで回答のステータスは Pending 状態になり、ランキング等にも出現しなくなります。
+ </p>
+
+ <form action="{{ url_for('admin_testcase_delete_post', { qslug: quiz.slug, tid: testcase.testcase_id }) }}" method=POST>
+ <input type="submit" class="btn btn-danger" value="削除">
+ <input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
+ <input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
+ </form>
+ <p>
+ 既存のテストケースを削除すると、この問題に対してすでに提出されている回答のステータスは他のテストケースを基にして再計算されます。
+ 再計算が終わるまで回答のステータスは Pending 状態になり、ランキング等にも出現しなくなります。
+ </p>
+{% endblock %}
diff --git a/services/app/templates/admin_testcase_list.html.twig b/services/app/templates/admin_testcase_list.html.twig
new file mode 100644
index 0000000..a146406
--- /dev/null
+++ b/services/app/templates/admin_testcase_list.html.twig
@@ -0,0 +1,18 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h2>テストケース一覧</h2>
+ <ul>
+ {% for testcase in testcases %}
+ <li><a href="{{ url_for('admin_testcase_edit', { qslug: quiz.slug, tid: testcase.testcase_id }) }}">テストケース #{{ testcase.testcase_id }}</a></li>
+ {% endfor %}
+ </ul>
+ <a href="{{ url_for('admin_testcase_new', { qslug: quiz.slug }) }}">テストケースを追加する</a>
+{% endblock %}
diff --git a/services/app/templates/admin_testcase_new.html.twig b/services/app/templates/admin_testcase_new.html.twig
new file mode 100644
index 0000000..38243a9
--- /dev/null
+++ b/services/app/templates/admin_testcase_new.html.twig
@@ -0,0 +1,12 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ {{ include('form/_form.html.twig') }}
+ <p>
+ テストケースを追加すると、この問題に対してすでに提出されている回答に対してもテストが実行されます。
+ その実行が終わるまで回答のステータスは Pending 状態になり、ランキング等にも出現しなくなります。
+ </p>
+{% endblock %}
diff --git a/services/app/templates/admin_user_edit.html.twig b/services/app/templates/admin_user_edit.html.twig
new file mode 100644
index 0000000..9da6a14
--- /dev/null
+++ b/services/app/templates/admin_user_edit.html.twig
@@ -0,0 +1,8 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ {{ include('form/_form.html.twig') }}
+{% endblock %}
diff --git a/services/app/templates/admin_user_list.html.twig b/services/app/templates/admin_user_list.html.twig
new file mode 100644
index 0000000..5d477a9
--- /dev/null
+++ b/services/app/templates/admin_user_list.html.twig
@@ -0,0 +1,14 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <p>
+ このページは管理画面です
+ </p>
+ <ul>
+ {% for user in users %}
+ <li>
+ <a href="{{ url_for('admin_user_edit', { username: user.username }) }}">{{ user.username }}</a>{% if user.is_admin %} (管理者){% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/services/app/templates/answer_list.html.twig b/services/app/templates/answer_list.html.twig
new file mode 100644
index 0000000..410ab20
--- /dev/null
+++ b/services/app/templates/answer_list.html.twig
@@ -0,0 +1,45 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h2>回答一覧</h2>
+ {% if answers|length == 0 %}
+ <p>
+ まだ回答がありません
+ </p>
+ {% else %}
+ <table>
+ <thead>
+ <tr>
+ <th>ランク</th>
+ <th>ID</th>
+ <th>作者</th>
+ <th>サイズ</th>
+ <th>投稿日時</th>
+ <th>ステータス</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for answer in answers %}
+ <tr>
+ <td>
+ {% if is_ranking_hidden %}
+ ?
+ {% else %}
+ {{ loop.index }}
+ {% endif %}
+ </td>
+ <td><a href="{{ url_for('answer_view', { qslug: quiz.slug, anum: answer.answer_number }) }}">#{{ answer.answer_number }}</a></td>
+ <td>{{ answer.author_name }}{% if answer.author_is_admin %} (staff){% endif %}</td>
+ <td>{{ answer.code_size }} byte</td>
+ <td>{{ answer.submitted_at|date('Y-m-d H:i:s', 'Asia/Tokyo') }}</td>
+ <td>{{ answer.execution_status.label() }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/services/app/templates/answer_new.html.twig b/services/app/templates/answer_new.html.twig
new file mode 100644
index 0000000..9dbf96e
--- /dev/null
+++ b/services/app/templates/answer_new.html.twig
@@ -0,0 +1,12 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h3>実装例</h3>
+ <pre><code class="hljs language-php">{{ quiz.example_code }}</code></pre>
+ <h3>回答</h3>
+ {{ include('form/_form.html.twig') }}
+{% endblock %}
diff --git a/services/app/templates/answer_view.html.twig b/services/app/templates/answer_view.html.twig
new file mode 100644
index 0000000..a76e161
--- /dev/null
+++ b/services/app/templates/answer_view.html.twig
@@ -0,0 +1,38 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h2>回答 #{{ answer.answer_number }}</h2>
+ <p>
+ {{ answer.author_name }} が {{ answer.submitted_at|date('Y-m-d H:i:s', 'Asia/Tokyo') }} に投稿
+ </p>
+ <h2>コード</h2>
+ <p>
+ {{ answer.code_size }} byte
+ </p>
+ <pre><code class="hljs language-php">{{ answer.code }}</code></pre>
+ <h2>実行結果</h2>
+ <div>
+ ステータス: <span class="js-aggregated-execution-status" data-answer-id="{{ answer.answer_id }}">{{ answer.execution_status.label() }}</span>
+ {% if answer.execution_status.showLoadingIndicator() %}
+ <div class="js-aggregated-execution-status-loading-indicator spinner-border text-primary spinner-border-sm" role="status"><span class="visually-hidden">Loading...</span></div>
+ {% endif %}
+ </div>
+ {% for ex in testcase_executions %}
+ <h3>テストケース {{ loop.index }}</h3>
+ <div>
+ ステータス: <span class="js-testcase-execution-status" data-testcase-execution-id="{{ ex.testcase_execution_id }}">{{ ex.status.label() }}</span>
+ {% if ex.status.showLoadingIndicator() %}
+ <div class="js-testcase-execution-status-loading-indicator spinner-border text-primary spinner-border-sm" role="status" data-testcase-execution-id="{{ ex.testcase_execution_id }}"><span class="visually-hidden">Loading...</span></div>
+ {% endif %}
+ </div>
+ <h4>標準出力</h4>
+ <pre><code class="js-testcase-execution-stdout hljs language-plaintext" data-testcase-execution-id="{{ ex.testcase_execution_id }}">{{ ex.stdout }}</code></pre>
+ <h4>標準エラー出力</h4>
+ <pre><code class="js-testcase-execution-stderr hljs language-plaintext" data-testcase-execution-id="{{ ex.testcase_execution_id }}">{{ ex.stderr }}</code></pre>
+ {% endfor %}
+ <script type="module" src="{{ base_path() }}/assets/loading.js"></script>
+{% endblock %}
diff --git a/services/app/templates/form/_form.html.twig b/services/app/templates/form/_form.html.twig
new file mode 100644
index 0000000..b6a9d1d
--- /dev/null
+++ b/services/app/templates/form/_form.html.twig
@@ -0,0 +1,24 @@
+<form method=POST{% if form.action %} action="{{ form.action }}"{% endif %} novalidate>
+ {% if form.errors.general %}
+ <div class="alert alert-danger">
+ {{ form.errors.general }}
+ </div>
+ {% endif %}
+
+ {% for item in form.items %}
+ {% if item.type == 'checkbox' %}
+ {{ include('form/_form_item_checkbox.html.twig') }}
+ {% elseif item.type == 'textarea' %}
+ {{ include('form/_form_item_textarea.html.twig') }}
+ {% else %}
+ {{ include('form/_form_item.html.twig') }}
+ {% endif %}
+ {% endfor %}
+
+ <div class="mb-3">
+ <input type="submit" class="btn btn-primary" value="{{ form.submit_label }}">
+ </div>
+
+ <input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
+ <input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
+</form>
diff --git a/services/app/templates/form/_form_item.html.twig b/services/app/templates/form/_form_item.html.twig
new file mode 100644
index 0000000..6e6a36b
--- /dev/null
+++ b/services/app/templates/form/_form_item.html.twig
@@ -0,0 +1,20 @@
+{% set value = form.state[item.name] %}
+{% set error = form.errors[item.name] %}
+{% set classes = 'form-control' %}
+{% if error %}
+ {% set classes = classes ~ ' is-invalid' %}
+{% endif %}
+{% set required = item.isRequired ? ' required' : '' %}
+{% set disabled = item.isDisabled ? ' disabled' : '' %}
+{% set extra = item.extra ? item.extra : '' %}
+<div class="mb-3">
+ {% if item.label %}
+ <label for="form-{{ item.name }}" class="form-label">{{ item.label }}</label>
+ {% endif %}
+
+ <input type="{{ item.type }}" class="{{ classes }}" id="form-{{ item.name }}" name="{{ item.name }}"{% if value %} value="{{ value }}"{% endif %}{{ required }}{{ disabled }} {{ extra }}>
+
+ {% if error %}
+ <div class="invalid-feedback">{{ error }}</div>
+ {% endif %}
+</div>
diff --git a/services/app/templates/form/_form_item_checkbox.html.twig b/services/app/templates/form/_form_item_checkbox.html.twig
new file mode 100644
index 0000000..e51cc0c
--- /dev/null
+++ b/services/app/templates/form/_form_item_checkbox.html.twig
@@ -0,0 +1,21 @@
+{% set value = form.state[item.name] %}
+{% set error = form.errors[item.name] %}
+{% set classes = 'form-check-input' %}
+{% if error %}
+ {% set classes = classes ~ ' is-invalid' %}
+{% endif %}
+{% set required = item.isRequired ? ' required' : '' %}
+{% set disabled = item.isDisabled ? ' disabled' : '' %}
+{% set extra = item.extra ? item.extra : '' %}
+{% set extra = extra ~ (value == 'on' ? ' checked' : '') %}
+<div class="mb-3 form-check">
+ <input type="checkbox" class="{{ classes }}" id="form-{{ item.name }}" name="{{ item.name }}"{% if value %} value="{{ value }}"{% endif %}{{ required }}{{ disabled }} {{ extra }}>
+
+ {% if item.label %}
+ <label for="form-{{ item.name }}" class="form-check-label">{{ item.label }}</label>
+ {% endif %}
+
+ {% if error %}
+ <div class="invalid-feedback">{{ error }}</div>
+ {% endif %}
+</div>
diff --git a/services/app/templates/form/_form_item_textarea.html.twig b/services/app/templates/form/_form_item_textarea.html.twig
new file mode 100644
index 0000000..bf5cc8c
--- /dev/null
+++ b/services/app/templates/form/_form_item_textarea.html.twig
@@ -0,0 +1,20 @@
+{% set value = form.state[item.name] %}
+{% set error = form.errors[item.name] %}
+{% set classes = 'form-control' %}
+{% if error %}
+ {% set classes = classes ~ ' is-invalid' %}
+{% endif %}
+{% set required = item.isRequired ? ' required' : '' %}
+{% set disabled = item.isDisabled ? ' disabled' : '' %}
+{% set extra = item.extra ? item.extra : '' %}
+<div class="mb-3">
+ {% if item.label %}
+ <label for="form-{{ item.name }}" class="form-label">{{ item.label }}</label>
+ {% endif %}
+
+ <textarea class="{{ classes }}" id="form-{{ item.name }}" name="{{ item.name }}"{{ required }}{{ disabled }} {{ extra }}>{{ value }}</textarea>
+
+ {% if error %}
+ <div class="invalid-feedback">{{ error }}</div>
+ {% endif %}
+</div>
diff --git a/services/app/templates/login.html.twig b/services/app/templates/login.html.twig
new file mode 100644
index 0000000..e21b32f
--- /dev/null
+++ b/services/app/templates/login.html.twig
@@ -0,0 +1,5 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ {{ include('form/_form.html.twig') }}
+{% endblock %}
diff --git a/services/app/templates/quiz_list.html.twig b/services/app/templates/quiz_list.html.twig
new file mode 100644
index 0000000..2aa4f98
--- /dev/null
+++ b/services/app/templates/quiz_list.html.twig
@@ -0,0 +1,9 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <ul>
+ {% for quiz in quizzes %}
+ <li><a href="{{ url_for('quiz_view', { qslug: quiz.slug }) }}">問題 #{{ quiz.quiz_id }}: {{ quiz.title }}</a></li>
+ {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/services/app/templates/quiz_view.html.twig b/services/app/templates/quiz_view.html.twig
new file mode 100644
index 0000000..87c83ce
--- /dev/null
+++ b/services/app/templates/quiz_view.html.twig
@@ -0,0 +1,53 @@
+{% extends '_page.html.twig' %}
+
+{% block content %}
+ <h2>{{ quiz.title }}</h2>
+ <p>
+ {{ quiz.description }}
+ </p>
+ <h3>実装例</h3>
+ <pre><code class="hljs language-php">{{ quiz.example_code }}</code></pre>
+ {% if is_open %}
+ <p>
+ <a href="{{ url_for('answer_new', { qslug: quiz.slug }) }}">回答する</a>
+ </p>
+ {% endif %}
+ <h2>ランキング</h2>
+ {% if is_ranking_hidden %}
+ 回答が締め切られるまで、ランキングは表示されません
+ {% elseif ranking|length == 0 %}
+ <p>
+ まだ正解した回答がありません
+ </p>
+ {% else %}
+ <table>
+ <thead>
+ <tr>
+ <th>ランク</th>
+ <th>ID</th>
+ <th>作者</th>
+ <th>サイズ</th>
+ <th>投稿日時</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for answer in ranking %}
+ <tr>
+ <td>{{ loop.index }}</td>
+ <td><a href="{{ url_for('answer_view', { qslug: quiz.slug, anum: answer.answer_number }) }}">#{{ answer.answer_number }}</a></td>
+ <td>{{ answer.author_name }}{% if answer.author_is_admin %} (staff){% endif %}</td>
+ <td>{{ answer.code_size }} byte</td>
+ <td>{{ answer.submitted_at|date('Y-m-d H:i:s', 'Asia/Tokyo') }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <div>
+ <canvas id="chart" data-quiz-id="{{ quiz.quiz_id }}"></canvas>
+ <script type="module" src="{{ base_path() }}/assets/chart.js"></script>
+ </div>
+ {% endif %}
+ <p>
+ <a href="{{ url_for('answer_list', { qslug: quiz.slug }) }}">すべての回答を見る</a>
+ </p>
+{% endblock %}