diff --git a/src/app.html b/src/app.html index 613d7b69..087505cf 100644 --- a/src/app.html +++ b/src/app.html @@ -12,12 +12,12 @@ - ClassroomIO – The Open Source Platform for Tech Bootcamp + ClassroomIO – The Open Source Platform for Tech Bootcamps - + @@ -27,7 +27,7 @@ - + diff --git a/src/lib/components/Apps/components/Poll/components/CreatePollForm.svelte b/src/lib/components/Apps/components/Poll/components/CreatePollForm.svelte index ddab59a0..a83685b4 100644 --- a/src/lib/components/Apps/components/Poll/components/CreatePollForm.svelte +++ b/src/lib/components/Apps/components/Poll/components/CreatePollForm.svelte @@ -1,25 +1,41 @@
@@ -50,32 +69,53 @@ {title}
- - +
+

+ Options + + + + +

+
+ {#each poll.options as option, index} - +
+ + + + +
{/each}
-
- +
+
diff --git a/src/lib/components/Apps/components/Poll/components/FormSection.svelte b/src/lib/components/Apps/components/Poll/components/FormSection.svelte deleted file mode 100644 index 855b2aec..00000000 --- a/src/lib/components/Apps/components/Poll/components/FormSection.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -
- {#if label} -

- {label} - - {#if typeof handleLabelButton === 'function'} - - {/if} -

- {/if} - -
- {#if type === 'text'} - - {/if} - - {#if type === 'date'} - - {/if} - - {#if typeof handleInputButton === 'function'} - - {/if} -
-
diff --git a/src/lib/components/Apps/components/Poll/components/Poll.svelte b/src/lib/components/Apps/components/Poll/components/Poll.svelte index 5c54c9ee..c3a31bfc 100644 --- a/src/lib/components/Apps/components/Poll/components/Poll.svelte +++ b/src/lib/components/Apps/components/Poll/components/Poll.svelte @@ -1,48 +1,56 @@ - @@ -59,9 +67,9 @@ {#each selectedOptionToView.selectedBy as user}
- +

- {user.label} + {user.fullname}

{/each} @@ -73,11 +81,7 @@ {poll.question}

- Created by @{poll.author.label} + Created by @{poll.author.username}

@@ -85,9 +89,14 @@
- - +
{/if} @@ -132,7 +141,7 @@
{#each option.selectedBy.slice(0, 3) as user} - + {/each}
@@ -140,21 +149,26 @@ {:else} {#each poll.options as option, index} - +
+ (viewResult = !viewResult)} + /> +
{/if} {/if} diff --git a/src/lib/components/Apps/components/Poll/components/Tabs.svelte b/src/lib/components/Apps/components/Poll/components/Tabs.svelte index 1d8596d7..6e82b08e 100644 --- a/src/lib/components/Apps/components/Poll/components/Tabs.svelte +++ b/src/lib/components/Apps/components/Poll/components/Tabs.svelte @@ -1,18 +1,34 @@ - -
- {#each tabs as tab} - - {/each} +
+
+ {#each tabs as tab} + + {/each} +
+ + + + + +
diff --git a/src/lib/components/Apps/components/Poll/index.svelte b/src/lib/components/Apps/components/Poll/index.svelte index 667d26e5..a1b64513 100644 --- a/src/lib/components/Apps/components/Poll/index.svelte +++ b/src/lib/components/Apps/components/Poll/index.svelte @@ -1,152 +1,251 @@ @@ -155,46 +254,35 @@
-
- {#if shouldCreatePoll} - - {:else} +
+ {#if openCreatePollForm} + (openCreatePollForm = !openCreatePollForm)} + bind:isSaving={isCreating} + /> + {:else if currentGroupMember}
- + (openCreatePollForm = !openCreatePollForm)} /> - {#if selectedTab === tabs[0].value} -
- -
- {/if} - - {#each polls as poll, index} - {#if selectedTab === tabs[0].value && poll.author.value === userId} - - {:else if selectedTab === tabs[1].value && poll.author.value !== userId} - - {/if} + {#if isLoading} + Loading... {:else} -
-

No polls to display

-
- {/each} + {#each selectedTab === tabs[0].value ? activePolls : expiredPolls as poll} + + {:else} +
+

No polls to display

+
+ {/each} + {/if}
{/if}
diff --git a/src/lib/components/Apps/components/Poll/service.ts b/src/lib/components/Apps/components/Poll/service.ts index 2eec29c1..516e35a5 100644 --- a/src/lib/components/Apps/components/Poll/service.ts +++ b/src/lib/components/Apps/components/Poll/service.ts @@ -1,87 +1,141 @@ import { snackbar } from '$lib/components/Snackbar/store'; -import type { PollType } from './types'; +import type { PollType, PollOptionType, FetchPollsResponse } from './types'; import { getSupabase } from '$lib/utils/functions/supabase'; -import type { GroupPerson } from '$lib/utils/types'; +import { polls } from './store'; const supabase = getSupabase(); -export async function createPoll(poll: PollType, groupmember: GroupPerson, courseId: string) { - if (!groupmember) return; - - const { data: newPolls, error } = await supabase - .from('apps_poll') - .insert({ - question: poll.question, - authorId: groupmember.id, - isPublic: poll.isPublic, - status: poll.question, - expiration: poll.expiration, - courseId - }) - .select(); - - if (!newPolls || error) { - snackbar.error('Error creating poll'); - console.error(error); - return; - } - - const [newPoll] = newPolls; - - if (!newPoll.id) return; - poll = { - poll, - ...newPoll - }; - - poll.options = await Promise.all( - poll.options.map(async (option) => { - const { data: newPollOptions, error: newPollOptionsError } = await supabase - .from('apps_poll_option') - .insert({ - poll_id: newPoll.id, - label: option.label - }) - .select(); - - if (!newPollOptions || newPollOptionsError) { - return option; - } - - const [newPollOption] = newPollOptions; - option.id = newPollOption.id; - - return option; - }) - ); - - return poll; -} - export async function fetchPolls(courseId: string) { - const {} = await supabase - .from('app_poll') + return await supabase + .from('apps_poll') .select( ` - id, - question, - isPublic, - status, - expiration - groupmember:authorId( - profile!inner( - id, + *, + created_at, + author:groupmember( + profile( + username, fullname, avatar_url ) - ) - apps_poll_option!inner( + ), + options: apps_poll_option ( id, label, + submissions:apps_poll_submission( + selectedBy:groupmember( + id, + profile( + username, + fullname, + avatar_url + ) + ) + ) ) ` ) - .match({ - courseId + .match({ courseId }) + .order('created_at', { ascending: false }) + .returns(); +} + +export const updatePollStatus = async (pollId: string, status: PollType['status']) => { + const { error } = await supabase.from('apps_poll').update({ status }).match({ id: pollId }); + + if (error) { + console.log(error); + snackbar.error('An error occurred while updating poll status'); + return; + } +}; + +export const togglePollSubmission = async ( + pollId: PollType['id'], + pollOptionId: PollOptionType['id'], + groupmemberId: string, + add: boolean +) => { + if (add) { + const { error } = await supabase.from('apps_poll_submission').insert({ + poll_id: pollId, + poll_option_id: pollOptionId, + selected_by_id: groupmemberId }); + if (error) { + console.log(error); + snackbar.error('An error occurred while submitting poll'); + return; + } + } else { + const { error } = await supabase.from('apps_poll_submission').delete().match({ + poll_id: pollId, + poll_option_id: pollOptionId, + selected_by_id: groupmemberId + }); + + if (error) { + console.log(error); + snackbar.error('An error occurred while submitting poll'); + return; + } + } +}; + +function isOptionSelectedByCurrentUser(option: PollOptionType, groupmemberId: string) { + return option.selectedBy.some((gmember) => gmember.id === groupmemberId); +} + +export function handleVote(pollId: string, groupmemberId: string, author: PollType['author']) { + return (optionId: string) => { + // we have the pollId and the optionId + polls.update((_polls) => { + return [ + ..._polls.map((poll) => { + // Prevent user from voting on their own poll + // if (poll.author.id === currentGroupMember?.id) return poll; + + // Prevent user from voting on a poll that has expired + if (poll.expiration && new Date(poll.expiration) < new Date()) return poll; + + if (poll.id === pollId) { + // Prevent user from voting on a poll that the status is not published + if (poll.status !== 'published') { + snackbar.info('Poll is not published yet'); + return poll; + } + + // Prevent user from voting twice + if ( + poll.options.some((option) => isOptionSelectedByCurrentUser(option, groupmemberId)) + ) { + snackbar.info('You can only vote once'); + return poll; + } + + poll.options = poll.options.map((option) => { + const isSelected = isOptionSelectedByCurrentUser(option, groupmemberId); + + // If user has not voted on this option, add their vote + if (option.id === optionId && !isSelected) { + option.selectedBy = [...option.selectedBy, author]; + + togglePollSubmission(pollId, optionId, groupmemberId, true); + } + // else if (isSelected) { + // // If user has voted on this option, remove their vote + // option.selectedBy = option.selectedBy.filter( + // (gmember) => gmember.id !== groupmemberId + // ); + // } + + return option; + }); + } + + return poll; + }) + ]; + }); + }; } diff --git a/src/lib/components/Apps/components/Poll/store.ts b/src/lib/components/Apps/components/Poll/store.ts new file mode 100644 index 00000000..bac5c500 --- /dev/null +++ b/src/lib/components/Apps/components/Poll/store.ts @@ -0,0 +1,4 @@ +import { writable, type Writable } from 'svelte/store'; +import type { PollType } from './types'; + +export const polls: Writable = writable([]); diff --git a/src/lib/components/Apps/components/Poll/types.ts b/src/lib/components/Apps/components/Poll/types.ts index f7f7dd60..29ab826a 100644 --- a/src/lib/components/Apps/components/Poll/types.ts +++ b/src/lib/components/Apps/components/Poll/types.ts @@ -1,14 +1,68 @@ -export type Poll = { - question: string; - authorId: string; // groupmember_id - isPublic: boolean; - expiration: string | Date; - options: { - label: string; - selectedBy: { - id: string; - fullname: string; - avatarUrl: string; - }[]; +export type PollOptionType = { + id: string; + label: string; + selectedBy: { + id: string; + username: string; + fullname: string; + avatarUrl: string; }[]; }; + +export type PollType = { + courseId: string; + id: string; + question: string; + author: { + id: string; + username: string; + fullname: string; + avatarUrl: string; + }; + status: string; + expiration: string | Date; + options: PollOptionType[]; +}; + +export type TabsType = { + label: string; + value: number; + number: number; +}[]; + +export type PollOptionsSubmissionType = { + poll_id: string; + poll_option_id: string; + selected_by_id: string; +}; + +export type FetchPollsResponse = { + id: string; + courseId: string; + expiration: string; + authorId: string; + status: string; + question: string; + created_at: string; + author: { + profile: { + username: string; + fullname: string; + avatar_url: string; + }; + }; + options: { + id: string; + label: string; + submissions: { + selectedBy: { + id: string; + profile: { + username: string; + fullname: string; + avatar_url: string; + }; + }; + }[]; + }[]; +}[]; diff --git a/src/lib/components/Apps/components/Poll/utils.ts b/src/lib/components/Apps/components/Poll/utils.ts new file mode 100644 index 00000000..51fc3292 --- /dev/null +++ b/src/lib/components/Apps/components/Poll/utils.ts @@ -0,0 +1,31 @@ +import type { PollType, FetchPollsResponse } from './types'; + +export const getPollsData = (data: FetchPollsResponse, isStudent: boolean) => { + const pollsData: PollType[] = data.map((appsPollItem) => ({ + id: appsPollItem.id, + courseId: appsPollItem.courseId, + expiration: appsPollItem.expiration, + status: appsPollItem.status, + question: appsPollItem.question, + author: { + id: appsPollItem.authorId, + username: appsPollItem.author.profile.username, + fullname: appsPollItem.author.profile.fullname, + avatarUrl: appsPollItem.author.profile.avatar_url + }, + options: appsPollItem.options.map((option) => ({ + id: option.id, + label: option.label, + selectedBy: option.submissions.map((submission) => { + return { + id: submission.selectedBy.id, + username: submission.selectedBy.profile.username, + fullname: submission.selectedBy.profile.fullname, + avatarUrl: submission.selectedBy.profile.avatar_url + }; + }) + })) + })); + + return pollsData.filter((p) => (isStudent ? p.status === 'published' : true)); +}; diff --git a/src/lib/components/Apps/index.svelte b/src/lib/components/Apps/index.svelte index 198f2416..0d300946 100644 --- a/src/lib/components/Apps/index.svelte +++ b/src/lib/components/Apps/index.svelte @@ -1,6 +1,5 @@ diff --git a/src/lib/components/Form/DateTime.svelte b/src/lib/components/Form/DateTime.svelte index d9ce119b..b9622d33 100644 --- a/src/lib/components/Form/DateTime.svelte +++ b/src/lib/components/Form/DateTime.svelte @@ -1,19 +1,24 @@ - diff --git a/src/lib/components/Form/TextField.svelte b/src/lib/components/Form/TextField.svelte index ef31b462..3b8b59f8 100644 --- a/src/lib/components/Form/TextField.svelte +++ b/src/lib/components/Form/TextField.svelte @@ -23,17 +23,19 @@ export let helperMessage = ''; export let autoComplete = true; export let onChange = () => {}; // This is to know if element is 'dirty' - export let onInputChange = (e) => {}; + export let onInputChange = () => {}; - let ref; - let fieldNode; + let ref: HTMLInputElement | undefined = undefined; + let fieldNode: HTMLInputElement | undefined = undefined; let focusClass = ''; - function typeAction(node) { - node.type = type; + function typeAction(node: HTMLInputElement | undefined) { + if (!node) return; + + node.type = type || 'text'; if (isRequired) { - node.required = ''; + node.required = true; } fieldNode = node; @@ -45,7 +47,7 @@ } onMount(() => { - if (autoFocus) { + if (autoFocus && ref) { ref.focus(); } diff --git a/src/lib/components/LMS/components/Learning.svelte b/src/lib/components/LMS/components/Learning.svelte index 100036fe..c953162b 100644 --- a/src/lib/components/LMS/components/Learning.svelte +++ b/src/lib/components/LMS/components/Learning.svelte @@ -14,17 +14,17 @@

Currently Learning

{#if $coursesInProgress.length > 0}
{#each $coursesInProgress as course}
- +

{course.title}

diff --git a/src/lib/components/Snackbar/store.js b/src/lib/components/Snackbar/store.js index 38a4f1b5..7a26c6be 100644 --- a/src/lib/components/Snackbar/store.js +++ b/src/lib/components/Snackbar/store.js @@ -27,6 +27,15 @@ export const snackbar = { store.message = message; store.severity = SNACKBAR_SEVERITY.ERROR; + return store; + }); + }, + info(message = 'Update') { + snackbarStore.update((store) => { + store.open = true; + store.message = message; + store.severity = SNACKBAR_SEVERITY.INFO; + return store; }); } diff --git a/src/lib/utils/services/courses/index.ts b/src/lib/utils/services/courses/index.ts index f5a9f2e4..c312a413 100644 --- a/src/lib/utils/services/courses/index.ts +++ b/src/lib/utils/services/courses/index.ts @@ -51,7 +51,8 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug'] profile:teacher_id(id, avatar_url, fullname), lesson_completion(id, profile_id, is_complete) ), - attendance:group_attendance(*) + attendance:group_attendance(*), + polls:apps_poll(status) ` ) .match(match) diff --git a/src/lib/utils/types/index.ts b/src/lib/utils/types/index.ts index f8af8033..0bc85b89 100644 --- a/src/lib/utils/types/index.ts +++ b/src/lib/utils/types/index.ts @@ -189,6 +189,7 @@ export interface Course { id: number; }[]; lessons?: Lesson[]; + polls: { status: string }[]; } export interface Groupmember { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b827a648..c9d7e276 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,7 +7,7 @@ import { getSupabase } from '$lib/utils/functions/supabase'; import { validateEmail } from '$lib/utils/functions/validateEmail'; - let email; + let email = ''; let isAdding = false; let success = false; @@ -42,7 +42,7 @@ setTimeout(() => { isAdding = false; success = false; - email = null; + email = ''; }, 5000); } diff --git a/src/routes/courses/[id]/+layout.svelte b/src/routes/courses/[id]/+layout.svelte new file mode 100644 index 00000000..06556286 --- /dev/null +++ b/src/routes/courses/[id]/+layout.svelte @@ -0,0 +1,52 @@ + + + diff --git a/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte b/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte index ccedcb52..908d72d7 100644 --- a/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte +++ b/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte @@ -18,7 +18,7 @@ import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte'; import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte'; import MODES from '$lib/utils/constants/mode'; - import { setCourse, course } from '$lib/components/Course/store'; + import { course } from '$lib/components/Course/store'; import Download from 'carbon-icons-svelte/lib/Download.svelte'; import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte'; import { apps } from '$lib/components/Apps/store'; @@ -72,23 +72,24 @@ isFetching = false; } - async function markLessonComplete() { + async function markLessonComplete(lessonId: string) { let found = false; isMarkingComplete = true; let completion: LessonCompletion = { is_complete: true, profile_id: $profile.id || '', - lesson_id: $lesson.id || '', + lesson_id: lessonId, created_at: new Date().toDateString(), updated_at: new Date().toDateString() }; const completions: LessonCompletion[] = $lesson.lesson_completion.map((_completion) => { if (_completion.profile_id === $profile.id) { found = true; - _completion.is_complete = !_completion.is_complete; - - completion = _completion; + completion = { + ...completion, + is_complete: _completion.is_complete + }; } return _completion; @@ -324,7 +325,11 @@ {$lesson.totalComments} -
-
-
+
+
-
+

Your Progress

student Learning score @@ -100,7 +100,7 @@ {/if}

{progressPercentage} %

diff --git a/src/routes/org/[slug]/+page.svelte b/src/routes/org/[slug]/+page.svelte index 3fe1de22..1592487c 100644 --- a/src/routes/org/[slug]/+page.svelte +++ b/src/routes/org/[slug]/+page.svelte @@ -213,7 +213,7 @@ >

- Thank you for what you do ❤️. You are changing lives, one classroom at a time, thanks to + Thank you for what you do ❤️. You are changing lives one classroom at a time and thanks to you, the world is a better place. 😊