SurveyVista/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml
2026-03-07 02:37:33 +01:00

853 lines
42 KiB
Text

@model ResponseQuestionnaireViewModel
@{
ViewData["Title"] = "DisplayQuestionnaire";
Layout = "~/Views/Shared/_QuestionnaireResponse.cshtml";
}
<style>
@@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
:root {
--bg: #0f1923;
--bg-card: #1a2332;
--bg-elevated: #1f2b3d;
--bg-input: #16202e;
--bg-hover: #243044;
--text-primary: #e8edf2;
--text-secondary: #9ba8b9;
--text-muted: #5e6e82;
--text-faint: #3d4e63;
--teal: #33b3ae;
--teal-soft: rgba(51, 179, 174, 0.1);
--teal-medium: rgba(51, 179, 174, 0.2);
--teal-glow: rgba(51, 179, 174, 0.12);
--teal-dark: #2a9490;
--amber: #f59e0b;
--amber-soft: rgba(245, 158, 11, 0.08);
--amber-border: rgba(245, 158, 11, 0.35);
--purple: #7c3aed;
--purple-soft: rgba(124, 58, 237, 0.1);
--red: #ef4444;
--red-soft: rgba(239, 68, 68, 0.08);
--green: #10b981;
--green-soft: rgba(16, 185, 129, 0.1);
--green-border: rgba(16, 185, 129, 0.35);
--border: rgba(255, 255, 255, 0.06);
--border-focus: rgba(51, 179, 174, 0.4);
--shadow-xs: 0 1px 3px rgba(0,0,0,0.2);
--shadow-sm: 0 2px 8px rgba(0,0,0,0.25);
--shadow-md: 0 4px 16px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.35);
--shadow-teal: 0 4px 20px rgba(51,179,174,0.2);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-spring: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@@keyframes slideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.08); } }
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
}
/* ===== PAGE SHELL ===== */
.survey-page { padding: 40px 20px 100px; min-height: 100vh; }
.survey-shell {
max-width: 960px;
margin: 0 auto;
animation: slideUp 0.5s ease both;
}
/* ===== HEADER CARD ===== */
.survey-header-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 32px 36px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.survey-title {
font-size: 26px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.4px;
margin-bottom: 8px;
}
.survey-title-dot {
display: inline-block; width: 8px; height: 8px;
background: var(--teal); border-radius: 50%;
margin-left: 6px; vertical-align: super;
animation: pulse 2s ease infinite;
}
.survey-desc { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
/* ===== USER INFO ===== */
.user-info-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 24px 36px;
margin-bottom: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
box-shadow: var(--shadow-xs);
}
.field-label {
display: block; font-size: 12px; font-weight: 600;
color: var(--text-muted); text-transform: uppercase;
letter-spacing: 0.4px; margin-bottom: 6px;
}
.field-input {
width: 100%; background: var(--bg-input);
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
padding: 11px 14px; font-size: 14px; font-family: inherit;
color: var(--text-primary); outline: none; transition: var(--transition);
}
.field-input::placeholder { color: var(--text-faint); }
.field-input:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--teal-glow); background: var(--bg-elevated); }
/* ===== BUILDER LAYOUT (stepper + content) ===== */
.survey-body {
display: flex;
gap: 20px;
align-items: flex-start;
}
/* Stepper sidebar */
.stepper-sidebar {
width: 220px;
min-width: 220px;
position: sticky;
top: 24px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 18px 14px;
box-shadow: var(--shadow-xs);
max-height: calc(100vh - 48px);
overflow-y: auto;
}
.stepper-title {
font-size: 11px; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 1px;
margin-bottom: 12px; padding: 0 6px;
}
.step-dot {
display: flex; align-items: center; gap: 10px;
padding: 10px 10px; margin-bottom: 4px;
border-radius: var(--radius-sm); cursor: default;
transition: var(--transition);
}
.step-dot .dot-num {
width: 26px; height: 26px; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; flex-shrink: 0;
background: var(--bg-elevated); color: var(--text-muted);
border: 1.5px solid var(--border); transition: var(--transition);
}
.step-dot .dot-label {
font-size: 12px; color: var(--text-muted); font-weight: 500;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.step-dot.active { background: var(--teal-soft); }
.step-dot.active .dot-num { background: var(--teal); color: #fff; border-color: var(--teal); }
.step-dot.active .dot-label { color: var(--teal); font-weight: 600; }
.stepper-sidebar::-webkit-scrollbar { width: 4px; }
.stepper-sidebar::-webkit-scrollbar-track { background: transparent; }
.stepper-sidebar::-webkit-scrollbar-thumb { background: var(--text-faint); border-radius: 4px; }
/* Content area */
.content-area { flex: 1; min-width: 0; }
/* ===== QUESTION STEP ===== */
.q-step {
display: none;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 32px 36px;
box-shadow: var(--shadow-xs);
animation: fadeIn 0.3s ease;
transition: var(--transition);
}
.q-step:hover { box-shadow: var(--shadow-sm); }
.q-step.active { display: block; }
.q-text {
font-size: 17px; font-weight: 600; color: var(--text-primary);
margin-bottom: 24px; letter-spacing: -0.2px; line-height: 1.5;
}
.q-num-badge {
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 8px;
background: var(--teal); color: #fff;
font-size: 12px; font-weight: 700;
margin-right: 10px; vertical-align: middle;
}
/* Shared input */
.q-input {
width: 100%; background: var(--bg-input); border: 1.5px solid var(--border);
border-radius: var(--radius-sm); padding: 12px 16px; font-size: 14px;
font-family: inherit; color: var(--text-primary); outline: none;
transition: var(--transition); resize: vertical;
}
.q-input::placeholder { color: var(--text-faint); }
.q-input:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--teal-glow); background: var(--bg-elevated); }
/* ===== OPTION LIST (Checkbox / Radio) ===== */
.option-list { display: flex; flex-direction: column; gap: 8px; }
.option-item {
display: flex; align-items: center; gap: 12px;
padding: 14px 16px; background: var(--bg-elevated);
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
cursor: pointer; transition: var(--transition); position: relative;
}
.option-item:hover { border-color: rgba(255,255,255,0.12); background: var(--bg-hover); }
.option-item input[type="checkbox"],
.option-item input[type="radio"] {
accent-color: var(--teal); width: 18px; height: 18px;
flex-shrink: 0; cursor: pointer;
}
.option-item label {
font-size: 14px; color: var(--text-primary); cursor: pointer;
flex: 1; font-weight: 400; line-height: 1.4; margin: 0;
}
/* Other options look identical to regular options — no special styling */
/* Other text box */
.other-text-container { display: none; margin-top: 8px; padding-left: 30px; animation: slideUp 0.2s ease both; }
.other-text-container.show { display: block; }
.other-text-input {
width: 100%; background: var(--bg-input); border: 1.5px solid var(--border);
border-radius: var(--radius-sm); padding: 10px 14px; font-size: 13px;
font-family: inherit; color: var(--text-primary); outline: none;
resize: vertical; min-height: 70px; transition: var(--transition);
}
.other-text-input:focus { border-color: var(--border-focus); box-shadow: 0 0 0 2px var(--teal-glow); }
/* ===== RATING ===== */
.rating-row { display: flex; flex-wrap: wrap; gap: 10px; }
.rating-card {
display: flex; flex-direction: column; align-items: center; gap: 6px;
padding: 14px 18px; background: var(--bg-elevated);
border: 1.5px solid var(--border); border-radius: var(--radius-md);
cursor: pointer; transition: var(--transition-spring);
}
.rating-card:hover { border-color: rgba(251,191,36,0.4); transform: translateY(-2px); }
.rating-card .r-label { font-size: 12px; color: var(--text-secondary); font-weight: 500; }
.rating-card .r-star { font-size: 28px; color: var(--text-faint); transition: var(--transition); cursor: pointer; }
.rating-card:hover .r-star { color: #fbbf24; }
.rating-card .r-star.selected { color: #f59e0b; filter: drop-shadow(0 2px 8px rgba(245,158,11,0.4)); }
.rating-card.rating-hover .r-star { color: #fbbf24; }
/* ===== LIKERT ===== */
.likert-row { display: flex; flex-wrap: wrap; gap: 8px; }
.likert-chip {
flex: 1; min-width: 120px; text-align: center;
padding: 14px 12px; background: var(--bg-elevated);
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
cursor: pointer; font-size: 13px; font-weight: 500;
color: var(--text-secondary); transition: var(--transition);
}
.likert-chip:hover { border-color: var(--teal-medium); color: var(--text-primary); }
.likert-chip input[type="radio"] { display: none; }
.likert-chip.selected { background: var(--teal-soft); border-color: var(--teal); color: var(--teal); }
.likert-chip.is-other-option { /* same as regular */ }
.likert-chip.is-other-option.selected { background: var(--teal-soft); border-color: var(--teal); color: var(--teal); }
/* ===== MATRIX ===== */
.matrix-wrap { overflow-x: auto; border-radius: var(--radius-md); border: 1px solid var(--border); }
.matrix-wrap table { width: 100%; border-collapse: collapse; }
.matrix-wrap th, .matrix-wrap td { padding: 14px 16px; border: 1px solid var(--border); text-align: center; font-size: 13px; }
.matrix-wrap th { background: var(--bg-elevated); color: var(--text-secondary); font-weight: 600; }
.matrix-wrap td { background: var(--bg-card); }
.mx-radio { display: none; }
.mx-label {
display: block; padding: 8px 12px; border-radius: 6px;
border: 1.5px solid var(--border); background: var(--bg-elevated);
cursor: pointer; font-weight: 500; color: var(--text-secondary);
transition: var(--transition);
}
.mx-label:hover { border-color: var(--teal); }
.mx-radio:checked + .mx-label { background: var(--teal); border-color: var(--teal); color: #fff; }
/* ===== IMAGE ===== */
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 14px; }
.img-card {
border: 1.5px solid var(--border); border-radius: var(--radius-md);
overflow: hidden; cursor: pointer; transition: var(--transition-spring);
background: var(--bg-elevated);
}
.img-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-sm); border-color: rgba(255,255,255,0.1); }
.img-card.selected { border-color: var(--teal); box-shadow: var(--shadow-teal); }
.img-card img { width: 100%; height: 140px; object-fit: cover; display: block; }
/* ===== RANKING ===== */
.rank-list { list-style: none; display: flex; flex-direction: column; gap: 8px; padding: 0; }
.rank-item {
display: flex; align-items: center; gap: 12px;
padding: 14px 16px; background: var(--bg-elevated);
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
cursor: grab; transition: var(--transition);
}
.rank-item:hover { border-color: rgba(255,255,255,0.1); background: var(--bg-hover); }
.rank-item .rank-num { color: var(--teal); font-weight: 700; font-size: 14px; min-width: 24px; }
.rank-item .rank-text { flex: 1; font-size: 14px; color: var(--text-primary); }
.rank-btns { display: flex; gap: 6px; }
.rank-btn {
background: var(--bg-input); border: 1px solid var(--border);
color: var(--text-secondary); border-radius: 6px; padding: 5px 10px;
font-size: 12px; cursor: pointer; transition: var(--transition); font-family: inherit;
}
.rank-btn:hover { border-color: var(--teal); color: var(--teal); }
/* ===== SLIDER ===== */
.slider-wrap { padding: 8px 0; }
.slider-range {
width: 100%; height: 6px; background: var(--bg-elevated);
border-radius: 100px; outline: none;
-webkit-appearance: none; appearance: none;
}
.slider-range::-webkit-slider-thumb {
-webkit-appearance: none; width: 22px; height: 22px;
background: var(--teal); border-radius: 50%;
cursor: pointer; box-shadow: var(--shadow-teal);
}
.slider-range::-moz-range-thumb {
width: 22px; height: 22px; background: var(--teal);
border-radius: 50%; cursor: pointer; border: none;
}
.slider-val {
display: inline-block; margin-top: 10px; padding: 6px 14px;
background: var(--bg-elevated); border: 1px solid var(--border);
border-radius: 100px; font-size: 15px; font-weight: 600; color: var(--teal);
}
/* ===== NAVIGATION ===== */
.q-nav {
display: flex; gap: 10px; margin-top: 28px;
padding-top: 20px; border-top: 1px solid var(--border);
}
.nav-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 10px 22px; border-radius: var(--radius-sm);
font-size: 13px; font-weight: 600; font-family: inherit;
cursor: pointer; transition: var(--transition-spring);
border: 1.5px solid var(--border); background: var(--bg-elevated);
color: var(--text-secondary);
}
.nav-btn:hover { border-color: rgba(255,255,255,0.15); color: var(--text-primary); transform: translateY(-1px); }
.nav-btn.primary { background: var(--teal); border-color: var(--teal); color: #fff; }
.nav-btn.primary:hover { background: var(--teal-dark); box-shadow: var(--shadow-teal); }
.submit-btn {
display: none; align-items: center; gap: 8px;
padding: 12px 28px; background: var(--teal); color: #fff;
border: none; border-radius: var(--radius-sm);
font-size: 14px; font-weight: 600; font-family: inherit;
cursor: pointer; transition: var(--transition-spring);
box-shadow: var(--shadow-teal); margin-top: 20px;
}
.submit-btn:hover { background: var(--teal-dark); transform: translateY(-1px); }
.hidden { display: none !important; }
.rank-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
/* End message */
.end-message-card {
background: var(--bg-card); border: 1px solid var(--green-border);
border-radius: var(--radius-lg); padding: 48px 36px; text-align: center;
}
.end-message-card h3 { color: var(--teal); margin-bottom: 12px; font-size: 22px; }
.end-message-card p { color: var(--text-secondary); font-size: 15px; }
@@media (max-width: 768px) {
.survey-page { padding: 12px 0 60px; }
.stepper-sidebar { display: none; }
.survey-body { flex-direction: column; gap: 0; }
.user-info-card { grid-template-columns: 1fr; border-radius: 0; margin-bottom: 0; border-left: none; border-right: none; }
.survey-header-card { border-radius: 0; border-left: none; border-right: none; padding: 24px 20px; margin-bottom: 0; }
.content-area { width: 100%; }
.q-step { border-radius: 0; border-left: none; border-right: none; padding: 24px 20px; margin: 0; }
.q-step.active { display: block; }
.q-text { font-size: 15px; margin-bottom: 18px; }
.q-num-badge { width: 24px; height: 24px; font-size: 11px; margin-right: 8px; }
.option-item { padding: 12px 14px; gap: 10px; }
.option-item label { font-size: 13px; }
.likert-row { flex-direction: column; }
.image-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
.rating-row { gap: 8px; }
.rating-card { padding: 10px 14px; }
.rating-card .r-star { font-size: 22px; }
.q-nav { margin-top: 20px; padding-top: 16px; }
.nav-btn { padding: 10px 18px; font-size: 13px; }
.submit-btn { margin: 16px 20px; }
.survey-shell { border-radius: 0; }
}
</style>
<div class="survey-page">
<div class="survey-shell">
<form id="questionnaireForm" method="post" asp-action="DisplayQuestionnaire">
<input type="hidden" name="Id" value="@Model.Id" />
<input type="hidden" name="Title" value="@Model.Title" />
<input type="hidden" name="Description" value="@Model.Description" />
<input type="hidden" id="questionsShown" name="QuestionsShown" value="" />
<input type="hidden" id="questionsSkipped" name="QuestionsSkipped" value="" />
<!-- Header -->
<div class="survey-header-card">
<h1 class="survey-title">@Model.Title<span class="survey-title-dot"></span></h1>
<div class="survey-desc">@Html.Raw(Model.Description)</div>
</div>
<!-- User Info -->
<div class="user-info-card">
<div>
<span class="field-label">Your Name</span>
<input type="text" class="field-input" name="UserName" placeholder="Enter your name" required autocomplete="name" />
</div>
<div>
<span class="field-label">Email Address</span>
<input type="email" class="field-input" name="Email" placeholder="Enter your email" required autocomplete="email" />
</div>
</div>
<!-- Body: Stepper + Content -->
<div class="survey-body">
<div class="stepper-sidebar">
<div class="stepper-title">Questions</div>
@for (int i = 0; i < Model.Questions.Count; i++)
{
<div class="step-dot @(i == 0 ? "active" : "")" data-step="@i">
<span class="dot-num">@(i + 1)</span>
<span class="dot-label">@Model.Questions[i].Type.ToString().Replace("_"," ")</span>
</div>
}
</div>
<div class="content-area">
@for (int i = 0; i < Model.Questions.Count; i++)
{
var question = Model.Questions[i];
<input type="hidden" name="Questions[@i].Text" value="@question.Text" />
<input type="hidden" name="Questions[@i].Id" value="@question.Id" />
<input type="hidden" name="Questions[@i].Type" value="@((int)question.Type)" />
@for (int j = 0; j < question.Answers.Count; j++)
{
<input type="hidden" name="Questions[@i].Answers[@j].Id" value="@question.Answers[j].Id" />
<input type="hidden" name="Questions[@i].Answers[@j].Text" value="@question.Answers[j].Text" />
}
<div class="q-step @(i == 0 ? "active" : "")" data-step="@i" data-question-id="@question.Id">
<div class="q-text"><span class="q-num-badge">@(i + 1)</span> @question.Text</div>
@switch (question.Type)
{
case QuestionType.Text:
<input type="text" class="q-input" name="Questions[@i].SelectedText" placeholder="Type your answer..." />
@if (question.Answers.Any())
{
<input type="hidden" name="Questions[@i].SelectedAnswerIds" value="@question.Answers[0].Id" data-condition="@question.Answers[0].ConditionJson" />
}
break;
case QuestionType.Open_ended:
<textarea class="q-input" name="Questions[@i].SelectedText" rows="4" placeholder="Type your answer..."></textarea>
@if (question.Answers.Any())
{
<input type="hidden" name="Questions[@i].SelectedAnswerIds" value="@question.Answers[0].Id" data-condition="@question.Answers[0].ConditionJson" />
}
break;
case QuestionType.CheckBox:
case QuestionType.Multiple_choice:
case QuestionType.Demographic:
<div class="option-list">
@foreach (var answer in question.Answers)
{
<div class="option-item @(answer.IsOtherOption ? "is-other-option" : "")">
<input type="checkbox" id="q@(i)_a@(answer.Id)" name="Questions[@i].SelectedAnswerIds" value="@answer.Id"
data-condition="@answer.ConditionJson" data-qi="@i" data-aid="@answer.Id"
data-is-other="@answer.IsOtherOption.ToString().ToLower()" />
<label for="q@(i)_a@(answer.Id)">@answer.Text</label>
</div>
@if (answer.IsOtherOption)
{
<div class="other-text-container" id="otherText_@(i)_@(answer.Id)">
<textarea class="other-text-input" name="Questions[@i].OtherTexts[@answer.Id]" id="otherArea_@(i)_@(answer.Id)" placeholder="Please specify..."></textarea>
</div>
}
}
</div>
break;
case QuestionType.TrueFalse:
<div class="option-list">
@foreach (var answer in question.Answers)
{
<div class="option-item @(answer.IsOtherOption ? "is-other-option" : "")">
<input type="radio" id="q@(i)_a@(answer.Id)" name="Questions[@i].SelectedAnswerIds" value="@answer.Id"
data-condition="@answer.ConditionJson" data-qi="@i" data-aid="@answer.Id"
data-is-other="@answer.IsOtherOption.ToString().ToLower()" />
<label for="q@(i)_a@(answer.Id)">@answer.Text</label>
</div>
@if (answer.IsOtherOption)
{
<div class="other-text-container" id="otherText_@(i)_@(answer.Id)">
<textarea class="other-text-input" name="Questions[@i].OtherTexts[@answer.Id]" id="otherArea_@(i)_@(answer.Id)" placeholder="Please specify..."></textarea>
</div>
}
}
</div>
break;
case QuestionType.Rating:
<div class="rating-row" data-question="@i">
@foreach (var answer in question.Answers)
{
<div class="rating-card" data-aid="@answer.Id">
<span class="r-label">@answer.Text</span>
<input type="radio" id="q@(i)_r@(answer.Id)" name="Questions[@i].SelectedAnswerIds" value="@answer.Id" class="hidden" data-condition="@answer.ConditionJson" />
<label for="q@(i)_r@(answer.Id)" class="bi bi-star-fill r-star"></label>
</div>
}
</div>
break;
case QuestionType.Likert:
<div class="likert-row">
@foreach (var answer in question.Answers)
{
<label class="likert-chip @(answer.IsOtherOption ? "is-other-option" : "")" data-aid="@answer.Id">
<input type="radio" name="Questions[@i].SelectedAnswerIds" value="@answer.Id"
data-condition="@answer.ConditionJson" data-qi="@i" data-aid="@answer.Id"
data-is-other="@answer.IsOtherOption.ToString().ToLower()" />
@answer.Text
</label>
@if (answer.IsOtherOption)
{
<div class="other-text-container" id="otherText_@(i)_@(answer.Id)">
<textarea class="other-text-input" name="Questions[@i].OtherTexts[@answer.Id]" id="otherArea_@(i)_@(answer.Id)" placeholder="Please specify..."></textarea>
</div>
}
}
</div>
break;
case QuestionType.Matrix:
<div class="matrix-wrap">
<table>
<thead>
<tr>
<th style="text-align:left;">Statement</th>
@foreach (var opt in question.Answers) { <th>@opt.Text</th> }
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left; font-weight:500; color:var(--text-primary);">@question.Text</td>
@foreach (var opt in question.Answers)
{
<td>
<input type="radio" class="mx-radio" id="q@(i)_mx@(opt.Id)" name="Questions[@i].SelectedAnswerIds" value="@opt.Id" data-condition="@opt.ConditionJson" data-qi="@i" data-aid="@opt.Id" data-is-other="@opt.IsOtherOption.ToString().ToLower()" />
<label for="q@(i)_mx@(opt.Id)" class="mx-label">@opt.Text</label>
</td>
}
</tr>
</tbody>
</table>
</div>
break;
case QuestionType.Image:
<div class="image-grid" data-question-id="@question.Id">
@foreach (var answer in question.Answers)
{
<div class="img-card @(answer.IsOtherOption ? "is-other-option" : "")" id="imgCard_@answer.Id" onclick="pickImage('@answer.Id','@question.Id')">
@if (answer.IsOtherOption)
{
<div style="height:140px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;background:var(--bg-elevated);">
<i class="bi bi-plus-circle" style="font-size:1.8rem;color:var(--teal);"></i>
<span style="font-size:13px;font-weight:600;color:var(--text-primary);">@answer.Text</span>
</div>
}
else
{
<img src="@answer.Text" alt="Option" />
}
<input type="radio" id="imgR_@answer.Id" name="Questions[@i].SelectedAnswerIds" value="@answer.Id" class="hidden"
data-condition="@answer.ConditionJson" data-qi="@i" data-aid="@answer.Id"
data-is-other="@answer.IsOtherOption.ToString().ToLower()" />
</div>
@if (answer.IsOtherOption)
{
<div class="other-text-container" id="otherText_@(i)_@(answer.Id)" style="grid-column:1/-1;">
<textarea class="other-text-input" name="Questions[@i].OtherTexts[@answer.Id]" id="otherArea_@(i)_@(answer.Id)" placeholder="Describe your choice..."></textarea>
</div>
}
}
</div>
break;
case QuestionType.Ranking:
<p class="rank-hint">Drag to reorder or use the buttons</p>
<ul class="rank-list" id="rank_@question.Id">
@{ int ri = 1; }
@foreach (var answer in question.Answers)
{
<li class="rank-item" draggable="true" id="ri_@answer.Id">
<span class="rank-num">@ri.</span>
<span class="rank-text">@answer.Text</span>
<input type="hidden" name="Questions[@i].SelectedAnswerIds" value="@answer.Id" data-condition="@answer.ConditionJson" />
<div class="rank-btns">
<button type="button" class="rank-btn" onclick="rankUp(this)"><i class="bi bi-arrow-up"></i></button>
<button type="button" class="rank-btn" onclick="rankDown(this)"><i class="bi bi-arrow-down"></i></button>
</div>
</li>
ri++;
}
</ul>
break;
case QuestionType.Slider:
@if (question.Answers.Any())
{
<div class="slider-wrap">
<input type="range" class="slider-range" name="Questions[@i].SelectedText" min="0" max="100" step="1" value="50"
oninput="this.parentElement.querySelector('.slider-val').textContent=this.value" />
<input type="hidden" name="Questions[@i].SelectedAnswerIds" value="@question.Answers[0].Id" data-condition="@question.Answers[0].ConditionJson" />
<span class="slider-val">50</span>
</div>
}
break;
default:
<div style="padding:16px;background:var(--red-soft);border:1px solid var(--red);border-radius:8px;color:var(--red);font-weight:500;">Unsupported question type.</div>
break;
}
<div class="q-nav">
@if (i > 0) { <button type="button" class="nav-btn prev-btn"><i class="bi bi-arrow-left"></i> Previous</button> }
@if (i < Model.Questions.Count - 1) { <button type="button" class="nav-btn primary next-btn">Next <i class="bi bi-arrow-right"></i></button> }
</div>
</div>
}
<button type="submit" class="submit-btn"><i class="bi bi-check-lg"></i> Submit</button>
</div>
</div>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function () {
const form = $('#questionnaireForm');
const steps = $('.q-step');
const dots = $('.step-dot');
const submitBtn = $('.submit-btn');
let cur = 0, navPath = [0], shown = [1], skipped = [];
// ===== STEP NAVIGATION =====
function showStep(i) {
steps.removeClass('active').eq(i).addClass('active');
dots.removeClass('active').eq(i).addClass('active');
submitBtn.css('display', i === steps.length - 1 ? 'inline-flex' : 'none');
try { dots.eq(i)[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(e){}
}
function track(n) { if (!shown.includes(n)) shown.push(n); }
function trackSkip(n, r) { skipped.push({ questionNumber: n, reason: r }); }
function updateTracking() { $('#questionsShown').val(JSON.stringify(shown)); $('#questionsSkipped').val(JSON.stringify(skipped)); }
// ===== CONDITION LOGIC =====
function getConditions() {
const el = steps.eq(cur)[0];
const conds = [];
el.querySelectorAll('input:checked, input[type="text"][name*="SelectedText"], textarea[name*="SelectedText"]').forEach(inp => {
const c = inp.getAttribute('data-condition');
if (c && c !== '' && c !== 'null') { try { conds.push(JSON.parse(c)); } catch(e){} }
});
// Also check hidden inputs with conditions (for Text/Open_ended/Slider)
el.querySelectorAll('input[type="hidden"][data-condition]').forEach(inp => {
const c = inp.getAttribute('data-condition');
if (c && c !== '' && c !== 'null') { try { conds.push(JSON.parse(c)); } catch(e){} }
});
return conds;
}
function evalCondition() {
for (let c of getConditions()) {
if (c.ActionType === 1 && c.TargetQuestionNumber) return { a: 'jump', t: c.TargetQuestionNumber };
if (c.ActionType === 2 && c.SkipCount) return { a: 'skip', c: c.SkipCount };
if (c.ActionType === 3) return { a: 'end', m: c.EndMessage };
}
return { a: 'next' };
}
function goNext() {
const r = evalCondition();
switch (r.a) {
case 'jump':
const ti = r.t - 1;
for (let x = cur + 2; x < r.t; x++) trackSkip(x, 'Jump from Q' + (cur+1));
cur = ti; navPath.push(cur); track(cur+1); showStep(cur); break;
case 'skip':
const ni = cur + r.c + 1;
for (let x = 1; x <= r.c; x++) { if (cur+x+1 <= steps.length) trackSkip(cur+x+1, 'Skip from Q'+(cur+1)); }
cur = Math.min(ni, steps.length-1); navPath.push(cur); track(cur+1); showStep(cur); break;
case 'end':
for (let x = cur+2; x <= steps.length; x++) trackSkip(x, 'Ended at Q'+(cur+1));
steps.hide();
$('.content-area').append(`<div class="end-message-card"><h3>Survey Complete</h3><p>${r.m||'Thank you for participating!'}</p></div>`);
submitBtn.hide(); break;
default:
if (cur < steps.length-1) { cur++; navPath.push(cur); track(cur+1); showStep(cur); }
}
}
function goPrev() { if (navPath.length > 1) { navPath.pop(); cur = navPath[navPath.length-1]; showStep(cur); } }
$(document).on('click', '.next-btn', goNext);
$(document).on('click', '.prev-btn', goPrev);
form.on('submit', updateTracking);
// ===== OTHER OPTION LOGIC =====
$(document).on('change', 'input[type="checkbox"]', function () {
if ($(this).attr('data-is-other') === 'true') {
const qi = $(this).data('qi'), aid = $(this).data('aid');
const box = $(`#otherText_${qi}_${aid}`);
if ($(this).is(':checked')) { box.addClass('show'); box.find('textarea').focus(); }
else { box.removeClass('show').find('textarea').val(''); }
}
});
$(document).on('change', 'input[type="radio"]', function () {
const qi = $(this).data('qi');
if (qi !== undefined && qi !== '') {
// Hide all other boxes for this question
$(`[id^="otherText_${qi}_"]`).removeClass('show').find('textarea').val('');
// Show if this is the other option
if ($(this).attr('data-is-other') === 'true') {
const aid = $(this).data('aid');
$(`#otherText_${qi}_${aid}`).addClass('show').find('textarea').focus();
}
}
});
// ===== RATING =====
$(document).on('click', '.rating-card', function () {
const row = $(this).closest('.rating-row');
row.find('.r-star').removeClass('selected');
$(this).prevAll('.rating-card').addBack().find('.r-star').addClass('selected');
$(this).find('input[type="radio"]').prop('checked', true);
});
$(document).on('mouseenter', '.rating-card', function () {
$(this).prevAll('.rating-card').addBack().addClass('rating-hover');
});
$(document).on('mouseleave', '.rating-card', function () {
$(this).closest('.rating-row').find('.rating-card').removeClass('rating-hover');
});
// ===== LIKERT =====
$(document).on('change', '.likert-chip input', function () {
$(this).closest('.likert-row').find('.likert-chip').removeClass('selected');
$(this).closest('.likert-chip').addClass('selected');
});
// ===== IMAGE =====
window.pickImage = function (aid, qid) {
$(`.image-grid[data-question-id="${qid}"] .img-card`).removeClass('selected');
$(`#imgCard_${aid}`).addClass('selected');
$(`#imgR_${aid}`).prop('checked', true).trigger('change');
};
// ===== RANKING =====
window.rankUp = function (btn) { const li = $(btn).closest('.rank-item'); if (li.prev().length) li.insertBefore(li.prev()); updateRankNums(li.closest('.rank-list')); };
window.rankDown = function (btn) { const li = $(btn).closest('.rank-item'); if (li.next().length) li.insertAfter(li.next()); updateRankNums(li.closest('.rank-list')); };
function updateRankNums(list) { list.find('.rank-item').each(function(i){ $(this).find('.rank-num').text((i+1)+'.'); }); }
// Drag ranking
let dragEl = null;
$(document).on('dragstart', '.rank-item', function(e) { dragEl = this; e.originalEvent.dataTransfer.effectAllowed = 'move'; });
$(document).on('dragover', '.rank-item', function(e) { e.preventDefault(); });
$(document).on('drop', '.rank-item', function(e) {
e.preventDefault();
if (dragEl !== this) {
const rect = this.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
if (e.originalEvent.clientY < mid) $(this).before(dragEl);
else $(this).after(dragEl);
updateRankNums($(this).closest('.rank-list'));
}
});
showStep(0);
});
</script>
}