diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 7b9b20e..d01391f 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -117,6 +117,7 @@ class Course(models.Model): name = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) is_old = models.BooleanField(default=False) + tags = models.CharField(max_length=128, blank=True) history = HistoricalRecords() diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index 966afbd..8a27db9 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -449,7 +449,7 @@ class StudentTrainingSerializer(TrainingSerializer): class CourseSerializer(serializers.ModelSerializer): class Meta: model = models.Course - fields = ['id', 'name', 'is_old', 'description'] + fields = ['id', 'name', 'is_old', 'description', 'tags'] class SessionSerializer(serializers.ModelSerializer): student_count = serializers.SerializerMethodField() diff --git a/webclient/src/Classes.js b/webclient/src/Classes.js index 0109656..d639e57 100644 --- a/webclient/src/Classes.js +++ b/webclient/src/Classes.js @@ -1,12 +1,13 @@ import React, { useState, useEffect, useReducer } from 'react'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; import './light.css'; -import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; +import { Label, Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import moment from 'moment-timezone'; import { isAdmin, isInstructor, getInstructor, BasicTable, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js'; import { PayPalPayNow } from './PayPal.js'; +import { tags } from './Courses.js'; function ClassTable(props) { const { classes } = props; @@ -92,6 +93,13 @@ function NewClassTable(props) { + {!!x.course.tags && x.course.tags.split(',').map(name => + + )} + + @@ -140,6 +148,7 @@ function NewClassTable(props) { let classesCache = false; let sortCache = true; +let tagFilterCache = false; export function ClassFeed(props) { const [classes, setClasses] = useState(classesCache); @@ -181,6 +190,7 @@ export function ClassFeed(props) { export function Classes(props) { const [classes, setClasses] = useState(classesCache); const [sortByCourse, setSortByCourse] = useState(sortCache); + const [tagFilter, setTagFilter] = useState(tagFilterCache); const { token, user } = props; useEffect(() => { @@ -194,8 +204,11 @@ export function Classes(props) { }); }, []); - const isTeaching = (x) => x.instructor_id === user.member.id; - const sortDate = (a, b) => a.datetime > b.datetime ? 1 : -1; + const byTeaching = (x) => x.instructor_id === user.member.id; + const byDate = (a, b) => a.datetime > b.datetime ? 1 : -1; + const byTag = (x) => tagFilter ? x.course_data.tags.includes(tagFilter) : true; + + console.log(tagFilter); return ( @@ -203,38 +216,70 @@ export function Classes(props) {

Click here to view the list of all courses.

- {!!user && !!classes.length && !!classes.filter(isTeaching).length && + {!!user && !!classes.length && !!classes.filter(byTeaching).length && <>
Classes You're Teaching
- + } - +

+ + + +

+ +

+ Filter by tag: +

+ {Object.entries(tags).map(([name, color]) => + + )} +
+

+

+ {tagFilter && } +

- {classes.length ? sortByCourse ? - + : - + :

Loading...

} diff --git a/webclient/src/Courses.js b/webclient/src/Courses.js index 27c05fb..0aaf90f 100644 --- a/webclient/src/Courses.js +++ b/webclient/src/Courses.js @@ -1,17 +1,35 @@ import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; import './light.css'; -import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; +import { Button, Label, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import moment from 'moment-timezone'; import { isInstructor, getInstructor, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; import { InstructorCourseList, InstructorCourseDetail } from './InstructorCourses.js'; import { InstructorClassList } from './InstructorClasses.js'; +export const tags = { + Protospace: 'black', + Laser: 'red', + CNC: 'orange', + Niche: 'yellow', + //name: 'olive', + Electronics: 'green', + Computers: 'teal', + Metal: 'blue', + //name: 'violet', + Event: 'purple', + Outing: 'pink', + Wood: 'brown', + Misc: 'grey', +}; + let courseCache = false; +let tagFilterCache = false; export function Courses(props) { const [courses, setCourses] = useState(courseCache); + const [tagFilter, setTagFilter] = useState(tagFilterCache); const { token, user } = props; useEffect(() => { @@ -25,6 +43,8 @@ export function Courses(props) { }); }, []); + const byTag = (x) => tagFilter ? x.tags.includes(tagFilter) : true; + return (
Courses
@@ -33,6 +53,35 @@ export function Courses(props) { } +

+ Filter by tag: +

+ {Object.entries(tags).map(([name, color]) => + + )} +
+

+

+ {tagFilter && } +

+ {courses ?
@@ -43,10 +92,17 @@ export function Courses(props) { {courses.length ? - courses.map(x => + courses.filter(byTag).map(x => {x.name} + + {!!x.tags && x.tags.split(',').map(name => + + )} + ) diff --git a/webclient/src/light.css b/webclient/src/light.css index f07fdfb..fd7be49 100644 --- a/webclient/src/light.css +++ b/webclient/src/light.css @@ -129,6 +129,10 @@ body { margin: 0 1.5em 1em !important; } +.coursetags .ui.tag.label { + margin-top: 1rem; +} + .footer { margin-top: -20rem; background: black;