fix: 🐛 debug timer, debug suppression quiz et debug shuffle reponses quiz
Build & Deploy / build-deploy (push) Failing after 1m5s
Build & Deploy / build-deploy (push) Failing after 1m5s
This commit is contained in:
+2
-2
@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { version = "0.8.0" }
|
leptos = { version = "0.8.0" }
|
||||||
leptos_router = { version = "0.8.0" }
|
leptos_router = { version = "0.8.0" }
|
||||||
axum = { version = "0.8.0", optional = true }
|
axum = { version = "0.8.9", optional = true }
|
||||||
console_error_panic_hook = { version = "0.1", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
leptos_axum = { version = "0.8.0", optional = true }
|
leptos_axum = { version = "0.8.0", optional = true }
|
||||||
leptos_meta = { version = "0.8.0" }
|
leptos_meta = { version = "0.8.0" }
|
||||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
anyhow = { version = "1", optional = true }
|
anyhow = { version = "1", optional = true }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono"], optional = true }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono"], optional = true }
|
||||||
axum-extra = { version = "0.12.5", features = ["cookie", "cookie-signed"], optional = true }
|
axum-extra = { version = "0.12.6", features = ["cookie", "cookie-signed"], optional = true }
|
||||||
time = { version = "0.3", optional = true }
|
time = { version = "0.3", optional = true }
|
||||||
web-sys = { version = "0.3", features = ["Window", "HtmlInputElement"], optional = true }
|
web-sys = { version = "0.3", features = ["Window", "HtmlInputElement"], optional = true }
|
||||||
dotenvy = { version = "0.15", optional = true }
|
dotenvy = { version = "0.15", optional = true }
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
use sqlx::{SqlitePool, sqlite::{SqlitePoolOptions, SqliteConnectOptions}};
|
||||||
|
use std::str::FromStr;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub async fn init_db(database_url: &str) -> Result<SqlitePool> {
|
pub async fn init_db(database_url: &str) -> Result<SqlitePool> {
|
||||||
|
let options = SqliteConnectOptions::from_str(database_url)?
|
||||||
|
.foreign_keys(true);
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.connect(database_url)
|
.connect_with(options)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Tables de base
|
// Tables de base
|
||||||
@@ -62,6 +66,14 @@ pub async fn init_db(database_url: &str) -> Result<SqlitePool> {
|
|||||||
used BOOLEAN NOT NULL DEFAULT 0,
|
used BOOLEAN NOT NULL DEFAULT 0,
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS quiz_sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
quiz_id INTEGER NOT NULL,
|
||||||
|
first_name TEXT NOT NULL,
|
||||||
|
last_name TEXT NOT NULL,
|
||||||
|
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(quiz_id, first_name, last_name)
|
||||||
|
);
|
||||||
INSERT OR IGNORE INTO config (key, value) VALUES ('session_password', 'changeme');
|
INSERT OR IGNORE INTO config (key, value) VALUES ('session_password', 'changeme');
|
||||||
INSERT OR IGNORE INTO config (key, value) VALUES ('admin_password', 'admin');
|
INSERT OR IGNORE INTO config (key, value) VALUES ('admin_password', 'admin');
|
||||||
"#,
|
"#,
|
||||||
|
|||||||
@@ -92,13 +92,13 @@ pub fn StudentDetailPage() -> impl IntoView {
|
|||||||
<p class="corr-question">{d.question_text.clone()}</p>
|
<p class="corr-question">{d.question_text.clone()}</p>
|
||||||
<p class="corr-chosen">
|
<p class="corr-chosen">
|
||||||
{if d.is_correct { "✅ " } else { "❌ " }}
|
{if d.is_correct { "✅ " } else { "❌ " }}
|
||||||
{format!("{}) {}", d.chosen_label, d.chosen_text)}
|
{d.chosen_text.clone()}
|
||||||
</p>
|
</p>
|
||||||
{if !d.is_correct {
|
{if !d.is_correct {
|
||||||
view! {
|
view! {
|
||||||
<p class="corr-right">
|
<p class="corr-right">
|
||||||
{format!("✔ Bonne réponse : {}) {}",
|
{format!("✔ Bonne réponse : {}",
|
||||||
d.correct_label, d.correct_text)}
|
d.correct_text)}
|
||||||
</p>
|
</p>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+75
-54
@@ -1,10 +1,35 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_router::hooks::{use_navigate, use_params_map, use_query_map};
|
use leptos_router::hooks::{use_navigate, use_params_map, use_query_map};
|
||||||
use crate::server::student::{get_quiz_with_questions, submit_quiz};
|
use crate::server::student::{get_quiz_with_questions, submit_quiz, start_or_resume_quiz};
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Timer(time_left: RwSignal<Option<i64>>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
{move || time_left.get().map(|secs| {
|
||||||
|
let mins = secs / 60;
|
||||||
|
let s = secs % 60;
|
||||||
|
let color = if secs <= 30 { "#f87171" }
|
||||||
|
else if secs <= 60 { "#fb923c" }
|
||||||
|
else { "var(--color-enuxia-muted)" };
|
||||||
|
view! {
|
||||||
|
<span style=format!(
|
||||||
|
"font-size:13px;font-weight:700;color:{};
|
||||||
|
padding:4px 12px;border-radius:20px;
|
||||||
|
background:rgba(0,0,0,0.2);
|
||||||
|
border:1px solid currentColor;
|
||||||
|
font-variant-numeric:tabular-nums;",
|
||||||
|
color
|
||||||
|
)>
|
||||||
|
{format!("⏱ {:02}:{:02}", mins, s)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn QuizPage() -> impl IntoView {
|
pub fn QuizPage() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
@@ -35,41 +60,60 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
{move || quiz_data.get().map(|result| {
|
{move || quiz_data.get().map(|result| {
|
||||||
match result {
|
match result {
|
||||||
Ok(Some((quiz, questions))) => {
|
Ok(Some((quiz, questions))) => {
|
||||||
let total = questions.len();
|
let total = questions.len();
|
||||||
let questions = StoredValue::new(questions);
|
let questions = StoredValue::new(questions);
|
||||||
let quiz_title = quiz.title.clone();
|
let quiz_title = quiz.title.clone();
|
||||||
|
|
||||||
// Minuteur
|
// Minuteur
|
||||||
if let Some(secs) = quiz.time_limit_seconds {
|
if let Some(secs) = quiz.time_limit_seconds {
|
||||||
if time_left.get().is_none() {
|
if time_left.get_untracked().is_none() {
|
||||||
time_left.set(Some(secs));
|
time_left.set(Some(secs));
|
||||||
leptos::task::spawn_local(async move {
|
#[cfg(feature = "hydrate")]
|
||||||
loop {
|
{
|
||||||
#[cfg(feature = "hydrate")]
|
let qid = quiz_id();
|
||||||
TimeoutFuture::new(1000).await;
|
let fn_ = first_name();
|
||||||
#[cfg(not(feature = "hydrate"))]
|
let ln = last_name();
|
||||||
break;
|
leptos::task::spawn_local(async move {
|
||||||
|
// Récupère le temps restant côté serveur
|
||||||
if submitted.get() { break; }
|
if let Ok(remaining) = start_or_resume_quiz(qid, fn_.clone(), ln.clone()).await {
|
||||||
let t = time_left.get().unwrap_or(0);
|
if remaining <= 0 {
|
||||||
if t <= 1 {
|
time_left.set(Some(0));
|
||||||
time_left.set(Some(0));
|
if !submitted.get() {
|
||||||
if !submitted.get() {
|
submitted.set(true);
|
||||||
submitted.set(true);
|
let ans = answers.get();
|
||||||
let fn_ = first_name();
|
let nav = navigate.get_value();
|
||||||
let ln = last_name();
|
if let Ok(sid) = submit_quiz(qid, fn_, ln, ans).await {
|
||||||
let qid = quiz_id();
|
nav(&format!("/result/{}", sid), Default::default());
|
||||||
let ans = answers.get();
|
}
|
||||||
let nav = navigate.get_value();
|
|
||||||
if let Ok(sid) = submit_quiz(qid, fn_, ln, ans).await {
|
|
||||||
nav(&format!("/result/{}", sid), Default::default());
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
time_left.set(Some(remaining));
|
||||||
}
|
}
|
||||||
time_left.set(Some(t - 1));
|
// Boucle de décompte
|
||||||
}
|
loop {
|
||||||
});
|
TimeoutFuture::new(1000).await;
|
||||||
|
if submitted.get() { break; }
|
||||||
|
let t = time_left.get().unwrap_or(0);
|
||||||
|
if t <= 1 {
|
||||||
|
time_left.set(Some(0));
|
||||||
|
if !submitted.get() {
|
||||||
|
submitted.set(true);
|
||||||
|
let fn_ = first_name();
|
||||||
|
let ln = last_name();
|
||||||
|
let qid = quiz_id();
|
||||||
|
let ans = answers.get();
|
||||||
|
let nav = navigate.get_value();
|
||||||
|
if let Ok(sid) = submit_quiz(qid, fn_, ln, ans).await {
|
||||||
|
nav(&format!("/result/{}", sid), Default::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
time_left.set(Some(t - 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,25 +124,7 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
<div class="quiz-topbar">
|
<div class="quiz-topbar">
|
||||||
<span class="quiz-name">{quiz_title.clone()}</span>
|
<span class="quiz-name">{quiz_title.clone()}</span>
|
||||||
<div style="display:flex;align-items:center;gap:12px;">
|
<div style="display:flex;align-items:center;gap:12px;">
|
||||||
{move || time_left.get().map(|secs| {
|
<Timer time_left=time_left/>
|
||||||
let mins = secs / 60;
|
|
||||||
let s = secs % 60;
|
|
||||||
let color = if secs <= 30 { "#f87171" }
|
|
||||||
else if secs <= 60 { "#fb923c" }
|
|
||||||
else { "var(--color-enuxia-muted)" };
|
|
||||||
view! {
|
|
||||||
<span style=format!(
|
|
||||||
"font-size:13px;font-weight:700;color:{};
|
|
||||||
padding:4px 12px;border-radius:20px;
|
|
||||||
background:rgba(0,0,0,0.2);
|
|
||||||
border:1px solid currentColor;
|
|
||||||
font-variant-numeric:tabular-nums;",
|
|
||||||
color
|
|
||||||
)>
|
|
||||||
{format!("⏱ {:02}:{:02}", mins, s)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
<span class="question-badge">
|
<span class="question-badge">
|
||||||
{move || format!("{} / {}", current.get() + 1, total)}
|
{move || format!("{} / {}", current.get() + 1, total)}
|
||||||
</span>
|
</span>
|
||||||
@@ -130,7 +156,6 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
let ans_list = qwa.answers.clone();
|
let ans_list = qwa.answers.clone();
|
||||||
let q_id = question.id;
|
let q_id = question.id;
|
||||||
|
|
||||||
// Réponse actuellement sélectionnée pour cette question
|
|
||||||
let selected = move || {
|
let selected = move || {
|
||||||
answers.get().iter()
|
answers.get().iter()
|
||||||
.find(|(qid, _)| *qid == q_id)
|
.find(|(qid, _)| *qid == q_id)
|
||||||
@@ -145,9 +170,9 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
<p class="question-text">{question.text.clone()}</p>
|
<p class="question-text">{question.text.clone()}</p>
|
||||||
|
|
||||||
<div class="answers-list">
|
<div class="answers-list">
|
||||||
{ans_list.into_iter().map(|answer| {
|
{ans_list.into_iter().enumerate().map(|(i, answer)| {
|
||||||
let aid = answer.id;
|
let aid = answer.id;
|
||||||
let lbl = answer.label.clone();
|
let lbl = String::from((b'A' + i as u8) as char);
|
||||||
let txt = answer.text.clone();
|
let txt = answer.text.clone();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@@ -182,7 +207,6 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
margin-top:8px;
|
margin-top:8px;
|
||||||
gap:12px;
|
gap:12px;
|
||||||
">
|
">
|
||||||
// Bouton précédent
|
|
||||||
{move || (current.get() > 0).then(|| view! {
|
{move || (current.get() > 0).then(|| view! {
|
||||||
<button
|
<button
|
||||||
class="btn-ghost"
|
class="btn-ghost"
|
||||||
@@ -198,13 +222,11 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
<span/>
|
<span/>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
// Bouton suivant ou soumettre
|
|
||||||
{move || {
|
{move || {
|
||||||
let idx = current.get();
|
let idx = current.get();
|
||||||
let has_answer = selected().is_some();
|
let has_answer = selected().is_some();
|
||||||
|
|
||||||
if idx == total - 1 {
|
if idx == total - 1 {
|
||||||
// Dernière question — bouton soumettre
|
|
||||||
view! {
|
view! {
|
||||||
<button
|
<button
|
||||||
style=move || format!(
|
style=move || format!(
|
||||||
@@ -231,7 +253,6 @@ pub fn QuizPage() -> impl IntoView {
|
|||||||
</button>
|
</button>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
} else {
|
} else {
|
||||||
// Question suivante
|
|
||||||
view! {
|
view! {
|
||||||
<button
|
<button
|
||||||
style=move || format!(
|
style=move || format!(
|
||||||
|
|||||||
@@ -119,13 +119,13 @@ pub fn ResultPage() -> impl IntoView {
|
|||||||
<p class="corr-question">{d.question_text.clone()}</p>
|
<p class="corr-question">{d.question_text.clone()}</p>
|
||||||
<p class="corr-chosen">
|
<p class="corr-chosen">
|
||||||
{if d.is_correct { "✅ " } else { "❌ " }}
|
{if d.is_correct { "✅ " } else { "❌ " }}
|
||||||
{format!("{}) {}", d.chosen_label, d.chosen_text)}
|
{d.chosen_text.clone()}
|
||||||
</p>
|
</p>
|
||||||
{if !d.is_correct {
|
{if !d.is_correct {
|
||||||
view! {
|
view! {
|
||||||
<p class="corr-right">
|
<p class="corr-right">
|
||||||
{format!("✔ Bonne réponse : {}) {}",
|
{format!("✔ Bonne réponse : {}",
|
||||||
d.correct_label, d.correct_text)}
|
d.correct_text)}
|
||||||
</p>
|
</p>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+17
-3
@@ -87,10 +87,24 @@ pub async fn delete_quiz(id: i64) -> Result<(), ServerFnError> {
|
|||||||
|
|
||||||
let Extension(pool): Extension<SqlitePool> = extract().await?;
|
let Extension(pool): Extension<SqlitePool> = extract().await?;
|
||||||
|
|
||||||
|
// Supprime dans l'ordre pour respecter les contraintes
|
||||||
|
sqlx::query!("DELETE FROM student_answers WHERE submission_id IN (SELECT id FROM submissions WHERE quiz_id = ?)", id)
|
||||||
|
.execute(&pool).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query!("DELETE FROM submissions WHERE quiz_id = ?", id)
|
||||||
|
.execute(&pool).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query!("DELETE FROM answers WHERE question_id IN (SELECT id FROM questions WHERE quiz_id = ?)", id)
|
||||||
|
.execute(&pool).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query!("DELETE FROM questions WHERE quiz_id = ?", id)
|
||||||
|
.execute(&pool).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query!("DELETE FROM resets WHERE quiz_id = ?", id)
|
||||||
|
.execute(&pool).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
sqlx::query!("DELETE FROM quizzes WHERE id = ?", id)
|
sqlx::query!("DELETE FROM quizzes WHERE id = ?", id)
|
||||||
.execute(&pool)
|
.execute(&pool).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
.await
|
|
||||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,58 @@ pub async fn get_quiz_with_questions(quiz_id: i64) -> Result<Option<(Quiz, Vec<Q
|
|||||||
Ok(Some((quiz, result)))
|
Ok(Some((quiz, result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn start_or_resume_quiz(
|
||||||
|
quiz_id: i64,
|
||||||
|
first_name: String,
|
||||||
|
last_name: String,
|
||||||
|
) -> Result<i64, ServerFnError> {
|
||||||
|
use axum::Extension;
|
||||||
|
use leptos_axum::extract;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
let Extension(pool): Extension<SqlitePool> = extract().await?;
|
||||||
|
|
||||||
|
let fn_lower = first_name.trim().to_lowercase();
|
||||||
|
let ln_lower = last_name.trim().to_lowercase();
|
||||||
|
|
||||||
|
let row = sqlx::query!(
|
||||||
|
"SELECT time_limit_seconds FROM quizzes WHERE id = ?",
|
||||||
|
quiz_id
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
let time_limit = row.time_limit_seconds.unwrap_or(0);
|
||||||
|
|
||||||
|
let session = sqlx::query!(
|
||||||
|
r#"SELECT started_at as "started_at: chrono::DateTime<chrono::Utc>"
|
||||||
|
FROM quiz_sessions
|
||||||
|
WHERE quiz_id = ? AND first_name = ? AND last_name = ?"#,
|
||||||
|
quiz_id, fn_lower, ln_lower
|
||||||
|
)
|
||||||
|
.fetch_optional(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
if let Some(row) = session {
|
||||||
|
let elapsed = (Utc::now() - row.started_at).num_seconds();
|
||||||
|
Ok((time_limit - elapsed).max(0))
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO quiz_sessions (quiz_id, first_name, last_name) VALUES (?, ?, ?)",
|
||||||
|
quiz_id, fn_lower, ln_lower
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(time_limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn check_already_done(
|
pub async fn check_already_done(
|
||||||
quiz_id: i64,
|
quiz_id: i64,
|
||||||
@@ -257,6 +309,15 @@ pub async fn submit_quiz(
|
|||||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nettoyage de la session quiz
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM quiz_sessions WHERE quiz_id = ? AND first_name = ? AND last_name = ?",
|
||||||
|
quiz_id, fn_lower, ln_lower
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
Ok(submission_id)
|
Ok(submission_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1065,4 +1065,55 @@ button {
|
|||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Text overflow fixes ──────────────────────────────────────────────────── */
|
||||||
|
.question-text,
|
||||||
|
.answer-option,
|
||||||
|
.corr-question,
|
||||||
|
.corr-chosen,
|
||||||
|
.corr-right,
|
||||||
|
.quiz-item .quiz-title,
|
||||||
|
.quiz-item .quiz-desc,
|
||||||
|
.q-badge,
|
||||||
|
.data-table td,
|
||||||
|
.answer-editor-row input[type="text"] {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Réponses dans l'éditeur admin */
|
||||||
|
.answer-option {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-option .opt-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Questions dans l'éditeur */
|
||||||
|
.question-card input[type="text"],
|
||||||
|
.question-card textarea {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table admin */
|
||||||
|
.data-table td {
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.data-table td {
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user