mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add updates functionality to app
This commit is contained in:
parent
f0e5fa925d
commit
8a2ccb2534
6 changed files with 147 additions and 53 deletions
|
|
@ -167,7 +167,7 @@ async def api_get_course_updates(
|
|||
|
||||
return await get_updates_by_course_uuid(request, course_uuid, current_user, db_session)
|
||||
|
||||
@router.post("/{course_uuid}/update")
|
||||
@router.post("/{course_uuid}/updates")
|
||||
async def api_create_course_update(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from typing import List
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
from sqlmodel import Session, col, select
|
||||
from src.db.course_updates import (
|
||||
CourseUpdate,
|
||||
CourseUpdateCreate,
|
||||
|
|
@ -89,7 +89,6 @@ async def update_update(
|
|||
if value is not None:
|
||||
setattr(update, key, value)
|
||||
|
||||
|
||||
db_session.add(update)
|
||||
|
||||
db_session.commit()
|
||||
|
|
@ -142,7 +141,11 @@ async def get_updates_by_course_uuid(
|
|||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
statement = select(CourseUpdate).where(CourseUpdate.course_id == course.id)
|
||||
statement = (
|
||||
select(CourseUpdate)
|
||||
.where(CourseUpdate.course_id == course.id)
|
||||
.order_by(col(CourseUpdate.creation_date).desc())
|
||||
) # https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors
|
||||
updates = db_session.exec(statement).all()
|
||||
|
||||
return [CourseUpdateRead(**update.model_dump()) for update in updates]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { ArrowRight, Check, File, Sparkles, Video } from 'lucide-react'
|
|||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import UserAvatar from '@components/Objects/UserAvatar'
|
||||
import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates'
|
||||
import { CourseProvider } from '@components/Contexts/CourseContext'
|
||||
|
||||
const CourseClient = (props: any) => {
|
||||
const [user, setUser] = useState<any>({})
|
||||
|
|
@ -75,7 +76,9 @@ const CourseClient = (props: any) => {
|
|||
<h1 className="text-3xl -mt-3 font-bold">{course.name}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<CourseProvider courseuuid={course.course_uuid}>
|
||||
<CourseUpdates />
|
||||
</CourseProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { PencilLine, Rss } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { PencilLine, Rss, TentTree } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
|
|
@ -9,8 +9,19 @@ import FormLayout, {
|
|||
Input,
|
||||
Textarea,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import useAdminStatus from '@components/Hooks/useAdminStatus'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { createCourseUpdate, deleteCourseUpdate } from '@services/courses/updates'
|
||||
import toast from 'react-hot-toast'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
|
||||
function CourseUpdates() {
|
||||
const course = useCourse() as any;
|
||||
const { data: updates } = useSWR(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`, swrFetcher)
|
||||
const [isModelOpen, setIsModelOpen] = React.useState(false)
|
||||
|
||||
function handleModelOpen() {
|
||||
|
|
@ -18,17 +29,16 @@ function CourseUpdates() {
|
|||
}
|
||||
|
||||
// if user clicks outside the model, close the model
|
||||
React.useEffect(() => {
|
||||
React.useLayoutEffect(() => {
|
||||
function handleClickOutside(event: any) {
|
||||
if (event.target.closest('.bg-white') === null) {
|
||||
setIsModelOpen(false)
|
||||
console.log(event.target.id)
|
||||
if (event.target.closest('.bg-white') || event.target.id === 'delete-update-button') return;
|
||||
setIsModelOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }} className='bg-white hover:bg-neutral-50 transition-all ease-linear nice-shadow rounded-full z-20 px-5 py-1'>
|
||||
|
|
@ -36,7 +46,7 @@ function CourseUpdates() {
|
|||
<div><Rss size={16} /> </div>
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<span>Updates</span>
|
||||
<span className='text-xs px-2 font-bold py-1 rounded-full bg-rose-100 text-rose-900'>5</span>
|
||||
{updates && <span className='text-xs px-2 font-bold py-0.5 rounded-full bg-rose-100 text-rose-900'>{updates.length}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{isModelOpen && <motion.div
|
||||
|
|
@ -49,13 +59,15 @@ function CourseUpdates() {
|
|||
}}
|
||||
style={{ position: 'absolute', top: '130%', right: 0 }}
|
||||
>
|
||||
<UpdatesModel />
|
||||
<UpdatesSection />
|
||||
</motion.div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const UpdatesModel = () => {
|
||||
const UpdatesSection = () => {
|
||||
const [selectedView, setSelectedView] = React.useState('list')
|
||||
const isAdmin = useAdminStatus() as boolean;
|
||||
return (
|
||||
<div className='bg-white/95 backdrop-blur-md nice-shadow rounded-lg w-[700px] overflow-hidden'>
|
||||
<div className='bg-gray-50/70 flex justify-between outline outline-1 rounded-lg outline-neutral-200/40'>
|
||||
|
|
@ -64,19 +76,24 @@ const UpdatesModel = () => {
|
|||
<span>Updates</span>
|
||||
|
||||
</div>
|
||||
<div className='py-2 px-4 space-x-2 items-center flex cursor-pointer text-xs font-medium hover:bg-gray-200 bg-gray-100 outline outline-1 outline-neutral-200/40'>
|
||||
{isAdmin && <div
|
||||
onClick={() => setSelectedView('new')}
|
||||
className='py-2 px-4 space-x-2 items-center flex cursor-pointer text-xs font-medium hover:bg-gray-200 bg-gray-100 outline outline-1 outline-neutral-200/40'>
|
||||
<PencilLine size={14} />
|
||||
<span>New Update</span>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
<div className=''>
|
||||
<NewUpdateForm />
|
||||
{selectedView === 'list' && <UpdatesListView />}
|
||||
{selectedView === 'new' && <NewUpdateForm setSelectedView={setSelectedView} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NewUpdateForm = () => {
|
||||
const NewUpdateForm = ({ setSelectedView }: any) => {
|
||||
const org = useOrg() as any;
|
||||
const course = useCourse() as any;
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
|
|
@ -96,9 +113,32 @@ const NewUpdateForm = () => {
|
|||
content: ''
|
||||
},
|
||||
validate,
|
||||
onSubmit: async (values) => { },
|
||||
onSubmit: async (values) => {
|
||||
const body = {
|
||||
title: values.title,
|
||||
content: values.content,
|
||||
course_uuid: course.courseStructure.course_uuid,
|
||||
org_id: org.id
|
||||
}
|
||||
const res = await createCourseUpdate(body)
|
||||
if (res.status === 200) {
|
||||
toast.success('Update added successfully')
|
||||
setSelectedView('list')
|
||||
mutate(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`)
|
||||
}
|
||||
else {
|
||||
toast.error('Failed to add update')
|
||||
}
|
||||
},
|
||||
enableReinitialize: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}
|
||||
, [course, org])
|
||||
|
||||
|
||||
return (
|
||||
<div className='bg-white/95 backdrop-blur-md nice-shadow rounded-lg w-[700px] overflow-hidden flex flex-col -space-y-2'>
|
||||
<div className='flex flex-col -space-y-2 px-4 pt-4'>
|
||||
|
|
@ -129,7 +169,7 @@ const NewUpdateForm = () => {
|
|||
/>
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
style={{ backgroundColor: 'white', height: '100px'}}
|
||||
style={{ backgroundColor: 'white', height: '100px' }}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.content}
|
||||
required
|
||||
|
|
@ -137,6 +177,7 @@ const NewUpdateForm = () => {
|
|||
</Form.Control>
|
||||
</FormField>
|
||||
<div className='flex justify-end py-2'>
|
||||
<button onClick={() => setSelectedView('list')} className='text-gray-500 px-4 py-2 rounded-md text-sm font-bold antialiased'>Cancel</button>
|
||||
<button className='bg-black text-white px-4 py-2 rounded-md text-sm font-bold antialiased'>Add Update</button>
|
||||
</div>
|
||||
</FormLayout>
|
||||
|
|
@ -146,37 +187,59 @@ const NewUpdateForm = () => {
|
|||
}
|
||||
|
||||
const UpdatesListView = () => {
|
||||
const course = useCourse() as any;
|
||||
const isAdmin = useAdminStatus() as boolean;
|
||||
const { data: updates } = useSWR(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`, swrFetcher)
|
||||
|
||||
return (
|
||||
<div className='px-5 bg-white overflow-y-auto' style={{ maxHeight: '400px' }}>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
{updates && updates.map((update: any) => (
|
||||
<div key={update.id} className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500 flex space-x-2 items-center justify-between '>{update.title} {isAdmin && <DeleteUpdateButton update={update} />}</div>
|
||||
<div className='text-gray-600'>{update.content}</div>
|
||||
</div>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
))}
|
||||
{(!updates || updates.length === 0) &&
|
||||
<div className='text-gray-500 text-center my-10 py-2 flex flex-col space-y-2'>
|
||||
<TentTree className='mx-auto' size={40} />
|
||||
<p>No updates yet</p>
|
||||
</div>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
</div>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
</div>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
</div>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
</div>
|
||||
<div className='py-2 border-b border-neutral-200'>
|
||||
<div className='font-bold text-gray-500'>New Update</div>
|
||||
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DeleteUpdateButton = ({ update }: any) => {
|
||||
const course = useCourse() as any;
|
||||
const org = useOrg() as any;
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await deleteCourseUpdate(course.courseStructure.course_uuid, update.courseupdate_uuid)
|
||||
if (res.status === 200) {
|
||||
toast.success('Update deleted successfully')
|
||||
mutate(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`)
|
||||
}
|
||||
else {
|
||||
toast.error('Failed to delete update')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Update"
|
||||
confirmationMessage="Are you sure you want to delete this update?"
|
||||
dialogTitle={'Delete Update ?'}
|
||||
buttonid='delete-update-button'
|
||||
dialogTrigger={
|
||||
<div id='delete-update-button' className='text-rose-600 text-xs bg-rose-100 rounded-full px-2 py-0.5 hover:cursor-pointer'>
|
||||
Delete
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
handleDelete()
|
||||
}}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type ModalParams = {
|
|||
functionToExecute: any
|
||||
dialogTrigger?: React.ReactNode
|
||||
status?: 'warning' | 'info'
|
||||
buttonid?: string
|
||||
}
|
||||
|
||||
const ConfirmationModal = (params: ModalParams) => {
|
||||
|
|
@ -58,6 +59,7 @@ const ConfirmationModal = (params: ModalParams) => {
|
|||
</div>
|
||||
<div className="flex flex-row-reverse pt-2">
|
||||
<div
|
||||
id={params.buttonid}
|
||||
className={`rounded-md text-sm px-3 py-2 font-bold flex justify-center items-center hover:cursor-pointer ${
|
||||
params.status === 'warning'
|
||||
? warningButtonColors
|
||||
|
|
|
|||
23
apps/web/services/courses/updates.ts
Normal file
23
apps/web/services/courses/updates.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { getAPIUrl } from '@services/config/config'
|
||||
import { RequestBody, getResponseMetadata } from '@services/utils/ts/requests'
|
||||
|
||||
export async function createCourseUpdate(body: any) {
|
||||
const result: any = await fetch(
|
||||
`${getAPIUrl()}courses/${body.course_uuid}/updates`,
|
||||
RequestBody('POST', body, null)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
export async function deleteCourseUpdate(
|
||||
course_uuid: string,
|
||||
update_uuid: number
|
||||
) {
|
||||
const result: any = await fetch(
|
||||
`${getAPIUrl()}courses/${course_uuid}/update/${update_uuid}`,
|
||||
RequestBody('DELETE', null, null)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue