Skip to content
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

Add initial support for user questions #94

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
12 changes: 12 additions & 0 deletions frontend/src/lib/components/Admin/AddButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import { Button } from 'flowbite-svelte';
import { PlusOutline } from 'flowbite-svelte-icons';
import { _ } from 'svelte-i18n';

export let onClick: () => void;
export let disabled: boolean = false;
</script>

<Button color="blue" {disabled} on:click={onClick}
><PlusOutline class="me-2 h-5 w-5" /> {$_('admin.add')}</Button
>
11 changes: 11 additions & 0 deletions frontend/src/lib/components/Admin/DeleteButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import { Button } from 'flowbite-svelte';
import { TrashBinOutline } from 'flowbite-svelte-icons';
import { _ } from 'svelte-i18n';

export let onClick: () => void;
</script>

<Button color="red" on:click={onClick}
><TrashBinOutline class="me-2 h-5 w-5" /> {$_('admin.delete')}</Button
>
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>
11 changes: 11 additions & 0 deletions frontend/src/lib/components/Admin/EditButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import { Button } from 'flowbite-svelte';
import { EditOutline } from 'flowbite-svelte-icons';
import { _ } from 'svelte-i18n';

export let onClick: () => void;
</script>

<Button color="yellow" on:click={onClick}
><EditOutline class="me-2 h-5 w-5" /> {$_('admin.edit')}</Button
>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Fileupload,
Modal
} from 'flowbite-svelte';
import { lang_id, languages } from '$lib/stores/adminStore';
import { languages } from '$lib/stores/adminStore';
import {
updateMilestoneGroup,
uploadMilestoneGroupImage,
Expand Down
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>
Loading