feat: add updates functionality to app

This commit is contained in:
swve 2024-04-17 21:26:21 +02:00
parent f0e5fa925d
commit 8a2ccb2534
6 changed files with 147 additions and 53 deletions

View file

@ -167,7 +167,7 @@ async def api_get_course_updates(
return await get_updates_by_course_uuid(request, course_uuid, current_user, db_session) 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( async def api_create_course_update(
request: Request, request: Request,
course_uuid: str, course_uuid: str,

View file

@ -2,7 +2,7 @@ from datetime import datetime
from typing import List from typing import List
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, status
from sqlmodel import Session, select from sqlmodel import Session, col, select
from src.db.course_updates import ( from src.db.course_updates import (
CourseUpdate, CourseUpdate,
CourseUpdateCreate, CourseUpdateCreate,
@ -89,7 +89,6 @@ async def update_update(
if value is not None: if value is not None:
setattr(update, key, value) setattr(update, key, value)
db_session.add(update) db_session.add(update)
db_session.commit() 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" 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() updates = db_session.exec(statement).all()
return [CourseUpdateRead(**update.model_dump()) for update in updates] return [CourseUpdateRead(**update.model_dump()) for update in updates]

View file

@ -16,6 +16,7 @@ import { ArrowRight, Check, File, Sparkles, Video } from 'lucide-react'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import UserAvatar from '@components/Objects/UserAvatar' import UserAvatar from '@components/Objects/UserAvatar'
import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates' import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates'
import { CourseProvider } from '@components/Contexts/CourseContext'
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const [user, setUser] = useState<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> <h1 className="text-3xl -mt-3 font-bold">{course.name}</h1>
</div> </div>
<div> <div>
<CourseProvider courseuuid={course.course_uuid}>
<CourseUpdates /> <CourseUpdates />
</CourseProvider>
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
import { PencilLine, Rss } from 'lucide-react' import { PencilLine, Rss, TentTree } from 'lucide-react'
import React from 'react' import React, { useEffect } from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import * as Form from '@radix-ui/react-form' import * as Form from '@radix-ui/react-form'
@ -9,8 +9,19 @@ import FormLayout, {
Input, Input,
Textarea, Textarea,
} from '@components/StyledElements/Form/Form' } 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() { 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) const [isModelOpen, setIsModelOpen] = React.useState(false)
function handleModelOpen() { function handleModelOpen() {
@ -18,17 +29,16 @@ function CourseUpdates() {
} }
// if user clicks outside the model, close the model // if user clicks outside the model, close the model
React.useEffect(() => { React.useLayoutEffect(() => {
function handleClickOutside(event: any) { function handleClickOutside(event: any) {
if (event.target.closest('.bg-white') === null) { console.log(event.target.id)
setIsModelOpen(false) if (event.target.closest('.bg-white') || event.target.id === 'delete-update-button') return;
setIsModelOpen(false);
} }
} document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside);
return () => { }, []);
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
return ( 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'> <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><Rss size={16} /> </div>
<div className='flex space-x-2 items-center'> <div className='flex space-x-2 items-center'>
<span>Updates</span> <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>
</div> </div>
{isModelOpen && <motion.div {isModelOpen && <motion.div
@ -49,13 +59,15 @@ function CourseUpdates() {
}} }}
style={{ position: 'absolute', top: '130%', right: 0 }} style={{ position: 'absolute', top: '130%', right: 0 }}
> >
<UpdatesModel /> <UpdatesSection />
</motion.div>} </motion.div>}
</div> </div>
) )
} }
const UpdatesModel = () => { const UpdatesSection = () => {
const [selectedView, setSelectedView] = React.useState('list')
const isAdmin = useAdminStatus() as boolean;
return ( return (
<div className='bg-white/95 backdrop-blur-md nice-shadow rounded-lg w-[700px] overflow-hidden'> <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'> <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> <span>Updates</span>
</div> </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} /> <PencilLine size={14} />
<span>New Update</span> <span>New Update</span>
</div> </div>}
</div> </div>
<div className=''> <div className=''>
<NewUpdateForm /> {selectedView === 'list' && <UpdatesListView />}
{selectedView === 'new' && <NewUpdateForm setSelectedView={setSelectedView} />}
</div> </div>
</div> </div>
) )
} }
const NewUpdateForm = () => { const NewUpdateForm = ({ setSelectedView }: any) => {
const org = useOrg() as any;
const course = useCourse() as any;
const validate = (values: any) => { const validate = (values: any) => {
const errors: any = {} const errors: any = {}
@ -96,9 +113,32 @@ const NewUpdateForm = () => {
content: '' content: ''
}, },
validate, 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, enableReinitialize: true,
}) })
useEffect(() => {
}
, [course, org])
return ( 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='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'> <div className='flex flex-col -space-y-2 px-4 pt-4'>
@ -129,7 +169,7 @@ const NewUpdateForm = () => {
/> />
<Form.Control asChild> <Form.Control asChild>
<Textarea <Textarea
style={{ backgroundColor: 'white', height: '100px'}} style={{ backgroundColor: 'white', height: '100px' }}
onChange={formik.handleChange} onChange={formik.handleChange}
value={formik.values.content} value={formik.values.content}
required required
@ -137,6 +177,7 @@ const NewUpdateForm = () => {
</Form.Control> </Form.Control>
</FormField> </FormField>
<div className='flex justify-end py-2'> <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> <button className='bg-black text-white px-4 py-2 rounded-md text-sm font-bold antialiased'>Add Update</button>
</div> </div>
</FormLayout> </FormLayout>
@ -146,37 +187,59 @@ const NewUpdateForm = () => {
} }
const UpdatesListView = () => { 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 ( return (
<div className='px-5 bg-white overflow-y-auto' style={{ maxHeight: '400px' }}> <div className='px-5 bg-white overflow-y-auto' style={{ maxHeight: '400px' }}>
<div className='py-2 border-b border-neutral-200'> {updates && updates.map((update: any) => (
<div className='font-bold text-gray-500'>New Update</div> <div key={update.id} className='py-2 border-b border-neutral-200'>
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div> <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>
<div className='py-2 border-b border-neutral-200'> ))}
<div className='font-bold text-gray-500'>New Update</div> {(!updates || updates.length === 0) &&
<div className='text-gray-600'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, doloremque.</div> <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>
<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> </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> </div>
}
functionToExecute={() => {
handleDelete()
}}
status="warning"
></ConfirmationModal>
) )
} }

View file

@ -12,6 +12,7 @@ type ModalParams = {
functionToExecute: any functionToExecute: any
dialogTrigger?: React.ReactNode dialogTrigger?: React.ReactNode
status?: 'warning' | 'info' status?: 'warning' | 'info'
buttonid?: string
} }
const ConfirmationModal = (params: ModalParams) => { const ConfirmationModal = (params: ModalParams) => {
@ -58,6 +59,7 @@ const ConfirmationModal = (params: ModalParams) => {
</div> </div>
<div className="flex flex-row-reverse pt-2"> <div className="flex flex-row-reverse pt-2">
<div <div
id={params.buttonid}
className={`rounded-md text-sm px-3 py-2 font-bold flex justify-center items-center hover:cursor-pointer ${ className={`rounded-md text-sm px-3 py-2 font-bold flex justify-center items-center hover:cursor-pointer ${
params.status === 'warning' params.status === 'warning'
? warningButtonColors ? warningButtonColors

View 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
}