diff --git a/django_email_learning/api/serializers.py b/django_email_learning/api/serializers.py index 751dfb3..4d242ea 100644 --- a/django_email_learning/api/serializers.py +++ b/django_email_learning/api/serializers.py @@ -174,6 +174,13 @@ class LessonCreate(BaseModel): type: Literal["lesson"] +class LessonUpdate(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + + model_config = ConfigDict(extra="forbid") + + class LessonResponse(BaseModel): id: int title: str @@ -282,10 +289,17 @@ def from_seconds(cls, seconds: int) -> "WaitingPeriod": class CreateCourseContentRequest(BaseModel): - priority: int = Field(gt=0, examples=[1]) + priority: int | None = Field(gt=0, examples=[1], default=None) waiting_period: WaitingPeriod content: LessonCreate | QuizCreate = Field(discriminator="type") + @property + def required_priority(self) -> int: + if self.priority is not None: + return self.priority + else: + raise ValueError("Priority must be set before converting to Django model.") + def to_django_model(self, course: Course) -> CourseContent: lesson = None quiz = None @@ -319,7 +333,7 @@ def to_django_model(self, course: Course) -> CourseContent: content_type = "quiz" course_content = CourseContent.objects.create( course=course, - priority=self.priority, + priority=self.required_priority, waiting_period=self.waiting_period.to_seconds(), lesson=lesson, quiz=quiz, diff --git a/django_email_learning/api/urls.py b/django_email_learning/api/urls.py index 96a788a..d03da41 100644 --- a/django_email_learning/api/urls.py +++ b/django_email_learning/api/urls.py @@ -3,6 +3,7 @@ from django_email_learning.api.views import ( CourseView, ImapConnectionView, + LessonView, OrganizationsView, SingleCourseView, CourseContentView, @@ -38,6 +39,11 @@ SingleCourseContentView.as_view(), name="single_course_content_view", ), + path( + "organizations//lessons//", + LessonView.as_view(), + name="lesson_view", + ), path("organizations/", OrganizationsView.as_view(), name="organizations_view"), path("session", UpdateSessionView.as_view(), name="update_session_view"), path("", page_not_found, name="root"), diff --git a/django_email_learning/api/views.py b/django_email_learning/api/views.py index 54a2f4e..9147df1 100644 --- a/django_email_learning/api/views.py +++ b/django_email_learning/api/views.py @@ -4,6 +4,7 @@ from django.db.utils import IntegrityError from django.http import JsonResponse from django.core.exceptions import ValidationError as DjangoValidationError +from django.db import models from pydantic import ValidationError from django_email_learning.api import serializers @@ -11,6 +12,7 @@ Course, CourseContent, ImapConnection, + Lesson, OrganizationUser, Organization, ) @@ -68,6 +70,14 @@ def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unt try: serializer = serializers.CreateCourseContentRequest.model_validate(payload) course = Course.objects.get(id=kwargs["course_id"]) + if serializer.priority is None: + # Set priority to max existing priority + 1 + max_priority = ( + CourseContent.objects.filter(course_id=course.id) + .aggregate(max_priority=models.Max("priority")) + .get("max_priority") + ) + serializer.priority = (max_priority or 0) + 1 course_content = serializer.to_django_model(course=course) return JsonResponse( @@ -130,6 +140,8 @@ def delete(self, request, *args, **kwargs): # type: ignore[no-untyped-def] except (IntegrityError, ValueError) as e: return JsonResponse({"error": str(e)}, status=409) + # TODO: Implement POST method for updating course content. + @method_decorator(accessible_for(roles={"admin", "editor"}), name="post") @method_decorator(accessible_for(roles={"admin", "editor"}), name="delete") @@ -255,6 +267,29 @@ def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unt return JsonResponse({"error": str(e)}, status=409) +@method_decorator(accessible_for(roles={"admin", "editor"}), name="post") +class LessonView(View): + def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def] + payload = json.loads(request.body) + try: + serializer = serializers.LessonUpdate.model_validate(payload) + lesson = Lesson.objects.get(id=kwargs["lesson_id"]) + if serializer.title is not None: + lesson.title = serializer.title + if serializer.content is not None: + lesson.content = serializer.content + lesson.save() + + return JsonResponse( + {}, + status=204, + ) + except Lesson.DoesNotExist: + return JsonResponse({"error": "Lesson not found"}, status=404) + except ValidationError as e: + return JsonResponse({"error": e.errors()}, status=400) + + @method_decorator(is_an_organization_member(), name="post") class UpdateSessionView(View): def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def] diff --git a/django_email_learning/platform/views.py b/django_email_learning/platform/views.py index 99b28d1..e265736 100644 --- a/django_email_learning/platform/views.py +++ b/django_email_learning/platform/views.py @@ -82,7 +82,8 @@ class CourseView(BasePlatformView): def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] context = super().get_context_data(**kwargs) course = Course.objects.get(pk=self.kwargs["course_id"]) - context["page_title"] = course.title + context["course"] = course + context["page_title"] = f"Course: {course.title}" return context diff --git a/django_email_learning/static/logo.png b/django_email_learning/static/logo.png new file mode 100644 index 0000000..36b8313 Binary files /dev/null and b/django_email_learning/static/logo.png differ diff --git a/django_email_learning/templates/platform/base.html b/django_email_learning/templates/platform/base.html index 3ca58c9..4283a82 100644 --- a/django_email_learning/templates/platform/base.html +++ b/django_email_learning/templates/platform/base.html @@ -1,4 +1,5 @@ {% load django_vite %} +{% load static %} @@ -14,6 +15,7 @@ + {% block title %}{{ page_title }}{% endblock %} diff --git a/django_email_learning/templates/platform/course.html b/django_email_learning/templates/platform/course.html index 7de95f7..67b7ab7 100644 --- a/django_email_learning/templates/platform/course.html +++ b/django_email_learning/templates/platform/course.html @@ -1,5 +1,9 @@ {% extends "platform/base.html" %} {% load django_vite %} {% block extra_head %} - + + {% vite_asset 'course/Course.jsx' %} {% endblock %} diff --git a/django_service/settings.py b/django_service/settings.py index c323e77..15c644d 100644 --- a/django_service/settings.py +++ b/django_service/settings.py @@ -65,7 +65,7 @@ CORS_ALLOW_CREDENTIALS = True CSRF_COOKIE_SECURE = False -CSRF_COOKIE_SAMESITE = "None" +# CSRF_COOKIE_SAMESITE = "None" ROOT_URLCONF = "django_service.urls" @@ -148,7 +148,14 @@ STATIC_URL = "static/" -STATIC_ROOT = BASE_DIR / "static" +# For development - where Django looks for static files +STATICFILES_DIRS = [ + BASE_DIR / "django_service" / "static", +] + +# For production - where collectstatic puts files +STATIC_ROOT = BASE_DIR / "staticfiles" + # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/django_service/static/src/assets/logo.png b/django_service/static/src/assets/logo.png new file mode 100644 index 0000000..36b8313 Binary files /dev/null and b/django_service/static/src/assets/logo.png differ diff --git a/django_service/static/src/assets/logo2.png b/django_service/static/src/assets/logo2.png new file mode 100644 index 0000000..2db13f6 Binary files /dev/null and b/django_service/static/src/assets/logo2.png differ diff --git a/django_service/templates/django_service/home.html b/django_service/templates/django_service/home.html index 8a026d4..d43ee0d 100644 --- a/django_service/templates/django_service/home.html +++ b/django_service/templates/django_service/home.html @@ -1,3 +1,4 @@ +{% load static %} @@ -43,6 +44,11 @@
+

+ Django Email Learning Logo

Welcome to Django Email Learning Development Sample Service

This is a sample Django service demonstrating the Django Email Learning package, diff --git a/django_service/urls.py b/django_service/urls.py index bbfb392..898f848 100644 --- a/django_service/urls.py +++ b/django_service/urls.py @@ -18,6 +18,9 @@ from django.views.generic import TemplateView from django.contrib.auth import views as auth_views from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + urlpatterns = [ path( @@ -34,3 +37,9 @@ include("django_email_learning.urls", namespace="django_email_learning"), ), ] + +# Serve static files during development +if settings.DEBUG: + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + # Uncomment the line below if you have media files (user uploads) + # urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/frontend/course/Course.jsx b/frontend/course/Course.jsx new file mode 100644 index 0000000..1ff6211 --- /dev/null +++ b/frontend/course/Course.jsx @@ -0,0 +1,118 @@ +import './styles.scss' + +import 'vite/modulepreload-polyfill' +import render from '../src/render.jsx'; +import Base from '../src/components/Base.jsx' +import FilterListIcon from '@mui/icons-material/FilterList'; +import DescriptionIcon from '@mui/icons-material/Description'; +import BallotIcon from '@mui/icons-material/Ballot'; +import { useState } from 'react'; +import { Box, Grid, Button, Dialog } from '@mui/material' +import LessonForm from './components/LessonForm.jsx'; +import QuizForm from './components/QuizForm.jsx'; +import ContentTable from './components/ContentTable.jsx'; +import { getCookie } from '../src/utils.js'; + + +function Course() { + const platformBaseUrl = localStorage.getItem('platformBaseUrl'); + const [dialogOpen, setDialogOpen] = useState(false) + const [dialogContent, setDialogContent] = useState(null) + const [lessonCache, setLessonCache] = useState("") + const [contentLoaded, setContentLoaded] = useState(false) + + const apiBaseUrl = localStorage.getItem('apiBaseUrl'); + const organizationId = localStorage.getItem('activeOrganizationId'); + + const resetDialog = () => { + setDialogOpen(false); + setContentLoaded(false); + } + + const handleClose = (event, reason) => { + if (reason !== "backdropClick") { + setDialogOpen(false); + } + } + const getContent = async (contentId, ) => { + console.log("Fetching content with ID:", contentId); + const response = await fetch(`${apiBaseUrl}/organizations/${organizationId}/courses/${course_id}/contents/${contentId}/`, { + method: 'GET', + headers: { + 'X-CSRFToken': getCookie('csrftoken') + }, + }); + if (response.ok) { + const data = await response.json(); + console.log("Content data:", data); + return data; + } else { + console.error('Error fetching content:', response.statusText); + return null; + } + } + + const tableEventHandler = async (event) => { + console.log("Event triggered from ContentTable", event); + if (event.type === 'content_loaded') { + setContentLoaded(true); + } + if (event.type === 'content_clicked') { + const content = await getContent(event.content_id); + if (content.type == 'lesson') { + console.log("Opening lesson editor for content:", content); + setDialogOpen(true); + setDialogContent( {setLessonCache(""); setDialogOpen(false);}} + successCallback={resetDialog} + courseId={course_id} + lessonId={content.lesson.id} />); + } + } + } + + return ( + , + children:

Filter Options Here
, + }} + showOrganizationSwitcher={false} + > + + + + + tableEventHandler(event)} /> + + + + + {dialogContent} + + + ) +} + +render({children: }); diff --git a/frontend/course/components/ContentTable.jsx b/frontend/course/components/ContentTable.jsx new file mode 100644 index 0000000..58ca097 --- /dev/null +++ b/frontend/course/components/ContentTable.jsx @@ -0,0 +1,93 @@ +import { TableContainer, Table, TableHead, TableRow, TableBody, TableCell, Paper, Typography } from '@mui/material'; +import { useState, useEffect } from 'react'; +import { getCookie } from '../../src/utils.js'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; + +const ContentTable = ({ courseId, eventHandler, loaded = false }) => { + const [contentList, setContentList] = useState([]); + + const apiBaseUrl = localStorage.getItem('apiBaseUrl'); + const organizationId = localStorage.getItem('activeOrganizationId'); + + const formatPeriod = (period) => { + if (!period) { + return ""; + } + let unit = period.type; + if (period.period === 1) { + unit = period.type.slice(0, -1); + } + return `${period.period} ${unit}`; + } + + useEffect(() => { + getContets(); + }, [loaded]); + + const deleteContent = (contentId) => { + fetch(`${apiBaseUrl}/organizations/${organizationId}/courses/${courseId}/contents/${contentId}/`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': getCookie('csrftoken') + }, + }) + .then(response => { + if (response.ok) { + setContentList(contentList.filter(content => content.id !== contentId)); + } else { + console.error('Error deleting content:', response.statusText); + } + }) + .catch(error => console.error('Error deleting content:', error)); + } + + + const getContets = () => { + + fetch(`${apiBaseUrl}/organizations/${organizationId}/courses/${courseId}/contents`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + }) + .then(response => response.json()) + .then(data => { + setContentList(data.course_contents); + let event = {type: 'content_loaded', data: data}; + eventHandler(event); + }) + .catch(error => console.error('Error fetching content list:', error)); + } + + return ( + + + + + Title + Waiting time + type + Actions + + + + {contentList.map((content) => ( + + {let event = {type: 'content_clicked', content_id: content.id}; eventHandler(event);}} + color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title} + {formatPeriod(content.waiting_period)} + {content.type} + + deleteContent(content.id)} /> + + + ))} + +
+
+ ); +} + +export default ContentTable; diff --git a/frontend/course/components/LessonForm.jsx b/frontend/course/components/LessonForm.jsx new file mode 100644 index 0000000..cc2b655 --- /dev/null +++ b/frontend/course/components/LessonForm.jsx @@ -0,0 +1,163 @@ +import { useState, useEffect, use } from 'react'; +import { Alert, Box, Button, Typography, Select, MenuItem, Tooltip } from '@mui/material'; +import RequiredTextField from '../../src/components/RequiredTextField.jsx'; +import ContentEditor from '../../src/components/ContentEditor'; +import { getCookie } from '../../src/utils.js'; + +function LessonForm({ header, initialTitle, initialContent, onContentChange, cancelCallback, successCallback, courseId, lessonId }) { + const [title, setTitle] = useState(initialTitle || ""); + const [content, setContent] = useState(initialContent || ""); + const [waitingPeriod, setWaitingPeriod] = useState(1); + const [waitingPeriodUnit, setWaitingPeriodUnit] = useState("days"); + const [titleHelperText, setTitleHelperText] = useState(""); + const [contentHelperText, setContentHelperText] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const apiBaseUrl = localStorage.getItem('apiBaseUrl'); + const orgId = localStorage.getItem('activeOrganizationId'); + + const addLesson = () =>{ + if (!validateForm()) { + setErrorMessage("Please fix the errors in the form before submitting."); + return; + } + + console.log("Adding lesson to course ID:", courseId); + fetch(apiBaseUrl + '/organizations/' + orgId + '/courses/' + courseId + '/contents/', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ + content: { + title: title, + content: content, + type: 'lesson' + }, + waiting_period: {"period": waitingPeriod, "type": waitingPeriodUnit}, + }), + }) + .then((response) => response.json()) + .then((data) => { + console.log('Lesson created successfully:', data); + setContent(""); + setTitle(""); + successCallback(); + }) + .catch((error) => { + console.error('Error creating lesson:', error); + }); + } + + const updateLesson = () => { + if (!validateForm()) { + setErrorMessage("Please fix the errors in the form before submitting."); + return; + } + + console.log("Updating lesson ID:", lessonId); + fetch(apiBaseUrl + '/organizations/' + orgId + '/lessons/' + lessonId + '/', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ + title: title, + content: content, + }), + }) + .then((response) => { + console.log(response) + if (response.status === 204) { + console.log('Lesson updated successfully'); + setContent(""); + setTitle(""); + successCallback(); + } + }) + .catch((error) => { + console.error('Error updating lesson:', error); + }); + } + + + useEffect(() => { + onContentChange(content); + }, [content]); + + const handleContentChange = (newContent) => { + setContent(newContent); + } + + const cancel = () => { + setContent(""); + setTitle(""); + cancelCallback(); + } + + const validateForm = () => { + let isValid = true; + if (!title) { + setTitleHelperText("Lesson title is required."); + isValid = false; + } else { + setTitleHelperText(""); + } + if (!content) { + setContentHelperText("Lesson content is required."); + isValid = false; + } else { + setContentHelperText(""); + } + return isValid; + } + + return ( + + {header} + { errorMessage && ( + + {errorMessage} + + )} + setTitle(e.target.value)} helperText={titleHelperText}/> + + + + {contentHelperText} + + + + setWaitingPeriod(e.target.value)} + sx={{ width: '200px', mr: 2 }} + inputProps={{ min: 1 }} + /> + + + + + + + + ); +} + +export default LessonForm; diff --git a/frontend/course/components/QuestionForm.jsx b/frontend/course/components/QuestionForm.jsx new file mode 100644 index 0000000..0947e2c --- /dev/null +++ b/frontend/course/components/QuestionForm.jsx @@ -0,0 +1,139 @@ +import { useState, useRef, useEffect, use } from 'react'; +import { Box, Grid, Typography, Button, Switch, Table, TableHead, TableBody, TableRow, TableCell, TextField } from '@mui/material'; +import RuleIcon from '@mui/icons-material/Rule'; +import EditIcon from '@mui/icons-material/Edit'; +import ClearIcon from '@mui/icons-material/Clear'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; + + + +const QuestionForm = ({question, index, deleteCallback}) => { + const [questionText, setQuestionText] = useState(question.question); + const [options, setOptions] = useState(question.options || []); + const [editMode, setEditMode] = useState(false); + const [addingOption, setAddingOption] = useState(false); + const optionInputRef = useRef(null); + + const editQuestion = () => { + if (editMode && questionText.trim() === '') { + return; + } + setEditMode(!editMode); + } + + useEffect(() => { + if (addingOption && optionInputRef.current) { + optionInputRef.current.focus(); + } + }, [addingOption]); + + const addToOptions = (optionText) => { + if (optionText.trim() !== "") { + setOptions([...options, {"optionText": optionText.trim(), "isCorrect": false}]); + } + setAddingOption(false); + } + + const updateOption = (optionIndex, isCorrect) => { + const updatedOptions = options.map((option, idx) => + idx === optionIndex ? { ...option, isCorrect: isCorrect } : option + ); + setOptions(updatedOptions); + } + + + return ( + + + + + {!editMode ? ( + {index + 1}. {questionText} + ) : ( + setQuestionText(e.target.value)} + onKeyDown={(e) => {if (e.key === 'Enter') { editQuestion(); }}} + autoFocus + helperText={!questionText ? "Question can not be empty": ""} + /> + )} + + + + + + {addingOption && (<> + + { + if (e.key === 'Enter') { + addToOptions(e.target.value); + } + }} + /> + + + + + + )} + + { options.length > 0 && + + + + Options + Correct Answer + Actions + + + + {options.map((option, idx) => ( + + {option.optionText} + updateOption(idx, e.target.checked)} /> + + { + const newOptionText = prompt("Edit option text:", option.optionText); + if (newOptionText !== null && newOptionText.trim() !== "") { + const updatedOptions = options.map((opt, i) => i === idx ? { ...opt, optionText: newOptionText.trim() } : opt); + setOptions(updatedOptions); + } + }} /> + { + const updatedOptions = options.filter((_, i) => i !== idx); + setOptions(updatedOptions); + }} /> + + + ))} + +
+
} +
+
+
+ ); +} + +export default QuestionForm; diff --git a/frontend/course/components/QuizForm.jsx b/frontend/course/components/QuizForm.jsx new file mode 100644 index 0000000..9ad300c --- /dev/null +++ b/frontend/course/components/QuizForm.jsx @@ -0,0 +1,117 @@ +import { useRef, useState, useEffect } from 'react'; +import { Box, Button, Grid, Typography } from '@mui/material'; +import QuizIcon from '@mui/icons-material/Quiz'; +import RequiredTextField from '../../src/components/RequiredTextField'; +import QuestionForm from './QuestionForm'; + +const QuizForm = ({cancelCallback, successCallback, courseId, quizId }) => { + + const [showQuestionField, setShowQuestionField] = useState(false); + const [newQuestion, setNewQuestion] = useState(""); + const [questions, setQuestions] = useState([]); + const questionInputRef = useRef(null); + const dialogRef = useRef(null); + + const addQuiz = () => { + // Implement add quiz logic here + console.log("Adding quiz to course ID:", courseId); + // After successful addition + successCallback(); + } + + const updateQuiz = () => { + // Implement update quiz logic here + console.log("Updating quiz ID:", quizId); + // After successful update + successCallback(); + } + + const cancel = () => { + cancelCallback(); + } + + useEffect(() => { + console.log('useEffect triggered, showQuestionField:', showQuestionField); + if (!showQuestionField) { + console.log('Focusing dialog'); + dialogRef.current?.focus(); + } + if (showQuestionField) { + console.log('Should focus question input, ref is:', questionInputRef.current); + if (questionInputRef.current) { + questionInputRef.current.focus(); + } else { + console.log('questionInputRef.current is null!'); + } + } + }, [showQuestionField]); + + + const addToQuestions = () => { + + if (newQuestion.trim() !== "") { + setQuestions([...questions, {"question": newQuestion.trim()}]); + } + setNewQuestion(""); + setShowQuestionField(false); + } + + const handleQuestionDelete = (index) => { + const updatedQuestions = questions.filter((_, i) => i !== index); + setQuestions(updatedQuestions); + } + + return ( + { + if (e.key === 'q' && !showQuestionField) { + setNewQuestion(""); + setTimeout(() => { + setShowQuestionField(true); + }, 100); + + } + }} tabIndex={0} focusable="true"> + { quizId ? "Update Quiz" : "New Quiz" } + + { showQuestionField && ( + + + + setNewQuestion(e.target.value)} sx={{ width: '100%' }} onKeyDown={(e) => { + if (e.key === 'Enter') { + addToQuestions(); + } + }}/> + + + + + + + + ) } + + { questions.map((question, index) => ( + handleQuestionDelete(index)} /> + )) } + + + + + + + ); +} + +export default QuizForm; diff --git a/frontend/course/index.html b/frontend/course/index.html new file mode 100644 index 0000000..66229a8 --- /dev/null +++ b/frontend/course/index.html @@ -0,0 +1,12 @@ + + + + + + Course + + +
+ + + diff --git a/frontend/course/styles.scss b/frontend/course/styles.scss new file mode 100644 index 0000000..b79141f --- /dev/null +++ b/frontend/course/styles.scss @@ -0,0 +1,94 @@ +.tiptap { + :first-child { + margin-top: 0; + } + + img { + display: block; + } + + [data-resize-handle] { + position: absolute; + background: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 2px; + z-index: 10; + + &:hover { + background: rgba(0, 0, 0, 0.8); + } + + /* Corner handles */ + &[data-resize-handle='top-left'], + &[data-resize-handle='top-right'], + &[data-resize-handle='bottom-left'], + &[data-resize-handle='bottom-right'] { + width: 8px; + height: 8px; + } + + &[data-resize-handle='top-left'] { + top: -4px; + left: -4px; + cursor: nwse-resize; + } + + &[data-resize-handle='top-right'] { + top: -4px; + right: -4px; + cursor: nesw-resize; + } + + &[data-resize-handle='bottom-left'] { + bottom: -4px; + left: -4px; + cursor: nesw-resize; + } + + &[data-resize-handle='bottom-right'] { + bottom: -4px; + right: -4px; + cursor: nwse-resize; + } + + /* Edge handles */ + &[data-resize-handle='top'], + &[data-resize-handle='bottom'] { + height: 6px; + left: 8px; + right: 8px; + } + + &[data-resize-handle='top'] { + top: -3px; + cursor: ns-resize; + } + + &[data-resize-handle='bottom'] { + bottom: -3px; + cursor: ns-resize; + } + + &[data-resize-handle='left'], + &[data-resize-handle='right'] { + width: 6px; + top: 8px; + bottom: 8px; + } + + &[data-resize-handle='left'] { + left: -3px; + cursor: ew-resize; + } + + &[data-resize-handle='right'] { + right: -3px; + cursor: ew-resize; + } + } + + [data-resize-state='true'] [data-resize-wrapper] { + outline: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 0.125rem; + } +} diff --git a/frontend/courses/Courses.jsx b/frontend/courses/Courses.jsx index af97f18..2e68587 100644 --- a/frontend/courses/Courses.jsx +++ b/frontend/courses/Courses.jsx @@ -1,10 +1,10 @@ import 'vite/modulepreload-polyfill' -import { useState, useEffect, use } from 'react' -import { Grid, Box, FormControlLabel, Link, Typography, Button, Dialog, Paper, RadioGroup, Radio, Switch, TableContainer, Table, TableHead, TableRow,TableBody, TableCell } from '@mui/material' +import { useState, useEffect } from 'react' +import { Grid, Box, Link, Button, Dialog, Paper, Switch, TableContainer, Table, TableHead, TableRow,TableBody, TableCell } from '@mui/material' import Base from '../src/components/Base.jsx' import CourseForm from './components/CourseForm.jsx'; import FilterListIcon from '@mui/icons-material/FilterList'; -import AddIcon from '@mui/icons-material/Add'; +import SchoolIcon from '@mui/icons-material/School'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import render from '../src/render.jsx'; import { getCookie } from '../src/utils.js'; @@ -105,7 +105,7 @@ function Courses() { > -