Merge pull request #222 from learnhouse/feat/course-updates

CourseUpdates feature
This commit is contained in:
Badr B 2024-04-17 21:41:45 +02:00 committed by GitHub
commit f7675e430f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 619 additions and 65 deletions

View file

@ -0,0 +1,41 @@
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel
class CourseUpdate(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
courseupdate_uuid: str
title: str
content: str
course_id: int = Field(
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
)
linked_activity_uuids: Optional[str] = Field(default=None)
org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str
class CourseUpdateCreate(SQLModel):
title: str
content: str
linked_activity_uuids: Optional[str] = Field(default=None)
org_id: int
class CourseUpdateRead(SQLModel):
id: int
title: str
content: str
course_id: int
courseupdate_uuid: str
linked_activity_uuids: Optional[str] = Field(default=None)
org_id: int
creation_date: str
update_date: str
class CourseUpdateUpdate(SQLModel):
title: Optional[str] = None
content: Optional[str] = None
linked_activity_uuids: Optional[str] = Field(default=None)

View file

@ -2,6 +2,11 @@ from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request from fastapi import APIRouter, Depends, UploadFile, Form, Request
from sqlmodel import Session from sqlmodel import Session
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.db.course_updates import (
CourseUpdateCreate,
CourseUpdateRead,
CourseUpdateUpdate,
)
from src.db.users import PublicUser from src.db.users import PublicUser
from src.db.courses import ( from src.db.courses import (
CourseCreate, CourseCreate,
@ -19,6 +24,7 @@ from src.services.courses.courses import (
delete_course, delete_course,
update_course_thumbnail, update_course_thumbnail,
) )
from src.services.courses.updates import create_update, delete_update, get_updates_by_course_uuid, update_update
router = APIRouter() router = APIRouter()
@ -51,7 +57,9 @@ async def api_create_course(
learnings=learnings, learnings=learnings,
tags=tags, tags=tags,
) )
return await create_course(request, org_id, course, current_user, db_session, thumbnail) return await create_course(
request, org_id, course, current_user, db_session, thumbnail
)
@router.put("/{course_uuid}/thumbnail") @router.put("/{course_uuid}/thumbnail")
@ -145,3 +153,64 @@ async def api_delete_course(
""" """
return await delete_course(request, course_uuid, current_user, db_session) return await delete_course(request, course_uuid, current_user, db_session)
@ router.get("/{course_uuid}/updates")
async def api_get_course_updates(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> List[CourseUpdateRead]:
"""
Get Course Updates by course_uuid
"""
return await get_updates_by_course_uuid(request, course_uuid, current_user, db_session)
@router.post("/{course_uuid}/updates")
async def api_create_course_update(
request: Request,
course_uuid: str,
update_object: CourseUpdateCreate,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseUpdateRead:
"""
Create new Course Update
"""
return await create_update(
request, course_uuid, update_object, current_user, db_session
)
@router.put("/{course_uuid}/update/{courseupdate_uuid}")
async def api_update_course_update(
request: Request,
course_uuid: str,
courseupdate_uuid: str,
update_object: CourseUpdateUpdate,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseUpdateRead:
"""
Update Course Update by courseupdate_uuid
"""
return await update_update(
request, courseupdate_uuid, update_object, current_user, db_session
)
@router.delete("/{course_uuid}/update/{courseupdate_uuid}")
async def api_delete_course_update(
request: Request,
course_uuid: str,
courseupdate_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
):
"""
Delete Course Update by courseupdate_uuid
"""
return await delete_update(request, courseupdate_uuid, current_user, db_session)

View file

@ -5,7 +5,7 @@ async def check_element_type(element_uuid):
""" """
Check if the element is a course, a user, a house or a collection, by checking its prefix Check if the element is a course, a user, a house or a collection, by checking its prefix
""" """
if element_uuid.startswith("course_"): if element_uuid.startswith("course_") or element_uuid.startswith("courseupdate_"):
return "courses" return "courses"
elif element_uuid.startswith("user_"): elif element_uuid.startswith("user_"):
return "users" return "users"

View file

@ -0,0 +1,151 @@
from datetime import datetime
from typing import List
from uuid import uuid4
from fastapi import HTTPException, Request, status
from sqlmodel import Session, col, select
from src.db.course_updates import (
CourseUpdate,
CourseUpdateCreate,
CourseUpdateRead,
CourseUpdateUpdate,
)
from src.db.courses import Course
from src.db.organizations import Organization
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.courses import rbac_check
async def create_update(
request: Request,
course_uuid: str,
update_object: CourseUpdateCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> CourseUpdateRead:
# CHekc if org exists
statement_org = select(Organization).where(Organization.id == update_object.org_id)
org = db_session.exec(statement_org).first()
if not org or org.id is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
)
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course or course.id is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
# Generate UUID
courseupdate_uuid = str(f"courseupdate_{uuid4()}")
update = CourseUpdate(
**update_object.model_dump(),
course_id=course.id,
courseupdate_uuid=courseupdate_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(update)
db_session.commit()
db_session.refresh(update)
return CourseUpdateRead(**update.model_dump())
# Update Course Update
async def update_update(
request: Request,
courseupdate_uuid: str,
update_object: CourseUpdateUpdate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> CourseUpdateRead:
statement = select(CourseUpdate).where(
CourseUpdate.courseupdate_uuid == courseupdate_uuid
)
update = db_session.exec(statement).first()
if not update or update.id is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Update does not exist"
)
# RBAC check
await rbac_check(
request, update.courseupdate_uuid, current_user, "update", db_session
)
for key, value in update_object.model_dump().items():
if value is not None:
setattr(update, key, value)
db_session.add(update)
db_session.commit()
db_session.refresh(update)
return CourseUpdateRead(**update.model_dump())
# Delete Course Update
async def delete_update(
request: Request,
courseupdate_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(CourseUpdate).where(
CourseUpdate.courseupdate_uuid == courseupdate_uuid
)
update = db_session.exec(statement).first()
if not update or update.id is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Update does not exist"
)
# RBAC check
await rbac_check(
request, update.courseupdate_uuid, current_user, "delete", db_session
)
db_session.delete(update)
db_session.commit()
return {"message": "Update deleted successfully"}
# Get Course Updates by Course ID
async def get_updates_by_course_uuid(
request: Request,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> List[CourseUpdateRead]:
# FInd if course exists
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course or course.id is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
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]

View file

@ -15,6 +15,8 @@ import {
import { ArrowRight, Check, File, Sparkles, Video } from 'lucide-react' 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 { CourseProvider } from '@components/Contexts/CourseContext'
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({}) const [user, setUser] = useState<any>({})
@ -68,9 +70,16 @@ const CourseClient = (props: any) => {
<PageLoading></PageLoading> <PageLoading></PageLoading>
) : ( ) : (
<GeneralWrapperStyled> <GeneralWrapperStyled>
<div className="pb-3"> <div className="pb-3 flex justify-between items-center">
<p className="text-md font-bold text-gray-400 pb-2">Course</p> <div>
<h1 className="text-3xl -mt-3 font-bold">{course.name}</h1> <p className="text-md font-bold text-gray-400 pb-2">Course</p>
<h1 className="text-3xl -mt-3 font-bold">{course.name}</h1>
</div>
<div>
<CourseProvider courseuuid={course.course_uuid}>
<CourseUpdates />
</CourseProvider>
</div>
</div> </div>
{props.course?.thumbnail_image && org ? ( {props.course?.thumbnail_image && org ? (
@ -150,13 +159,13 @@ const CourseClient = (props: any) => {
<div className="courseicon items-center flex space-x-2 text-neutral-400"> <div className="courseicon items-center flex space-x-2 text-neutral-400">
{activity.activity_type === {activity.activity_type ===
'TYPE_DYNAMIC' && ( 'TYPE_DYNAMIC' && (
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<Sparkles <Sparkles
className="text-gray-400" className="text-gray-400"
size={13} size={13}
/> />
</div> </div>
)} )}
{activity.activity_type === 'TYPE_VIDEO' && ( {activity.activity_type === 'TYPE_VIDEO' && (
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<Video <Video
@ -167,13 +176,13 @@ const CourseClient = (props: any) => {
)} )}
{activity.activity_type === {activity.activity_type ===
'TYPE_DOCUMENT' && ( 'TYPE_DOCUMENT' && (
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<File <File
className="text-gray-400" className="text-gray-400"
size={13} size={13}
/> />
</div> </div>
)} )}
</div> </div>
<Link <Link
className="flex font-semibold grow pl-2 text-neutral-500" className="flex font-semibold grow pl-2 text-neutral-500"
@ -191,25 +200,25 @@ const CourseClient = (props: any) => {
<div className="flex "> <div className="flex ">
{activity.activity_type === {activity.activity_type ===
'TYPE_DYNAMIC' && ( 'TYPE_DYNAMIC' && (
<> <>
<Link <Link
className="flex grow pl-2 text-gray-500" className="flex grow pl-2 text-gray-500"
href={ href={
getUriWithOrg(orgslug, '') + getUriWithOrg(orgslug, '') +
`/course/${courseuuid}/activity/${activity.activity_uuid.replace( `/course/${courseuuid}/activity/${activity.activity_uuid.replace(
'activity_', 'activity_',
'' ''
)}` )}`
} }
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Page</p> <p>Page</p>
<ArrowRight size={13} /> <ArrowRight size={13} />
</div> </div>
</Link> </Link>
</> </>
)} )}
{activity.activity_type === 'TYPE_VIDEO' && ( {activity.activity_type === 'TYPE_VIDEO' && (
<> <>
<Link <Link
@ -232,25 +241,25 @@ const CourseClient = (props: any) => {
)} )}
{activity.activity_type === {activity.activity_type ===
'TYPE_DOCUMENT' && ( 'TYPE_DOCUMENT' && (
<> <>
<Link <Link
className="flex grow pl-2 text-gray-500" className="flex grow pl-2 text-gray-500"
href={ href={
getUriWithOrg(orgslug, '') + getUriWithOrg(orgslug, '') +
`/course/${courseuuid}/activity/${activity.activity_uuid.replace( `/course/${courseuuid}/activity/${activity.activity_uuid.replace(
'activity_', 'activity_',
'' ''
)}` )}`
} }
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Document</p> <p>Document</p>
<ArrowRight size={13} /> <ArrowRight size={13} />
</div> </div>
</Link> </Link>
</> </>
)} )}
</div> </div>
</div> </div>
</> </>

View file

@ -0,0 +1,260 @@
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'
import FormLayout, {
FormField,
FormLabelAndMessage,
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'
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
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() {
setIsModelOpen(!isModelOpen)
}
// if user clicks outside the model, close the model
React.useLayoutEffect(() => {
function handleClickOutside(event: any) {
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);
}, []);
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 onClick={handleModelOpen} className='flex items-center space-x-2 font-normal hover:cursor-pointer text-gray-600'>
<div><Rss size={16} /> </div>
<div className='flex space-x-2 items-center'>
<span>Updates</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
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: 'spring',
stiffness: 1300,
damping: 70,
}}
style={{ position: 'absolute', top: '130%', right: 0 }}
>
<UpdatesSection />
</motion.div>}
</div>
)
}
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'>
<div className='py-2 px-4 font-bold text-gray-500 flex space-x-2 items-center'>
<Rss size={16} />
<span>Updates</span>
</div>
{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 className=''>
{selectedView === 'list' && <UpdatesListView />}
{selectedView === 'new' && <NewUpdateForm setSelectedView={setSelectedView} />}
</div>
</div>
)
}
const NewUpdateForm = ({ setSelectedView }: any) => {
const org = useOrg() as any;
const course = useCourse() as any;
const validate = (values: any) => {
const errors: any = {}
if (!values.title) {
errors.title = 'Title is required'
}
if (!values.content) {
errors.content = 'Content is required'
}
return errors
}
const formik = useFormik({
initialValues: {
title: '',
content: ''
},
validate,
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'>
<div className='text-gray-500 px-3 py-0.5 rounded-full font-semibold text-xs'>Test Course </div>
<div className='text-black px-3 py-0.5 rounded-full text-lg font-bold'>Add new Course Update</div>
</div>
<div className='px-5 -py-2'>
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="title">
<FormLabelAndMessage
label="Title"
message={formik.errors.title}
/>
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.title}
type="text"
required
/>
</Form.Control>
</FormField>
<FormField name="content">
<FormLabelAndMessage
label="Content"
message={formik.errors.content}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white', height: '100px' }}
onChange={formik.handleChange}
value={formik.values.content}
required
/>
</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>
</div>
</div>
)
}
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' }}>
{updates && updates.map((update: any) => (
<div key={update.id} className='py-2 border-b border-neutral-200 antialiased'>
<div className='font-bold text-gray-500 flex space-x-2 items-center justify-between '>
<div className='flex space-x-2 items-center'>
<span> {update.title}</span>
<span
title={"Created at " + dayjs(update.creation_date).format('MMMM D, YYYY')}
className='text-xs font-semibold text-gray-300'>
{dayjs(update.creation_date).fromNow()}
</span>
</div>
{isAdmin && <DeleteUpdateButton update={update} />}</div>
<div className='text-gray-600'>{update.content}</div>
</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>
)
}
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>
)
}
export default CourseUpdates

View file

@ -50,6 +50,7 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
}, 10); }, 10);
}; };
// Store the Y document in the browser // Store the Y document in the browser
new IndexeddbPersistence(props.activity.activity_uuid, doc) new IndexeddbPersistence(props.activity.activity_uuid, doc)

View file

@ -23,14 +23,8 @@ export const Menu = (props: any) => {
return ( return (
<> <>
<div className="backdrop-blur-lg h-[60px] blur-3xl z-10" style={{}}> <div className="backdrop-blur-lg h-[60px] blur-3xl -z-10" style={{}}>
<div
className="h-[150px] blur-3xl z-0"
style={{
background:
'radial-gradient(1397.20% 56.18% at 75.99% 53.73%, rgba(253, 182, 207, 0.08) 0%, rgba(3, 110, 146, 0.08) 100%)',
}}
></div>
</div> </div>
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50"> <div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
<div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16"> <div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16">

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
}

View file

@ -5,6 +5,10 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
.nice-shadow {
@apply shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40
}
html, html,
body { body {
padding: 0; padding: 0;