Finish remaining question types

This commit is contained in:
Charlotte Van Petegem 2025-05-20 18:28:20 +02:00
parent 0d54f756d8
commit 5bd1e821f7
Signed by: chvp
SSH key fingerprint: SHA256:s9rb8jBVfdahqWHuBAcHCBP1wmj4eYQXZfqgz4H3E9E
20 changed files with 431 additions and 8 deletions

View file

@ -3,3 +3,38 @@
display: flex;
flex-direction: row;
}
.acrostic-row {
display: flex;
flex-direction: row;
}
.acrostic-offset,.acrostic-number {
display: inline-block;
width: 2rem;
height: 2rem;
border: 2px solid transparent;
}
.acrostic-input {
padding: 0;
width: 2rem;
height: 2rem;
text-transform: uppercase;
text-align: center;
}
.acrostic-column {
background-color: lightgrey;
}
.connections-grid {
display: grid;
grid-template-columns: 25% 25% 25% 25%;
}
.connections-element {
border: 1px solid black;
margin: 0.5em;
padding: 1em;
}

View file

@ -24,4 +24,16 @@ class QuestionsController < ApplicationController
def handle_politicians_answer
@answer.update!(data: params[:order])
end
def handle_acrostic_answer
@answer.update!(data: params[:data])
end
def handle_connections_answer
@answer.update!(data: params[:order])
end
def handle_lyrics_answer
@answer.update!(data: params[:data])
end
end

View file

@ -4,6 +4,7 @@ class SectionsController < ApplicationController
end
def show
@sections = Section.all
@section = Section.find(params[:id])
@questions = @section.questions.includes(:answer)
end

View file

@ -0,0 +1,39 @@
import { submitValueDebounced } from './submit_value.js'
export default function initAcrosticQuestions() {
document.querySelectorAll('[data-behaviour="question_acrostic"]').forEach((acrostic) => {
const submitUrl = acrostic.dataset.submitUrl
const _submit = submitValueDebounced()
const allInputs = Array.from(acrostic.querySelectorAll(".acrostic-input"))
function submit() {
const data = allInputs.map((input) => input.value)
_submit(submitUrl, {data})
}
allInputs.forEach((input) => {
const index = Number.parseInt(input.dataset.index)
const previous = acrostic.querySelector(`.acrostic-input[data-index="${index - 1}"]`)
const next = acrostic.querySelector(`.acrostic-input[data-index="${index + 1}"]`)
input.addEventListener('input', (event) => {
if (event.data) {
input.value = event.data[0]
}
submit()
if (event.data) {
if (next) {
next.focus()
}
} else {
if (previous) {
previous.focus()
}
}
})
input.addEventListener('focus', () => {
input.select()
})
})
})
}

View file

@ -2,9 +2,19 @@
import initSimpleQuestions from './simple_input.js'
import initImageQuestions from './image_input.js'
import initPoliticianQuestions from './politicians_answer.js'
import initAcrosticQuestions from './acrostic_answer.js'
import initConnectionsQuestions from './connections_answer.js'
import initLyricsQuestions from './lyrics_answer.js'
import { Sortable, Swap } from 'sortablejs'
Sortable.mount(new Swap())
addEventListener("DOMContentLoaded", () => {
initSimpleQuestions()
initImageQuestions()
initPoliticianQuestions()
initAcrosticQuestions()
initConnectionsQuestions()
initLyricsQuestions()
})

View file

@ -0,0 +1,19 @@
import { Sortable } from 'sortablejs'
import { submitValue } from './submit_value.js'
export default function initConnectionsQuestions() {
document.querySelectorAll('[data-behaviour="connections_answer').forEach((list) => {
const submitUrl = list.dataset.submitUrl
Sortable.create(list, {
swap: true,
onUpdate: (event) => {
const newOrder = Array.from(event.to.children).map((el) => Number.parseInt(el.dataset.id))
submitValue(submitUrl, { order: newOrder })
}
})
})
}

View file

@ -0,0 +1,21 @@
import { submitValueDebounced } from './submit_value.js'
export default function initLyricsQuestions() {
document.querySelectorAll('[data-behaviour="lyrics_answer"]').forEach((lyrics) => {
const submitUrl = lyrics.dataset.submitUrl
const _submit = submitValueDebounced()
const allInputs = Array.from(lyrics.querySelectorAll(".lyrics-input"))
function submit() {
const data = allInputs.map((input) => input.value)
_submit(submitUrl, {data})
}
allInputs.forEach((input) => {
const index = Number.parseInt(input.dataset.index)
input.addEventListener('input', () => submit())
input.addEventListener('change', () => submit())
})
})
}

