Skip to content

Commit

Permalink
Add initial support for user questions
Browse files Browse the repository at this point in the history
- backend
  - add models/questions with UserQuestion and UserQuestionText
  - add /user-questions endpoint to get list of questions
  - add /admin/user-questions endpoints to create, update and delete questions
- admin frontend
  - add UserQuestions component to edit list of questions
  - only support text and select inputs for now
  - refactor admin frontend
  • Loading branch information
lkeegan committed Sep 27, 2024
1 parent 4a7dae5 commit c174630
Show file tree
Hide file tree
Showing 19 changed files with 591 additions and 41 deletions.
99 changes: 97 additions & 2 deletions frontend/src/lib/admin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { milestoneGroups } from '$lib/stores/adminStore';
import { milestoneGroups, userQuestions } from '$lib/stores/adminStore';

export async function refreshMilestoneGroups() {
console.log('refreshMilestoneGroups...');
Expand Down Expand Up @@ -54,7 +54,7 @@ export async function updateMilestoneGroup(milestoneGroup) {
console.log('updateMilestoneGroup...');
console.log(milestoneGroup);
try {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestone-groups`, {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestone-groups/`, {
method: 'PUT',
credentials: 'include',
headers: {
Expand Down Expand Up @@ -125,3 +125,98 @@ export async function deleteMilestoneGroup(milestoneGroupId: number | null) {
export function milestoneGroupImageUrl(id: number) {
return `${import.meta.env.VITE_MONDEY_API_URL}/static/mg${id}.jpg`;
}

export async function refreshUserQuestions() {
console.log('refreshQuestions...');
try {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/user-questions/`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
});
const json = await res.json();
console.log(json);
if (res.status === 200) {
userQuestions.set(json);
} else {
console.log('Failed to get UserQuestions');
userQuestions.set([]);
}
} catch (e) {
console.error(e);
userQuestions.set([]);
}
}

export async function newUserQuestion() {
try {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/user-questions/`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (res.status === 200) {
const newUserQuestion = await res.json();
console.log(newUserQuestion);
await refreshUserQuestions();
return newUserQuestion;
} else {
console.log('Failed to create new Question');
}
} catch (e) {
console.error(e);
}
}

export async function updateUserQuestion(userQuestion) {
console.log('updateUserQuestion...');
console.log(userQuestion);
try {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/user-questions/`, {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(userQuestion)
});
if (res.status === 200) {
const updatedUserQuestion = await res.json();
console.log(updatedUserQuestion);
await refreshUserQuestions();
return updatedUserQuestion;
} else {
console.log('Failed to create new UserQuestion');
}
} catch (e) {
console.error(e);
}
return null;
}

export async function deleteUserQuestion(id: number) {
try {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/user-questions/${id}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
});
if (res.status === 200) {
await refreshUserQuestions();
} else {
console.log('Failed to delete Question');
}
} catch (e) {
console.error(e);
}
return null;
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
<script lang="ts">
import { Button, Modal } from 'flowbite-svelte';
import { ExclamationCircleOutline } from 'flowbite-svelte-icons';
import { deleteMilestoneGroup } from '$lib/admin';
import { _ } from 'svelte-i18n';
export let open: boolean;
export let groupId: number | null;
export let onClick: () => void;
</script>

<Modal bind:open size="xs" autoclose>
<div class="text-center">
<ExclamationCircleOutline class="mx-auto mb-4 h-12 w-12 text-gray-400 dark:text-gray-200" />
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
Are you sure you want to delete this MilestoneGroup?
{$_('admin.delete-are-you-sure')}
</h3>
<Button
color="red"
class="me-2"
on:click={() => {
deleteMilestoneGroup(groupId);
}}>Yes, I'm sure</Button
>
<Button color="alternative">No, cancel</Button>
<Button color="red" class="me-2" on:click={onClick}>
{$_('admin.yes-sure')}
</Button>
<Button color="alternative">{$_('admin.no-cancel')}</Button>
</div>
</Modal>
139 changes: 139 additions & 0 deletions frontend/src/lib/components/Admin/EditUserQuestionModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<script lang="ts">
import {
Button,
Badge,
Card,
InputAddon,
Textarea,
Input,
Label,
ButtonGroup,
Modal,
Select,
type SelectOptionType
} from 'flowbite-svelte';
import { lang_id, languages } from '$lib/stores/adminStore';
import { updateUserQuestion } from '$lib/admin';
import InputPreview from '$lib/components/Admin/InputPreview.svelte';
export let open: boolean = false;
export let userQuestion: object | null = null;
let preview_lang_id = '1';
let preview_answer = '';
const inputTypes: SelectOptionType<string>[] = [
{ value: 'text', name: 'text' },
{ value: 'select', name: 'select' }
];
function updateOptionsJson() {
const values = userQuestion.options.split(';');
for (const lid in $languages) {
const items = userQuestion.text[lid].options.split(';');
userQuestion.text[lid].options_json = JSON.stringify(
values.map(function (value, index) {
return { value: value, name: items[index] };
})
);
}
}
export async function saveChanges() {
try {
await updateUserQuestion(userQuestion);
} catch (e) {
console.error(e);
}
}
</script>

<Modal title="Edit user question" bind:open autoclose size="xl">
{#if userQuestion}
<div class="flex flex-row items-center">
<div class="mr-5 grow">
<div class="mb-5">
<Label class="mb-2">Question</Label>
{#each Object.values(userQuestion.text) as text}
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>{$languages[text.lang_id]}</InputAddon>
<Input
bind:value={text.question}
on:input={() => {
userQuestion = userQuestion;
}}
placeholder="Question"
/>
</ButtonGroup>
</div>
{/each}
</div>
<div class="mb-5">
<Label class="mb-2">Input type</Label>
<Select class="mt-2" items={inputTypes} bind:value={userQuestion.input} placeholder="" />
</div>
{#if userQuestion.input === 'select'}
<div class="mb-5">
<Label class="mb-2">Options</Label>
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>name</InputAddon>
<Textarea
bind:value={userQuestion.options}
on:input={updateOptionsJson}
placeholder="Option names"
/>
</ButtonGroup>
</div>
{#each Object.values(userQuestion.text) as text}
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>{$languages[text.lang_id]}</InputAddon>
<Textarea
bind:value={text.options}
on:input={updateOptionsJson}
placeholder="Options"
/>
</ButtonGroup>
</div>
{/each}
</div>
{/if}
</div>
<div>
<Card>
<div class="mb-5">
<Label class="mb-2">Preview</Label>
<div class="flex flex-row">
<ButtonGroup class="mb-2 mr-2">
{#each Object.entries($languages) as [lid, lang]}
<Button
checked={preview_lang_id === lid}
on:click={(e) => {
e.stopPropagation();
preview_lang_id = lid;
}}>{lang}</Button
>
{/each}
</ButtonGroup>
</div>
<Card class="mb-4 bg-blue-300">
<InputPreview
data={userQuestion}
lang_id={preview_lang_id}
bind:answer={preview_answer}
/>
</Card>
<Label class="mb-2">Generated answer:</Label>
<Badge large border color="dark">{preview_answer}</Badge>
</div>
</Card>
</div>
</div>
{/if}
<svelte:fragment slot="footer">
<Button color="green" on:click={saveChanges}>Save changes</Button>
<Button color="alternative">Cancel</Button>
</svelte:fragment>
</Modal>
30 changes: 30 additions & 0 deletions frontend/src/lib/components/Admin/InputPreview.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { Input, Select, Label } from 'flowbite-svelte';
export let data;
export let lang_id;
export let answer = '';
function parse_options_json(options_json) {
try {
return JSON.parse(data.text[lang_id].options_json);
} catch (e) {
console.log("Couldn't parse options_json");
console.log(e);
}
return [];
}
$: items = parse_options_json(data.text[lang_id].options_json);
</script>

<div class="mb-5">
<Label class="font-semibold text-gray-700 dark:text-gray-400">{data.text[lang_id].question}</Label
>
</div>
<div class="mb-5">
{#if data.input === 'select'}
<Select {items} bind:value={answer} placeholder="" />
{:else}
<Input type="text" bind:value={answer} />
{/if}
</div>
21 changes: 16 additions & 5 deletions frontend/src/lib/components/Admin/Languages.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
import type { SelectOptionType } from 'flowbite-svelte';
import { updateLanguages } from '$lib/i18n';
import { languages } from '$lib/stores/adminStore';
import DeleteModal from '$lib/components/Admin/DeleteModal.svelte';
const langCodes = ISO6391.getAllCodes();
const langNames = ISO6391.getAllNativeNames();
const langItems = langCodes.reduce(
(acc, k, i) => [...acc, { value: k, name: langNames[i] }],
[] as SelectOptionType<string>[]
);
const langItems = langCodes.map((k, i) => {
return { value: k, name: langNames[i] };
}) as SelectOptionType<string>[];
let selectedLang: string = '';
let currentLanguageId: string = '';
let showDeleteModal: boolean = false;
async function newLanguage() {
try {
Expand Down Expand Up @@ -88,7 +91,8 @@
<Button
color="red"
on:click={() => {
deleteLanguage(id);
currentLanguageId = id;
showDeleteModal = true;
}}>Delete</Button
>
</TableBodyCell>
Expand All @@ -113,3 +117,10 @@
</TableBody>
</Table>
</Card>

<DeleteModal
bind:open={showDeleteModal}
onClick={() => {
deleteLanguage(currentLanguageId);
}}
></DeleteModal>
Loading

0 comments on commit c174630

Please sign in to comment.