-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/surveys #170
Open
Ahmedhossamdev
wants to merge
19
commits into
main
Choose a base branch
from
feat/surveys
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+644
−8
Open
Feat/surveys #170
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
9988c82
Add alias for stores directory in Svelte config
Ahmedhossamdev 8f8c298
Add SurveyModal component
Ahmedhossamdev 6f852ab
Add hero question survey functionality to StopPane component
Ahmedhossamdev 917ea7a
Add survey API endpoints for fetching, submitting, and updating surve…
Ahmedhossamdev 2747570
Update alerts API endpoint to include region path in URL
Ahmedhossamdev 3dfe06b
Add survey store
Ahmedhossamdev f8f6581
Add surveys
Ahmedhossamdev bdc712a
Add SurveyQuestion component
Ahmedhossamdev 5b6d186
Add survey utility functions for loading and managing survey responses
Ahmedhossamdev 7e83ae9
Add user utility functions for managing user ID cookies
Ahmedhossamdev 1c49741
Update .env.example to include region ID for OneBusAway API
Ahmedhossamdev bac3d5e
Update README.md
Ahmedhossamdev 2dc3531
Refactorfunctions to use 'stop' parameter instead of stopId and add f…
Ahmedhossamdev a3402d0
Update loadSurveys function to use 'stop' parameter instead of stop.id
Ahmedhossamdev 7c241f0
Add onMount lifecycle to load surveys using stop and user ID
Ahmedhossamdev 6da2b3a
Linting and formatting
Ahmedhossamdev ab08be3
Remove unused 'time' import from +page.svelte
Ahmedhossamdev a6e1215
Fix typo
Ahmedhossamdev c1a67cf
Rename surveyPublicIdentifierOutside to surveyPublicId
Ahmedhossamdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,10 +4,14 @@ | |
import LoadingSpinner from '$components/LoadingSpinner.svelte'; | ||
import Accordion from '$components/containers/SingleSelectAccordion.svelte'; | ||
import AccordionItem from '$components/containers/AccordionItem.svelte'; | ||
import SurveyModal from '$components/surveys/SurveyModal.svelte'; | ||
import SurveyQuestion from '$components/surveys/SurveyQuestion.svelte'; | ||
import { onDestroy } from 'svelte'; | ||
|
||
import '$lib/i18n.js'; | ||
import { isLoading, t } from 'svelte-i18n'; | ||
import { submitHeroQuestion, skipSurvey } from '$lib/Surveys/surveyUtils'; | ||
import { surveyStore, showSurveyModal } from '$stores/surveyStore'; | ||
import { getUserId } from '$lib/utils/user'; | ||
|
||
/** | ||
* @typedef {Object} Props | ||
|
@@ -28,6 +32,7 @@ | |
let error = $state(); | ||
|
||
let interval = null; | ||
let currentStopSurvey = $state(null); | ||
|
||
async function loadData(stopID) { | ||
loading = true; | ||
|
@@ -80,6 +85,49 @@ | |
tripSelected({ detail: data }); | ||
handleUpdateRouteMap({ detail: { show } }); | ||
} | ||
|
||
let heroAnswer = ''; | ||
let nextSurveyQuestion = $state(false); | ||
let surveyPublicIdentifier = $state(null); | ||
let showHeroQuestion = $state(true); | ||
|
||
async function handleNext() { | ||
if (heroAnswer && heroAnswer.trim() != '') { | ||
showSurveyModal.set(true); | ||
nextSurveyQuestion = true; | ||
|
||
let surveyResponse = { | ||
survey_id: currentStopSurvey.id, | ||
user_identifier: getUserId(), | ||
stop_identifier: stop.id, | ||
stop_latitude: stop.lat, | ||
stop_longitude: stop.lon, | ||
responses: [] | ||
}; | ||
|
||
surveyResponse.responses[0] = { | ||
question_id: currentStopSurvey.questions[0].id, | ||
question_label: currentStopSurvey.questions[0].content.label_text, | ||
question_type: currentStopSurvey.questions[0].content.type, | ||
answer: heroAnswer | ||
}; | ||
|
||
surveyPublicIdentifier = await submitHeroQuestion(surveyResponse); | ||
showHeroQuestion = false; | ||
} | ||
} | ||
|
||
function handleSkip() { | ||
skipSurvey(currentStopSurvey); | ||
showHeroQuestion = false; | ||
} | ||
function handleHeroQuestionChange(event) { | ||
heroAnswer = event.target.value; | ||
} | ||
|
||
$effect(() => { | ||
currentStopSurvey = $surveyStore; | ||
}); | ||
</script> | ||
|
||
{#if $isLoading} | ||
|
@@ -93,7 +141,6 @@ | |
{#if error} | ||
<p>{error}</p> | ||
{/if} | ||
|
||
{#if arrivalsAndDepartures} | ||
<div class="space-y-4"> | ||
<div> | ||
|
@@ -114,6 +161,44 @@ | |
</div> | ||
</div> | ||
</div> | ||
{#if showHeroQuestion && currentStopSurvey} | ||
<div class="hero-question-container relative rounded-lg bg-gray-50 p-6 shadow"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please extract the content inside of the |
||
<button | ||
onclick={handleSkip} | ||
class="absolute right-2 top-2 text-2xl text-gray-500 hover:text-gray-700" | ||
title="Skip hero question" | ||
> | ||
× | ||
</button> | ||
<h2 class="h2 mb-4">{currentStopSurvey.name}</h2> | ||
<SurveyQuestion | ||
question={currentStopSurvey.questions[0]} | ||
index={0} | ||
required={currentStopSurvey.questions[0].required} | ||
onInputChange={handleHeroQuestionChange} | ||
variant="compact" | ||
error={[false]} | ||
/> | ||
<div class="mt-4 flex justify-end"> | ||
<button | ||
onclick={handleNext} | ||
class="rounded bg-green-500 px-4 py-3 text-white shadow transition hover:bg-green-600" | ||
> | ||
Next | ||
</button> | ||
</div> | ||
</div> | ||
{/if} | ||
|
||
{#if nextSurveyQuestion} | ||
<SurveyModal | ||
currentSurvey={currentStopSurvey} | ||
{stop} | ||
skipHeroQuestion={true} | ||
surveyPublicIdentifierOutside={surveyPublicIdentifier} | ||
/> | ||
{/if} | ||
|
||
{#if arrivalsAndDepartures.arrivalsAndDepartures.length === 0} | ||
<div class="flex items-center justify-center"> | ||
<p>{$t('no_arrivals_or_departures_in_next_30_minutes')}</p> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
<script> | ||
import { Modal, Button } from 'flowbite-svelte'; | ||
import SurveyQuestion from './SurveyQuestion.svelte'; | ||
import { | ||
submitHeroQuestion as submitHeroQuestionUtil, | ||
updateSurveyResponse as updateSurveyResponseUtil, | ||
skipSurvey, | ||
submitSurvey | ||
} from '$lib/Surveys/surveyUtils'; | ||
|
||
import { showSurveyModal, surveyStore } from '$stores/surveyStore'; | ||
import { getUserId } from '$lib/utils/user'; | ||
|
||
let { stop = $bindable(null), skipHeroQuestion, surveyPublicIdentifierOutside } = $props(); | ||
|
||
let userAnswers = $state([]); | ||
let heroQuestionAnswered = $state(false); | ||
let heroQuestion = $state(null); | ||
let remainingQuestions = $state([]); | ||
let surveyPublicIdentifier = $state(null); | ||
let surveySubmitted = $state(false); | ||
let errors = $state([]); | ||
|
||
let currentSurvey = $state($surveyStore); | ||
|
||
if (currentSurvey && currentSurvey.questions) { | ||
heroQuestion = currentSurvey.questions[0]; | ||
remainingQuestions = currentSurvey.questions.slice(1); | ||
} | ||
|
||
let surveyResponse = { | ||
survey_id: currentSurvey.id, | ||
user_identifier: getUserId(), | ||
stop_identifier: stop?.id ?? null, | ||
stop_latitude: stop?.lat ?? null, | ||
stop_longitude: stop?.lon ?? null, | ||
responses: [] | ||
}; | ||
|
||
function handleInputChange(event, question, index) { | ||
const type = question.content.type; | ||
|
||
if (type === 'text' || type === 'radio') { | ||
userAnswers[index] = event.target.value; | ||
} else if (type === 'checkbox') { | ||
const value = event.target.value; | ||
if (event.target.checked) { | ||
userAnswers[index] = [...(userAnswers[index] || []), value]; | ||
} else { | ||
userAnswers[index] = (userAnswers[index] || []).filter((option) => option !== value); | ||
if (userAnswers[index].length === 0) { | ||
delete userAnswers[index]; | ||
} | ||
} | ||
} | ||
|
||
surveyResponse.responses[index] = { | ||
question_id: question.id, | ||
question_label: question.content.label_text, | ||
question_type: question.content.type, | ||
answer: userAnswers[index] || null | ||
}; | ||
} | ||
|
||
function validateAnswers() { | ||
let valid = true; | ||
errors = new Array(currentSurvey.questions.length).fill(false); | ||
|
||
if (!heroQuestionAnswered && !skipHeroQuestion) { | ||
if (heroQuestion.required && (!userAnswers[0] || userAnswers[0].length === 0)) { | ||
errors[0] = true; | ||
valid = false; | ||
} | ||
} else { | ||
remainingQuestions.forEach((question, index) => { | ||
const answer = userAnswers[index + 1]; | ||
if (question.required && (!answer || (Array.isArray(answer) && answer.length === 0))) { | ||
errors[index + 1] = true; | ||
valid = false; | ||
} | ||
}); | ||
} | ||
|
||
return valid; | ||
} | ||
|
||
async function submitHeroQuestion() { | ||
if (!validateAnswers()) return; | ||
|
||
try { | ||
surveyPublicIdentifier = await submitHeroQuestionUtil(surveyResponse); | ||
heroQuestionAnswered = true; | ||
submitSurvey(currentSurvey, false); | ||
} catch (error) { | ||
console.error('Error submitting hero question:', error); | ||
} | ||
} | ||
|
||
async function updateSurveyResponse() { | ||
if (surveyPublicIdentifierOutside) [(surveyPublicIdentifier = surveyPublicIdentifierOutside)]; | ||
updateSurveyResponseUtil(surveyPublicIdentifier, surveyResponse); | ||
} | ||
|
||
function handleSubmit() { | ||
if (!validateAnswers()) return; | ||
|
||
updateSurveyResponse(); | ||
surveySubmitted = true; | ||
submitSurvey(currentSurvey, true); | ||
} | ||
</script> | ||
|
||
{#if $showSurveyModal && currentSurvey} | ||
<Modal open={$showSurveyModal} size="3xl" class="max-w-5xl rounded-2xl"> | ||
<div | ||
class="flex items-center justify-between rounded-t-2xl border-b border-gray-200 p-6 dark:border-gray-700" | ||
> | ||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{currentSurvey.name}</h2> | ||
</div> | ||
|
||
<div class="flex flex-col space-y-6 p-6"> | ||
{#if surveySubmitted} | ||
<div class="flex flex-1 flex-col items-center justify-center p-12"> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
class="h-20 w-20 text-green-500" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
stroke="currentColor" | ||
> | ||
<path | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
d="M5 13l4 4L19 7" | ||
/> | ||
</svg> | ||
<h2 class="mt-6 text-4xl font-bold text-gray-900 dark:text-white">Survey Submitted</h2> | ||
<p class="mt-3 text-xl text-gray-700 dark:text-gray-300"> | ||
Thank you for taking the survey! | ||
</p> | ||
</div> | ||
{:else} | ||
<div class="max-h-[60vh] overflow-y-auto p-6"> | ||
{#if !heroQuestionAnswered && !skipHeroQuestion} | ||
<SurveyQuestion | ||
question={heroQuestion} | ||
index={0} | ||
value={userAnswers[0]} | ||
onInputChange={(e) => handleInputChange(e, heroQuestion, 0)} | ||
required={heroQuestion?.required} | ||
error={errors[0]} | ||
/> | ||
{:else} | ||
<div class="space-y-8"> | ||
{#each remainingQuestions as question, index} | ||
<SurveyQuestion | ||
{question} | ||
index={index + 1} | ||
value={userAnswers[index + 1]} | ||
onInputChange={(e) => handleInputChange(e, question, index + 1)} | ||
required={question.required} | ||
error={errors[index + 1]} | ||
/> | ||
{/each} | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
<div | ||
class="sticky bottom-0 flex justify-end gap-4 rounded-b-2xl border-t border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800" | ||
> | ||
{#if !heroQuestionAnswered && !skipHeroQuestion} | ||
<Button | ||
onclick={submitHeroQuestion} | ||
color="green" | ||
class="rounded-lg px-10 py-3 shadow-md transition-shadow hover:shadow-lg" | ||
> | ||
Next | ||
</Button> | ||
{:else} | ||
<Button | ||
onclick={skipSurvey} | ||
color="red" | ||
class="rounded-lg px-10 py-3 shadow-md transition-shadow hover:shadow-lg" | ||
> | ||
Skip | ||
</Button> | ||
<Button | ||
onclick={handleSubmit} | ||
color="green" | ||
class="rounded-lg px-10 py-3 shadow-md transition-shadow hover:shadow-lg" | ||
> | ||
Submit | ||
</Button> | ||
{/if} | ||
</div> | ||
{/if} | ||
</div> | ||
</Modal> | ||
{/if} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opt for an early return here to avoid unnecessary nesting.