View file

@ -1,11 +1,8 @@
import { Sortable, Swap } from 'sortablejs'
import { Sortable } from 'sortablejs'
import { submitValue } from './submit_value.js'
export default function initPoliticianQuestions() {
Sortable.mount(new Swap())
document.querySelectorAll('[data-behaviour="politicians_answer').forEach((list) => {
const submitUrl = list.dataset.submitUrl

View file

@ -21,8 +21,8 @@
# section_id (section_id => sections.id)
#
class Question < ApplicationRecord
enum :answer_kind, { simple: 0, image: 1, politicians: 2 }, prefix: true
enum :question_kind, { simple: 0, video: 1 }, prefix: true
enum :answer_kind, { simple: 0, image: 1, politicians: 2, acrostic: 3, connections: 4, lyrics: 5 }, prefix: true
enum :question_kind, { simple: 0, video: 1, acrostic: 3 }, prefix: true
belongs_to :section
has_one :answer

View file

@ -0,0 +1,22 @@
<% max_position = question.data.map { |c| c["position"] }.max %>
<% index = -1 %>
<div class="acrostic-puzzle"
data-behaviour="question_acrostic"
data-submit-url="<%= answer_question_url(question.id) %>"
>
<% question.data.each.with_index do |constraints, i| %>
<div class="acrostic-row">
<span class="acrostic-number"><%= i + 1 %></span>
<% (max_position - constraints["position"]).times do %>
<span class="acrostic-offset"></span>
<% end %>
<% (constraints["position"] - 1).times do %>
<%= render partial: "questions/acrostic_input", locals: {on_column: false, index: (index += 1), answer_data: question.answer&.data} %>
<% end %>
<%= render partial: "questions/acrostic_input", locals: {on_column: true, index: (index += 1), answer_data: question.answer&.data} %>
<% (constraints["length"] - constraints["position"]).times do %>
<%= render partial: "questions/acrostic_input", locals: {on_column: false, index: (index += 1), answer_data: question.answer&.data} %>
<% end %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,6 @@
<%= tag.input class: {"acrostic-input": true, "acrostic-column": on_column},
data: {index: index},
value: answer_data&.then { |it| it[index] },
type: "text",
autocomplete: "off"
%>

View file

@ -0,0 +1,3 @@
<% question.text.lines.each.with_index do |q, i| %>
<p><strong><%= i + 1 %>.</strong> <%= q %></p>
<% end %>

View file

@ -0,0 +1,11 @@
<div
class="connections-grid"
data-behaviour="connections_answer"
data-submit-url="<%= answer_question_url(question.id) %>"
>
<% answers = question.data.each.with_index.to_a %>
<% answers = reorder_with_indices(answers, question.answer.data) if question.answer.present? %>
<% answers.each do |a, i| %>
<span class="connections-element" data-id="<%= i %>"><%= a %></span>
<% end %>
</div>

View file

@ -0,0 +1,15 @@
<% index = -1 %>
<div data-behaviour="lyrics_answer"
data-submit-url="<%= answer_question_url(question.id) %>"
>
<% question.data.lines.each do |l| %>
<% parts = l.split "{}", -1 %>
<p>
<%= render partial: "questions/lyrics_given_part", locals: {text: parts[0]} %>
<% parts[1..].each do |text| %>
<%= render partial: "questions/lyrics_input", locals: {index: (index += 1), answer_data: question.answer&.data} %>
<%= render partial: "questions/lyrics_given_part", locals: {text: text} %>
<% end %>
</p>
<% end %>
</div>

View file

@ -0,0 +1,3 @@
<% if text.present? %>
<span><%= text %></span>
<% end %>

View file

@ -0,0 +1,6 @@
<%= tag.input class: "lyrics-input",
data: {index: index},
value: answer_data&.then { |it| it[index] },
type: "text",
autocomplete: "off"
%>

View file

@ -5,8 +5,8 @@
<% end %>
</div>
<div data-behaviour="politicians_answer"
data-submit-url="<%= answer_question_url(question.id) %>"
>
data-submit-url="<%= answer_question_url(question.id) %>"
>
<% answers = question.data["right"].each.with_index.to_a %>
<% answers = reorder_with_indices(answers, question.answer.data) if question.answer.present? %>
<% answers.each do |a, i| %>

View file

@ -1,3 +1,8 @@
<%= link_to sections_url do %>Home<% end %>
<% @sections.each do |section| %>
<%= link_to section.title, section_url(section) %>
<% end %>
<h4><%= @section.title %></h4>
<p><%= @section.description %></p>