From b58a3dc6d91a0573141e72f250a77909e924a9db Mon Sep 17 00:00:00 2001 From: naoki1510 <15890587+naoki1510@users.noreply.github.com> Date: Fri, 8 Mar 2024 23:24:38 +0900 Subject: [PATCH 1/4] change server ip --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ed49719..d320a37 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,4 +17,4 @@ jobs: username: ${{secrets.SSH_USERNAME}} port: ${{secrets.SSH_PORT}} script: | - ssh 192.168.0.20 "cd ~/github/pam-quiz && make production_deploy" \ No newline at end of file + ssh 192.168.0.70 "cd ~/github/pam-quiz && make production_deploy" \ No newline at end of file From 77420b5f395d8dfcb261b0d18b3515125819178e Mon Sep 17 00:00:00 2001 From: naoki1510 <15890587+naoki1510@users.noreply.github.com> Date: Fri, 8 Mar 2024 23:27:10 +0900 Subject: [PATCH 2/4] add vscode workspace --- pam-quiz.code-workspace | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pam-quiz.code-workspace diff --git a/pam-quiz.code-workspace b/pam-quiz.code-workspace new file mode 100644 index 0000000..3a1406e --- /dev/null +++ b/pam-quiz.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "api" + }, + { + "path": "client" + } + ], + "settings": {} +} \ No newline at end of file From e487101e3d89c232d60160c78e393c502f50ba53 Mon Sep 17 00:00:00 2001 From: naoki1510 <15890587+naoki1510@users.noreply.github.com> Date: Sat, 9 Mar 2024 13:15:35 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E8=A7=A3=E7=AD=94=E6=AE=B5=E9=9A=8E?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/controllers/questions_controller.rb | 15 ++++--- api/app/models/question.rb | 19 +++++++++ api/app/views/answers/_answer.json.jbuilder | 4 +- api/app/views/choices/_choice.json.jbuilder | 2 +- .../views/questions/_question.json.jbuilder | 2 +- api/config/routes.rb | 1 + ...09022235_add_open_answer_at_to_question.rb | 5 +++ api/db/schema.rb | 3 +- client/src/api/answer.ts | 2 + client/src/api/questions.ts | 13 ++++++ .../src/components/answers/CreateAnswer.tsx | 3 +- .../src/components/questions/QuestionCard.tsx | 42 ++++++++++++++----- 12 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 api/db/migrate/20240309022235_add_open_answer_at_to_question.rb diff --git a/api/app/controllers/questions_controller.rb b/api/app/controllers/questions_controller.rb index a0abc90..f8abd17 100644 --- a/api/app/controllers/questions_controller.rb +++ b/api/app/controllers/questions_controller.rb @@ -1,6 +1,6 @@ class QuestionsController < ApplicationController - before_action :set_question, only: %i[ show update destroy start end reset ] - before_action :set_show_correct, only: %i[ index show start end reset ] + before_action :set_question, only: %i[ show update destroy start end reset open_answer ] + before_action :set_show_correct, only: %i[ index show start end reset open_answer ] # GET /questions def index @@ -64,6 +64,11 @@ def reset render :show end + def open_answer + @question.update!(open_answer_at: Time.current) + render :show + end + private # Use callbacks to share common setup or constraints between actions. def set_question @@ -81,9 +86,7 @@ def set_show_correct end def reset_question - @question.update!(started_at: nil, ended_at: nil) - @question.choices.each do |choice| - choice.answers.destroy_all - end + @question.update!(started_at: nil, ended_at: nil, open_answer_at: nil) + Answer.joins(:question).where(questions: {id: @question.id}).destroy_all end end diff --git a/api/app/models/question.rb b/api/app/models/question.rb index 1e876d6..add21a7 100644 --- a/api/app/models/question.rb +++ b/api/app/models/question.rb @@ -12,8 +12,27 @@ def correct_answers answers.where(choice_id: correct_choices.ids) end + def active? + started_at.present? && ended_at.present? && started_at <= Time.current && Time.current < ended_at + end + def finished? ended_at.present? && ended_at <= Time.current end + def until_end + (ended_at - Time.current) if (started_at.present? && ended_at.present? && started_at < Time.current && Time.current < ended_at) + end + + def answer_opened? + open_answer_at.present? && open_answer_at <= Time.current + end + + def status + return :answer_opened if answer_opened? + return :finished if finished? + return :active if active? + :waiting + end + end \ No newline at end of file diff --git a/api/app/views/answers/_answer.json.jbuilder b/api/app/views/answers/_answer.json.jbuilder index d4fb9b0..d793a0c 100644 --- a/api/app/views/answers/_answer.json.jbuilder +++ b/api/app/views/answers/_answer.json.jbuilder @@ -1 +1,3 @@ -json.extract! answer, :id, :user_id, :choice_id \ No newline at end of file +json.extract! answer, :id, :user_id, :choice_id +json.username answer.user.name +json.title answer.choice.title \ No newline at end of file diff --git a/api/app/views/choices/_choice.json.jbuilder b/api/app/views/choices/_choice.json.jbuilder index 5caa0bf..c6ab64d 100644 --- a/api/app/views/choices/_choice.json.jbuilder +++ b/api/app/views/choices/_choice.json.jbuilder @@ -1,5 +1,5 @@ json.extract! choice, :id, :title, :description, :image, :question_id -json.is_correct choice.is_correct if @is_show_correct +json.is_correct choice.is_correct if @is_show_correct || choice.question.answer_opened? json.answers do json.array! choice.answers, partial: 'answers/answer', as: :answer end \ No newline at end of file diff --git a/api/app/views/questions/_question.json.jbuilder b/api/app/views/questions/_question.json.jbuilder index 5fd5f9c..6a55ea6 100644 --- a/api/app/views/questions/_question.json.jbuilder +++ b/api/app/views/questions/_question.json.jbuilder @@ -1,4 +1,4 @@ -json.extract! question, :id, :title, :image, :question_type, :point +json.extract! question, :id, :title, :image, :question_type, :point, :status, :until_end json.is_finished question.finished? json.choices do json.array! question.choices.ordered, partial: "choices/choice", as: :choice diff --git a/api/config/routes.rb b/api/config/routes.rb index 951cf11..75a8595 100644 --- a/api/config/routes.rb +++ b/api/config/routes.rb @@ -15,4 +15,5 @@ get "questions/:id/start" => "questions#start" get "questions/:id/end" => "questions#end" get "questions/:id/reset" => "questions#reset" + get "questions/:id/open_answer" => "questions#open_answer" end diff --git a/api/db/migrate/20240309022235_add_open_answer_at_to_question.rb b/api/db/migrate/20240309022235_add_open_answer_at_to_question.rb new file mode 100644 index 0000000..7a7d710 --- /dev/null +++ b/api/db/migrate/20240309022235_add_open_answer_at_to_question.rb @@ -0,0 +1,5 @@ +class AddOpenAnswerAtToQuestion < ActiveRecord::Migration[7.1] + def change + add_column :questions, :open_answer_at, :timestamp + end +end diff --git a/api/db/schema.rb b/api/db/schema.rb index e5f5f5b..7603d42 100644 --- a/api/db/schema.rb +++ b/api/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_03_102618) do +ActiveRecord::Schema[7.1].define(version: 2024_03_09_022235) do create_table "answers", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "choice_id", null: false @@ -42,6 +42,7 @@ t.timestamp "ended_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.timestamp "open_answer_at" end create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| diff --git a/client/src/api/answer.ts b/client/src/api/answer.ts index 207b070..adfd34c 100644 --- a/client/src/api/answer.ts +++ b/client/src/api/answer.ts @@ -5,6 +5,8 @@ export type Answer = { id?: number; user_id: number; choice_id: number; + username?: string; + choice?: string; }; export type CreateAnswerParams = { diff --git a/client/src/api/questions.ts b/client/src/api/questions.ts index cb188c4..4217c75 100644 --- a/client/src/api/questions.ts +++ b/client/src/api/questions.ts @@ -13,6 +13,7 @@ export type Question = { choices: Choice[]; until_end?: number; is_finished: boolean; + status?: "active" | "finished" | "answer_opened" | "waiting"; }; export const createEmptyQuestion = (): Question => { @@ -124,6 +125,18 @@ export const resetQuestion = ( }).then((res) => res.json() as Promise); }; +export const openAnswer = ( + id: string | number, + params?: URLSearchParams, + controller?: AbortController +) => { + return fetch(`${host}/questions/${id}/open_answer?${params}`, { + method: "GET", + headers: getHeaders, + signal: controller?.signal, + }).then((res) => res.json() as Promise); +}; + export const useQuestions = (params?: URLSearchParams) => { const [questions, setQuestions] = useState(); const [loading, setLoading] = useState(false); diff --git a/client/src/components/answers/CreateAnswer.tsx b/client/src/components/answers/CreateAnswer.tsx index a11fbdc..4e41bdd 100644 --- a/client/src/components/answers/CreateAnswer.tsx +++ b/client/src/components/answers/CreateAnswer.tsx @@ -41,7 +41,7 @@ export default memo(function CreateAnswer() { new URLSearchParams({ active: "true" }) ); const { questions: lastQuestions, fetchQuestions: fetchLastQuestions } = - useQuestions(new URLSearchParams({ last: "true", show_correct: "true" })); + useQuestions(new URLSearchParams({ last: "true" })); const { user } = useUser(userId || ""); useEffect(() => { @@ -60,6 +60,7 @@ export default memo(function CreateAnswer() { }); }) ); + setSelectedChoices([]); fetchLastQuestions(); } }); diff --git a/client/src/components/questions/QuestionCard.tsx b/client/src/components/questions/QuestionCard.tsx index 1548c25..b52892c 100644 --- a/client/src/components/questions/QuestionCard.tsx +++ b/client/src/components/questions/QuestionCard.tsx @@ -16,6 +16,7 @@ import { Question, deleteQuestion, endQuestion, + openAnswer, resetQuestion, startQuestion, } from "api/questions"; @@ -24,8 +25,10 @@ import { memo, useCallback } from "react"; import { IoCheckmark, IoClose, + IoDocumentText, IoPencil, IoPlayCircle, + IoRefresh, IoRemove, IoStopCircle, IoTrash, @@ -81,6 +84,14 @@ export default memo(function QuestionCard(props: ShowQuestionProps) { ).then(setQuestion); }, [question.id]); + const handleOpenAnswer = useCallback(() => { + if (question.id !== undefined) + openAnswer( + question.id, + new URLSearchParams({ show_correct: "true" }) + ).then(setQuestion); + }, [question.id]); + return ( {choice.is_correct ? ( - ) : isSelected ? ( + ) : isSelected && question.status === "answer_opened" ? ( ) : ( )} - {choice.description}{" "} - {choice.is_correct - ? "(正解)" - : isSelected - ? "(あなたの答え)" - : ""}{" "} - {choice.answers.length}人 + {choice.description} {choice.is_correct && "(正解)"}{" "} + {isSelected && "(あなたの答え)"} {choice.answers.length}人 ); })} @@ -137,7 +143,7 @@ export default memo(function QuestionCard(props: ShowQuestionProps) { {showActions && ( - {question.until_end ? ( + {question.status === "active" ? ( <> - あと{Math.round(question.until_end)}秒 + あと{Math.round(question.until_end ?? 0)}秒 + ) : question.status === "finished" ? ( + + ) : question.status === "answer_opened" ? ( + ) : (