feat: migrate course UI from supabase (#615)
* chore: fix storybook build * chore: implement theming * community list not showing properly * refactor: migrate Chip component to @cio/ui/custom/chip - Updated imports for the Chip component across multiple files to use the new location in the @cio/ui package. - Replaced the previous Chip implementation with a new version that utilizes a Badge for better styling and functionality. - Adjusted related components to ensure consistent usage of the updated Chip component. * fix: align course sidebar design to match organization design (#611) * fix: align course sidebar design to match organization design * feat: add hideSearch prop to AppHeader and update sidebar layout --------- Co-authored-by: Rotimi Best <best@classroomio.com> * refactor: replace PrimaryButton with Button component across multiple files - Updated various components to use the new Button component from @cio/ui/base/button instead of the deprecated PrimaryButton. - Ensured consistent button usage throughout the application for improved styling and functionality. - Removed the PrimaryButton component and its associated constants from the codebase. * refactor: standardize button and input usage across components - Replaced instances of deprecated components such as TextField, TextArea, and IconButton with their updated counterparts (InputField, TextareaField, and updated IconButton). - Ensured consistent event handling by changing `onClick` to `onclick` for better alignment with Svelte's event binding conventions. - Removed unused components and streamlined imports to enhance code maintainability and readability. * chore: themeing, course card, use $features * refactor: centralize UI components and update modal implementations - Refactored various components to centralize UI elements, improving discoverability and maintainability. - Replaced deprecated modal components with the new Dialog component for consistent modal behavior across the application. - Removed unused components and streamlined imports to enhance code clarity and reduce complexity. * feat: add new documentation and refactor org and lms UI components - Introduced new documentation files for Auth Flow, Breadcrumbs Refactoring, Page and Route Creation, and Route Page Setup to enhance developer onboarding and reference. - Refactored various UI components, including the Course Sidebar and Lesson Modal, to improve layout and functionality. - Removed deprecated components and streamlined imports for better maintainability. - Updated translations across multiple languages to ensure consistency in user experience. * refactor: update UI components and improve styling consistency - Refactored various components to enhance styling consistency, including updates to button and text color classes. - Removed deprecated components and streamlined imports for better maintainability. - Replaced instances of PageBody with a more flexible Page component structure across multiple routes. - Updated translations and UI elements to align with the new design system. - Deleted unused components to reduce code complexity and improve clarity. * update output.css --------- Co-authored-by: Abdulmuizz Hamzat <103611443+canhamzacode@users.noreply.github.com>
This commit is contained in:
co-authored by
Abdulmuizz Hamzat
parent
e6a3cf9332
commit
9fb6c7cbaa
@@ -0,0 +1,289 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import NewFeedModal from '$lib/components/Course/components/NewsFeed/NewFeedModal.svelte';
|
||||
import NewsFeedCard from '$lib/components/Course/components/NewsFeed/NewsFeedCard.svelte';
|
||||
import NewsFeedLoader from '$lib/components/Course/components/NewsFeed/NewsFeedLoader.svelte';
|
||||
import { newsFeed } from '$lib/components/Course/components/NewsFeed/store';
|
||||
import { group } from '$lib/components/Course/store';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
createComment,
|
||||
deleteNewsFeed,
|
||||
deleteNewsFeedComment,
|
||||
fetchNewsFeedReaction,
|
||||
fetchNewsFeeds,
|
||||
handleEditFeed,
|
||||
toggleFeedIsPinned
|
||||
} from '$lib/utils/services/newsfeed';
|
||||
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Feed } from '$lib/utils/types/feed';
|
||||
import PinIcon from '@lucide/svelte/icons/pin';
|
||||
import { onMount } from 'svelte';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
import Empty from '@cio/ui/custom/empty/empty.svelte';
|
||||
import { BookIcon } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
let { courseId }: Props = $props();
|
||||
|
||||
let createdComment;
|
||||
let edit = $state(false);
|
||||
let isLoading = $state(false);
|
||||
let editFeed: Feed | null = $state(null);
|
||||
|
||||
let query = new URLSearchParams(page.url.search);
|
||||
let feedId = query.get('feedId') || '';
|
||||
|
||||
const author = $derived.by(() => {
|
||||
const groupMember = $group.people.find((person) => person.profile_id === $profile.id);
|
||||
|
||||
return {
|
||||
id: groupMember?.id || '',
|
||||
username: $profile.username || '',
|
||||
fullname: $profile.fullname || '',
|
||||
avatar_url: $profile.avatarUrl || ''
|
||||
};
|
||||
});
|
||||
|
||||
const onEdit = (id: string, content: string) => {
|
||||
handleEditFeed(id, content);
|
||||
|
||||
$newsFeed = $newsFeed.map((feed) => {
|
||||
if (feed.id === id) {
|
||||
return { ...feed, content: content };
|
||||
}
|
||||
|
||||
return feed;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteComment = (id: string) => {
|
||||
deleteNewsFeedComment(id);
|
||||
$newsFeed = $newsFeed.flatMap((feed) => ({
|
||||
...feed,
|
||||
comment: feed.comment.filter((comment) => comment.id !== id)
|
||||
}));
|
||||
|
||||
return snackbar.success('snackbar.course.success.comment_deleted');
|
||||
};
|
||||
|
||||
const addNewReaction = async (reactionType: string, feedId: string, authorId: string) => {
|
||||
const { data } = await fetchNewsFeedReaction(feedId);
|
||||
|
||||
if (!data) return;
|
||||
|
||||
const reactedFeed = data || $newsFeed.find((feed) => feed.id === feedId);
|
||||
|
||||
let reactedAuthorIds: string[] = reactedFeed.reaction[reactionType];
|
||||
|
||||
if (reactedAuthorIds.includes(authorId)) {
|
||||
reactedAuthorIds = reactedAuthorIds.filter((reactionAuthorId) => reactionAuthorId !== authorId);
|
||||
} else {
|
||||
reactedAuthorIds = [...reactedAuthorIds, authorId];
|
||||
}
|
||||
|
||||
reactedFeed.reaction = {
|
||||
...reactedFeed.reaction,
|
||||
[reactionType]: reactedAuthorIds
|
||||
};
|
||||
|
||||
const response = await supabase
|
||||
.from('course_newsfeed')
|
||||
.update({
|
||||
reaction: reactedFeed.reaction
|
||||
})
|
||||
.eq('id', feedId);
|
||||
|
||||
if (response.error) {
|
||||
return snackbar.error('snackbar.course.error.reaction_error');
|
||||
}
|
||||
|
||||
$newsFeed = $newsFeed.map((feed) => {
|
||||
if (feed.id === feedId) {
|
||||
feed.reaction = reactedFeed.reaction;
|
||||
}
|
||||
|
||||
return feed;
|
||||
});
|
||||
};
|
||||
|
||||
const addNewComment = async (comment: string, feedId: string, authorId: string) => {
|
||||
const { response } = await createComment({
|
||||
content: comment,
|
||||
author_id: authorId,
|
||||
course_newsfeed_id: feedId
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
return snackbar.error('snackbar.course.error.commenting_error');
|
||||
}
|
||||
|
||||
if (!response.data) return;
|
||||
|
||||
createdComment = response?.data[0];
|
||||
|
||||
triggerSendEmail(NOTIFICATION_NAME.NEWSFEED, {
|
||||
authorId: author.id,
|
||||
feedId: feedId,
|
||||
comment
|
||||
});
|
||||
|
||||
$newsFeed = $newsFeed.map((feed) => {
|
||||
if (feed.id === feedId) {
|
||||
const newComment = {
|
||||
id: createdComment.id,
|
||||
author: {
|
||||
profile: { ...author }
|
||||
},
|
||||
created_at: createdComment.created_at,
|
||||
content: comment
|
||||
};
|
||||
|
||||
snackbar.success('snackbar.course.success.comment_added');
|
||||
|
||||
return {
|
||||
...feed,
|
||||
comment: [...feed.comment, { ...newComment }]
|
||||
};
|
||||
}
|
||||
|
||||
return feed;
|
||||
});
|
||||
};
|
||||
|
||||
const onPin = async (feedId, isPinned) => {
|
||||
const newIsPinned = !isPinned;
|
||||
const { response } = await toggleFeedIsPinned(feedId, newIsPinned);
|
||||
|
||||
if (response.error) {
|
||||
return snackbar.error('snackbar.course.error.toggle_error');
|
||||
}
|
||||
|
||||
$newsFeed = $newsFeed.map((feed) => {
|
||||
if (feed.id === feedId) {
|
||||
snackbar.success(
|
||||
`${
|
||||
newIsPinned ? 'snackbar.course.success.pinned' : 'snackbar.course.success.unpinned'
|
||||
} snackbar.course.success.successfully`
|
||||
);
|
||||
|
||||
feed.isPinned = newIsPinned;
|
||||
return feed;
|
||||
}
|
||||
|
||||
return feed;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFeed = (id: string) => {
|
||||
deleteNewsFeed(id);
|
||||
|
||||
const deletedFeed = $newsFeed.filter((feed) => feed.id !== id);
|
||||
snackbar.success('snackbar.course.success.feed_delete_success');
|
||||
|
||||
$newsFeed = deletedFeed;
|
||||
};
|
||||
|
||||
const initNewsFeed = async (courseId: string) => {
|
||||
if (!courseId) return;
|
||||
try {
|
||||
isLoading = true;
|
||||
const { data, error } = await fetchNewsFeeds(courseId);
|
||||
if (error) {
|
||||
snackbar.error('snackbar.course.error.news_feed_fail');
|
||||
}
|
||||
if (data) {
|
||||
$newsFeed = data
|
||||
.map((feedItem) => ({
|
||||
...feedItem,
|
||||
isPinned: feedItem.is_pinned
|
||||
}))
|
||||
.sort((a, b) => Number(b.isPinned) - Number(a.isPinned));
|
||||
}
|
||||
} catch (error) {
|
||||
snackbar.error('snackbar.course.error.feed_fetch_error');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
function getPageRoles(org: AccountOrg) {
|
||||
const roles: number[] = [1, 2];
|
||||
|
||||
if (org.customization.course.newsfeed) {
|
||||
roles.push(3);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initNewsFeed(courseId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<RoleBasedSecurity
|
||||
allowedRoles={getPageRoles($currentOrg)}
|
||||
onDenied={() => {
|
||||
goto(`/courses/${courseId}/lessons?next=true`);
|
||||
}}
|
||||
>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<NewFeedModal
|
||||
{courseId}
|
||||
{author}
|
||||
bind:edit
|
||||
bind:editFeed
|
||||
onSave={(newFeed) => {
|
||||
$newsFeed = [newFeed, ...$newsFeed];
|
||||
}}
|
||||
{onEdit}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
{#if isLoading}
|
||||
<div>
|
||||
<NewsFeedLoader />
|
||||
<NewsFeedLoader />
|
||||
<NewsFeedLoader />
|
||||
</div>
|
||||
{:else if !$newsFeed.length}
|
||||
<Empty
|
||||
title={$t('course.navItem.news_feed.body_header')}
|
||||
description={$t('course.navItem.news_feed.body_content')}
|
||||
icon={BookIcon}
|
||||
class="h-full"
|
||||
></Empty>
|
||||
{:else}
|
||||
{#each $newsFeed as feed}
|
||||
{#if feed.isPinned}
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<PinIcon size={16} class="filled" />
|
||||
|
||||
<p class="text-sm">{$t('course.navItem.news_feed.pinned')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<NewsFeedCard
|
||||
{feed}
|
||||
{deleteFeed}
|
||||
{addNewComment}
|
||||
{deleteComment}
|
||||
{addNewReaction}
|
||||
{onPin}
|
||||
{author}
|
||||
bind:edit
|
||||
bind:editFeed
|
||||
isActive={feedId === feed.id}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</RoleBasedSecurity>
|
||||
|
||||
Reference in New Issue
Block a user