fix: move feed state to a store and create feed loading UI (#189)
* style: Improved font size of the answers in the FAQ section of the landing page * feat: move all email to defer queue (#186) * fix: use relative part for defer func #185 * chore: move sendEmail into defer * fix: remove anon function to defer #185 * feat: implement batch email sending with defer.run #185 * chore: debug sendEmails #185 * chore: add robots.txt * chore: use promise.all in sendEmails * chore: debug defer sendEmails #185 * chore: try defer sendEmail without loop #105 * chore: comment code * chore: change loop style #185 * chore: use promise.all for vercel edge #185 * Moved the feed state to a store and added a loader * feat: add await to sendEmailNotification #185 * chore: use sendEmails #185 * fix: debug sendEmails #185 * fix: email not updating in LMS * fix: remove early return in sendEmails #185 * fixed the edit bug and moved the loader state to the file * added a loading state for the post button * change isDisabled to isLoading --------- Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
@@ -20,68 +20,65 @@
|
|||||||
export let editFeed: Feed;
|
export let editFeed: Feed;
|
||||||
|
|
||||||
let newPost = '';
|
let newPost = '';
|
||||||
|
let isLoading = false;
|
||||||
let createdFeed;
|
let createdFeed;
|
||||||
|
|
||||||
const onPost = async () => {
|
const onPost = async () => {
|
||||||
if (!newPost) return;
|
if (!newPost) return;
|
||||||
|
isLoading = true;
|
||||||
// Edit state
|
|
||||||
if (edit) {
|
|
||||||
onEdit(editFeed.id, newPost);
|
|
||||||
|
|
||||||
snackbar.success('Feed edited successfully');
|
|
||||||
|
|
||||||
edit = false;
|
|
||||||
newPost = '';
|
|
||||||
$isNewFeedModal.open = false;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create state
|
|
||||||
try {
|
try {
|
||||||
const {
|
if (edit) {
|
||||||
response: { data }
|
onEdit(editFeed.id, newPost);
|
||||||
} = await createNewFeed({
|
snackbar.success('Feed edited successfully');
|
||||||
content: newPost,
|
|
||||||
author_id: author.id,
|
|
||||||
course_id: courseId,
|
|
||||||
reaction: {
|
|
||||||
smile: [],
|
|
||||||
thumbsup: [],
|
|
||||||
thumbsdown: [],
|
|
||||||
clap: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data) return;
|
edit = false;
|
||||||
|
newPost = '';
|
||||||
|
$isNewFeedModal.open = false;
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
response: { data }
|
||||||
|
} = await createNewFeed({
|
||||||
|
content: newPost,
|
||||||
|
author_id: author.id,
|
||||||
|
course_id: courseId,
|
||||||
|
reaction: {
|
||||||
|
smile: [],
|
||||||
|
thumbsup: [],
|
||||||
|
thumbsdown: [],
|
||||||
|
clap: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
createdFeed = data[0];
|
if (!data) return;
|
||||||
|
|
||||||
|
createdFeed = data[0];
|
||||||
|
|
||||||
|
if (!createdFeed) return;
|
||||||
|
|
||||||
|
onSave({
|
||||||
|
id: createdFeed.id,
|
||||||
|
content: newPost,
|
||||||
|
author: {
|
||||||
|
profile: { ...author }
|
||||||
|
},
|
||||||
|
created_at: createdFeed.created_at,
|
||||||
|
comment: [],
|
||||||
|
reaction: createdFeed.reaction,
|
||||||
|
isPinned: false
|
||||||
|
});
|
||||||
|
|
||||||
|
snackbar.success('New feed added successfully');
|
||||||
|
triggerSendEmail(NOTIFICATION_NAME.NEWSFEED, {
|
||||||
|
authorId: createdFeed.author_id,
|
||||||
|
feedId: createdFeed.id
|
||||||
|
});
|
||||||
|
resetEditor();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return snackbar.error('An error occurred while creating feed');
|
snackbar.error('An error occurred while ' + (edit ? 'editing feed' : 'creating feed'));
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!createdFeed) return;
|
|
||||||
|
|
||||||
onSave({
|
|
||||||
id: createdFeed.id,
|
|
||||||
content: newPost,
|
|
||||||
author: {
|
|
||||||
profile: { ...author }
|
|
||||||
},
|
|
||||||
created_at: createdFeed.created_at,
|
|
||||||
comment: [],
|
|
||||||
reaction: createdFeed.reaction,
|
|
||||||
isPinned: false
|
|
||||||
});
|
|
||||||
|
|
||||||
snackbar.success('New feed added successfully');
|
|
||||||
triggerSendEmail(NOTIFICATION_NAME.NEWSFEED, {
|
|
||||||
authorId: createdFeed.author_id,
|
|
||||||
feedId: createdFeed.id
|
|
||||||
});
|
|
||||||
|
|
||||||
resetEditor();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetEditor = () => {
|
const resetEditor = () => {
|
||||||
@@ -110,12 +107,8 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-end py-2">
|
<div class="flex items-center justify-end py-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<PrimaryButton
|
<PrimaryButton label="Cancel" variant={VARIANTS.OUTLINED} onClick={resetEditor} />
|
||||||
label="Cancel"
|
<PrimaryButton {isLoading} label="Post" onClick={onPost} />
|
||||||
variant={VARIANTS.OUTLINED}
|
|
||||||
onClick={() => ($isNewFeedModal.open = false)}
|
|
||||||
/>
|
|
||||||
<PrimaryButton label="Post" onClick={onPost} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-5 border border-gray-300 p-4 rounded-md mb-7 max-w-3xl">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<SkeletonPlaceholder style="height: 2rem; width: 2rem;" class="rounded-full overflow-hidden" />
|
||||||
|
<SkeletonText width="30%" />
|
||||||
|
</div>
|
||||||
|
<SkeletonText paragraph lines={3} />
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import type { Feed } from '$lib/utils/types/feed';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const isNewFeedModal = writable({
|
export const isNewFeedModal = writable({
|
||||||
open: false
|
open: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const newsFeed = writable<Feed[]>([]);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
toggleFeedIsPinned,
|
toggleFeedIsPinned,
|
||||||
fetchNewsFeedReaction
|
fetchNewsFeedReaction
|
||||||
} from '$lib/utils/services/newsfeed';
|
} from '$lib/utils/services/newsfeed';
|
||||||
|
import { newsFeed } from '$lib/components/Course/components/NewsFeed/store';
|
||||||
import Box from '$lib/components/Box/index.svelte';
|
import Box from '$lib/components/Box/index.svelte';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
@@ -28,11 +29,12 @@
|
|||||||
NOTIFICATION_NAME,
|
NOTIFICATION_NAME,
|
||||||
triggerSendEmail
|
triggerSendEmail
|
||||||
} from '$lib/utils/services/notification/notification';
|
} from '$lib/utils/services/notification/notification';
|
||||||
|
import NewsFeedLoader from '$lib/components/Course/components/NewsFeed/NewsFeedLoader.svelte';
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
let createdComment;
|
let createdComment;
|
||||||
let edit = false;
|
let edit = false;
|
||||||
|
let isLoading = false;
|
||||||
let editFeed: Feed;
|
let editFeed: Feed;
|
||||||
let author = {
|
let author = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -40,14 +42,14 @@
|
|||||||
fullname: '',
|
fullname: '',
|
||||||
avatar_url: ''
|
avatar_url: ''
|
||||||
};
|
};
|
||||||
let newsFeed: Feed[] = [];
|
|
||||||
let query = new URLSearchParams($page.url.search);
|
let query = new URLSearchParams($page.url.search);
|
||||||
let feedId = query.get('feedId') || '';
|
let feedId = query.get('feedId') || '';
|
||||||
|
|
||||||
const onEdit = (id: string, content: string) => {
|
const onEdit = (id: string, content: string) => {
|
||||||
handleEditFeed(id, content);
|
handleEditFeed(id, content);
|
||||||
|
|
||||||
newsFeed = newsFeed.map((feed) => {
|
$newsFeed = $newsFeed.map((feed) => {
|
||||||
if (feed.id === id) {
|
if (feed.id === id) {
|
||||||
return { ...feed, content: content };
|
return { ...feed, content: content };
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,7 @@
|
|||||||
|
|
||||||
const deleteComment = (id: string) => {
|
const deleteComment = (id: string) => {
|
||||||
deleteNewsFeedComment(id);
|
deleteNewsFeedComment(id);
|
||||||
newsFeed = newsFeed.flatMap((feed) => ({
|
$newsFeed = $newsFeed.flatMap((feed) => ({
|
||||||
...feed,
|
...feed,
|
||||||
comment: feed.comment.filter((comment) => comment.id !== id)
|
comment: feed.comment.filter((comment) => comment.id !== id)
|
||||||
}));
|
}));
|
||||||
@@ -71,7 +73,7 @@
|
|||||||
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const reactedFeed = data || newsFeed.find((feed) => feed.id === feedId);
|
const reactedFeed = data || $newsFeed.find((feed) => feed.id === feedId);
|
||||||
|
|
||||||
let reactedAuthorIds: string[] = reactedFeed.reaction[reactionType];
|
let reactedAuthorIds: string[] = reactedFeed.reaction[reactionType];
|
||||||
|
|
||||||
@@ -99,7 +101,7 @@
|
|||||||
return snackbar.error('An error occurred while reacting to news feed');
|
return snackbar.error('An error occurred while reacting to news feed');
|
||||||
}
|
}
|
||||||
|
|
||||||
newsFeed = newsFeed.map((feed) => {
|
$newsFeed = $newsFeed.map((feed) => {
|
||||||
if (feed.id === feedId) {
|
if (feed.id === feedId) {
|
||||||
feed.reaction = reactedFeed.reaction;
|
feed.reaction = reactedFeed.reaction;
|
||||||
}
|
}
|
||||||
@@ -129,7 +131,7 @@
|
|||||||
comment
|
comment
|
||||||
});
|
});
|
||||||
|
|
||||||
newsFeed = newsFeed.map((feed) => {
|
$newsFeed = $newsFeed.map((feed) => {
|
||||||
if (feed.id === feedId) {
|
if (feed.id === feedId) {
|
||||||
const newComment = {
|
const newComment = {
|
||||||
id: createdComment.id,
|
id: createdComment.id,
|
||||||
@@ -160,7 +162,7 @@
|
|||||||
return snackbar.error('Failed to toggle pinned feed');
|
return snackbar.error('Failed to toggle pinned feed');
|
||||||
}
|
}
|
||||||
|
|
||||||
newsFeed = newsFeed.map((feed) => {
|
$newsFeed = $newsFeed.map((feed) => {
|
||||||
if (feed.id === feedId) {
|
if (feed.id === feedId) {
|
||||||
snackbar.success(`${newIsPinned ? 'Pinned' : 'Unpinned'} Successfully`);
|
snackbar.success(`${newIsPinned ? 'Pinned' : 'Unpinned'} Successfully`);
|
||||||
|
|
||||||
@@ -175,28 +177,31 @@
|
|||||||
const deleteFeed = (id: string) => {
|
const deleteFeed = (id: string) => {
|
||||||
deleteNewsFeed(id);
|
deleteNewsFeed(id);
|
||||||
|
|
||||||
const deletedFeed = newsFeed.filter((feed) => feed.id !== id);
|
const deletedFeed = $newsFeed.filter((feed) => feed.id !== id);
|
||||||
snackbar.success('Feed deleted successfully');
|
snackbar.success('Feed deleted successfully');
|
||||||
|
|
||||||
newsFeed = deletedFeed;
|
$newsFeed = deletedFeed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initNewsFeed = async (courseId: string) => {
|
const initNewsFeed = async (courseId: string) => {
|
||||||
if (!courseId) return;
|
if (!courseId) return;
|
||||||
|
try {
|
||||||
const { data, error } = await fetchNewsFeeds(courseId);
|
isLoading = true;
|
||||||
|
const { data, error } = await fetchNewsFeeds(courseId);
|
||||||
if (error) {
|
if (error) {
|
||||||
snackbar.error('Failed to fetch news feeds');
|
snackbar.error('Failed to fetch news feeds');
|
||||||
return;
|
}
|
||||||
|
if (data) {
|
||||||
|
$newsFeed = data.map((feedItem) => ({
|
||||||
|
...feedItem,
|
||||||
|
isPinned: feedItem.is_pinned
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
snackbar.error('An error occured while fetching feed');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
newsFeed = data.map((feedItem) => ({
|
|
||||||
...feedItem,
|
|
||||||
isPinned: feedItem.is_pinned
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function setAuthor(groups, profileId) {
|
function setAuthor(groups, profileId) {
|
||||||
@@ -214,7 +219,7 @@
|
|||||||
|
|
||||||
$: setAuthor($group, $profile.id);
|
$: setAuthor($group, $profile.id);
|
||||||
|
|
||||||
$: newsFeed = newsFeed.sort((a, b) => Number(b.isPinned) - Number(a.isPinned));
|
$: $newsFeed = $newsFeed.sort((a, b) => Number(b.isPinned) - Number(a.isPinned));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CourseContainer bind:courseId={data.courseId}>
|
<CourseContainer bind:courseId={data.courseId}>
|
||||||
@@ -234,13 +239,18 @@
|
|||||||
bind:edit
|
bind:edit
|
||||||
bind:editFeed
|
bind:editFeed
|
||||||
onSave={(newFeed) => {
|
onSave={(newFeed) => {
|
||||||
newsFeed = [newFeed, ...newsFeed];
|
$newsFeed = [newFeed, ...$newsFeed];
|
||||||
}}
|
}}
|
||||||
{onEdit}
|
{onEdit}
|
||||||
/>
|
/>
|
||||||
</RoleBasedSecurity>
|
</RoleBasedSecurity>
|
||||||
|
{#if isLoading}
|
||||||
{#if !newsFeed.length}
|
<div>
|
||||||
|
<NewsFeedLoader />
|
||||||
|
<NewsFeedLoader />
|
||||||
|
<NewsFeedLoader />
|
||||||
|
</div>
|
||||||
|
{:else if !$newsFeed.length}
|
||||||
<Box>
|
<Box>
|
||||||
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
||||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||||
@@ -251,7 +261,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
{:else}
|
{:else}
|
||||||
{#each newsFeed as feed}
|
{#each $newsFeed as feed}
|
||||||
{#if feed.isPinned}
|
{#if feed.isPinned}
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
<PinFilled size={16} />
|
<PinFilled size={16} />
|
||||||
|
|||||||
Reference in New Issue
Block a user