From 447988e7ca7f169b2e47f58a9d7f1218bb9de6eb Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 16 Apr 2026 14:46:13 -0500 Subject: [PATCH 01/55] Began work on classroom assignments --- src/App.tsx | 1 + src/components/Classrooms/TeacherTabs.tsx | 47 +++++++++++++++++++++++ src/pages/ClassroomsDashboard.tsx | 1 + src/pages/Root.tsx | 24 ++++++------ 4 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/components/Classrooms/TeacherTabs.tsx diff --git a/src/App.tsx b/src/App.tsx index a251801e..a0c3223f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -245,6 +245,7 @@ class App extends React.Component { } /> } /> } /> + } /> } /> diff --git a/src/components/Classrooms/TeacherTabs.tsx b/src/components/Classrooms/TeacherTabs.tsx new file mode 100644 index 00000000..a4dbdab1 --- /dev/null +++ b/src/components/Classrooms/TeacherTabs.tsx @@ -0,0 +1,47 @@ + +import { ThemeProps } from '../constants/theme'; +import { StyleProps } from '../../util/style'; +import LocalizedString from '../../util/LocalizedString'; +import * as React from 'react'; +import { styled } from 'styletron-react'; +import { TabBar } from '..//Layout/TabBar'; +import tr from '@i18n'; + +export interface TeacherTabsPublicProps extends ThemeProps, StyleProps { + +} + +export interface TeacherTabsPrivateProps extends ThemeProps { + locale: LocalizedString.Language; +} + +type Props = TeacherTabsPublicProps & TeacherTabsPrivateProps; +const TeacherTabs = ({ + theme, + locale, +}: Props) => { + const [tabIndex, setTabIndex] = React.useState(0); + + const tabs: TabBar.TabDescription[] = [ + { + name: LocalizedString.lookup(tr('Home'), locale), + content:
Home
+ }, + { + name: LocalizedString.lookup(tr('Assignments'), locale), + content:
Assignments
+ }, + { + name: LocalizedString.lookup(tr('People'), locale), + content:
People
+ }, + ]; + + return ( +
+ yay! +
+ ) +} + +) \ No newline at end of file diff --git a/src/pages/ClassroomsDashboard.tsx b/src/pages/ClassroomsDashboard.tsx index c9a640d2..280edb8f 100644 --- a/src/pages/ClassroomsDashboard.tsx +++ b/src/pages/ClassroomsDashboard.tsx @@ -149,6 +149,7 @@ class ClassroomsDashboard extends React.PureComponent { onClick={() => this.props.navigate(`/classrooms/${userId}/teacherView`)} /> + {showTour && ( diff --git a/src/pages/Root.tsx b/src/pages/Root.tsx index effb3dbd..b1e7e0cc 100644 --- a/src/pages/Root.tsx +++ b/src/pages/Root.tsx @@ -740,8 +740,8 @@ class Root extends React.Component { ), robot: this.props.robots[ - Dict.unique(Scene.robots(Async.latestValue(this.props.scene))) - ?.robotId ?? 'demobot' + Dict.unique(Scene.robots(Async.latestValue(this.props.scene))) + ?.robotId ?? 'demobot' ], locale: this.props.locale, }); @@ -1476,21 +1476,23 @@ class Root extends React.Component { )} {modal.type === Modal.Type.DeleteRecord && modal.record.type === Record.Type.Scene && ( - - )} + + )} {modal.type === Modal.Type.SettingsScene && ( )} {modal.type === Modal.Type.ResetCode && ( From 7798b8fd8ba594bc277ea00c110bf34bd75868b8 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 23 Apr 2026 14:05:37 -0500 Subject: [PATCH 02/55] Added classroom assignment interface --- src/state/State/Classroom/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/state/State/Classroom/index.ts b/src/state/State/Classroom/index.ts index 1b2867f2..5c014dc2 100644 --- a/src/state/State/Classroom/index.ts +++ b/src/state/State/Classroom/index.ts @@ -9,10 +9,20 @@ export interface Classroom { studentIds: Dict<{ id: string, displayName: string }>; // IDs of students in the classroom teacherId: string; // ID of the teacher teacherDisplayName: string; // Display name of the teacher - docId?: string; // document ID in the database type: 'classroom'; + docId?: string; // document ID in the database + classroomAssignments?: Dict; // assignments in the classroom, keyed by assignment ID +} +export interface ClassroomAssignment { + assignmentId: string; + title: string; + description?: string; + points?: number | ''; // points can be a number or an empty string if not set + dueDate?: string; // ISO string + docId?: string; // assignment document ID in the database + challenges?: string[]; // list of challenge IDs included in the assignment } export namespace Classroom { @@ -45,6 +55,7 @@ export namespace AsyncClassroom { export const unloaded = (brief: ClassroomBrief): AsyncClassroom => ({ type: Async.Type.Unloaded, brief, + }); export const loaded = (classroom: Classroom): AsyncClassroom => ({ From 4082f3216b0124800f89f4b6756ece31dba02b83 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 23 Apr 2026 14:06:13 -0500 Subject: [PATCH 03/55] Redoing UI for teacher view to allow teachers to see more details about created classroom and assign challenges to students --- src/pages/ClassroomTeacherView.tsx | 183 ++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 30 deletions(-) diff --git a/src/pages/ClassroomTeacherView.tsx b/src/pages/ClassroomTeacherView.tsx index 01e0896c..1e0d8c63 100644 --- a/src/pages/ClassroomTeacherView.tsx +++ b/src/pages/ClassroomTeacherView.tsx @@ -26,13 +26,18 @@ import { DeleteDialog } from '../components/Dialog'; import ClassroomLeaderboardsDialog from '../components/Dialog/ClassroomLeaderboardsDialog'; import Challenge from '../components/Challenge'; import { AsyncChallenge } from '../state/State/Challenge'; -import { Challenges, ChallengeCompletions } from '../state/State'; +import { Challenges, ChallengeCompletions, Classrooms } from '../state/State'; import { Project } from 'state/State/Project'; import TourTarget from '../components/Tours/TourTarget'; import { TourRegistry } from '../tours/TourRegistry'; import GuidedTour from '../components/Tours/GuidedTour'; import TourDoc, { getTeacherViewTourSteps, getTourSteps, TourStep } from '../tours/Tours'; import { completeTour, fetchTourIfNeeded, retakeTour } from '../state/reducer/tours'; +import TeacherTabs from '../components/Classrooms/TeacherTabs'; +import { Card } from '../components/interface/Card'; +import CreateAssignmentView from '../components/Classrooms/CreateAssignmentView'; +import AssignToDialog from '../components/Dialog/AssignToDialog'; + export interface ClassroomTeacherViewRootRouteParams { classroomId: string; @@ -110,6 +115,8 @@ interface ClassroomTeacherViewState { deleteObject?: IvyGateClassroom | User | null; currentTourStepIndex?: number; continueTour?: boolean; + currentSelectedClassroom?: AsyncClassroom | null; + createAssignmentVisible?: boolean; } interface ClickProps { @@ -142,15 +149,62 @@ const ClassroomsTitleContainer = styled('div', (props: ThemeProps) => ({ display: 'flex', flexDirection: 'column', margin: '20px', + +})); + +const ClassroomsCardContainer = styled('div', (props: ThemeProps) => ({ + alignItems: 'center', + justifyContent: 'flex-start', + display: 'flex', + flexDirection: 'row', + margin: '20px', +})); + +const ClassroomCardScrollContainer = styled('div', { + width: '100%', + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + height: '32%', + scrollbarWidth: 'thin', + scrollbarColor: 'rgba(121,121,121,0.6) transparent', + //backgroundColor: 'purple', + '::-webkit-scrollbar': { + width: '14px', + height: '14px', + }, + '::-webkit-scrollbar-track': { + background: 'transparent', + }, + '::-webkit-scrollbar-thumb': { + backgroundColor: 'rgba(121,121,121,0.4)', + borderRadius: '8px', + }, + '::-webkit-scrollbar-thumb:hover': { + backgroundColor: 'rgba(121,121,121,0.7)', + }, +}); + +const CardWrapper = styled('div', (props: ThemeProps & { selected?: boolean }) => ({ + borderRadius: `${props.theme.itemPadding * 4}px`, + cursor: 'pointer', + backgroundColor: props.selected ? 'pink' : 'transparent', })); +const StickyButtonWrap = styled('div', { + position: 'absolute', + left: '20px', + top: '6%', + zIndex: 2, +}); + const ClassroomHeaderContainer = styled('div', (props: ThemeProps) => ({ display: 'flex', flexDirection: 'row', justifyContent: 'center', gap: '3em', - width: '90vw' - + backgroundColor: 'pink', + width: '90vw', + height: '90vh' })); const ManageClassroomsContainer = styled('div', (props: ThemeProps) => ({ @@ -163,7 +217,7 @@ const ManageClassroomsContainer = styled('div', (props: ThemeProps) => ({ height: '100%', })); -const Button = styled('div', (props: ThemeProps & ClickProps) => ({ +const Button = styled('div', (props: ThemeProps) => ({ display: 'flex', alignItems: 'center', flexDirection: 'row', @@ -173,7 +227,7 @@ const Button = styled('div', (props: ThemeProps & ClickProps) => ({ ':last-child': { borderBottom: 'none' }, - opacity: props.disabled ? '0.5' : '1.0', + //opacity: props.disabled ? '0.5' : '1.0', fontWeight: 400, ':hover': { cursor: 'pointer', @@ -183,6 +237,9 @@ const Button = styled('div', (props: ThemeProps & ClickProps) => ({ transition: 'background-color 0.2s, opacity 0.2s' })); + + + export const IVYGATE_LANGUAGE_MAPPING: Dict = { 'ecmascript': 'javascript', 'python': 'customPython', @@ -208,6 +265,8 @@ class ClassroomTeacherView extends React.Component { challenges: {}, showCreateClassroomDialog: false, isStudentInClassroom: null as boolean | null, + currentSelectedClassroom: null, + showJoinClassroomDialog: false, showClassroomLeaderboardSelector: false, showSelectedClassroomLeaderboard: false, @@ -229,6 +288,7 @@ class ClassroomTeacherView extends React.Component { } componentDidUpdate(prevProps: Props, prevState: State) { + console.log("componentDidUpdate called with props: ", this.props, " and state: ", this.state); if (prevProps.classroomList !== this.props.classroomList) { this.getIvygateClassrooms(); } @@ -534,41 +594,104 @@ class ClassroomTeacherView extends React.Component { void retakeTour(this.props.tour, this.props.uid, TourDoc.IDS.TEACHER_VIEW); }; + private exisitingClassroomCards = () => { + const { classroomList, theme, locale } = this.props; + + console.log("existingClassroomCards: ", classroomList); + console.log("Object.entries(classroomList): ", Object.entries(classroomList)); + return Object.entries(classroomList).map(([id, asyncClassroom]) => { + console.log("Checking classroom: ", id, asyncClassroom); + if (asyncClassroom.type === Async.Type.Loaded && classroomList !== null) { + const classroom = asyncClassroom.value; + return ( + this.setState({ currentSelectedClassroom: asyncClassroom })}> + this.setState({ currentSelectedClassroom: asyncClassroom })} + title={classroom.classroomId} + theme={theme} + customheight='150px' + customwidth='200px' + backgroundImage={'linear-gradient(#3b3c3c, transparent), url(../../static/example_images/classroom-botguy.png)'} + backgroundPosition={'center top'} + custommargin='10px' + /> + + ); + } + return null; + }).filter(card => card !== null); + + }; + + private handleAssignemntAction = (currentSelectedClassroom: AsyncClassroom | null, action: 'edit' | 'delete') => { + console.log("handleAssignmentAction called with classroom: ", currentSelectedClassroom, " and action: ", action); + this.setState({ createAssignmentVisible: true }); + }; render() { const { props, state } = this; - const { style } = props; - const { showAreYouSureDialog, deleteObject } = state; + const { style, locale } = props; + const { showAreYouSureDialog, deleteObject, showCreateClassroomDialog, createAssignmentVisible } = state; const theme = DARK; const showTour = props.tourLoaded && !props.tour.completed; return ( - - -

{LocalizedString.lookup(tr('Classrooms - Teacher View'), props.locale)}

- {this.props.classroomList && Object.keys(this.props.classroomList).length > 0 && ( - )} - - {this.renderManageClassrooms()} - {showAreYouSureDialog && ( - - - )} - - - -
- -
+ {createAssignmentVisible ? ( + + this.setState({ createAssignmentVisible: false })} + theme={theme} + classroom={state.currentSelectedClassroom} + + /> + ) : ( + + + + + + + + + {this.exisitingClassroomCards()} + + + + + + + {showAreYouSureDialog && ( + + + )} + { + showCreateClassroomDialog && ( + + )} + + ) + }
+ {showTour && ( Date: Thu, 23 Apr 2026 14:06:24 -0500 Subject: [PATCH 04/55] Added custom css to Card --- src/components/interface/Card.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/interface/Card.tsx b/src/components/interface/Card.tsx index b090a4d1..6cc24be6 100644 --- a/src/components/interface/Card.tsx +++ b/src/components/interface/Card.tsx @@ -14,6 +14,9 @@ export interface CardProps extends StyleProps, ThemeProps { hoverBackgroundSize?: string; selected?: boolean; onClick: (event: React.MouseEvent) => void; + customheight?: string; + customwidth?: string; + custommargin?: string; } interface CardState { } @@ -27,13 +30,16 @@ const Container = styled('div', (props: ThemeProps & { backgroundposition: string; backgroundsize: string; hoverbackgroundsize: string; + customwidth: string; + customheight: string; + custommargin: string; }) => ({ width: '100%', height: '100%', - minWidth: '320px', - maxWidth: '350px', - minHeight: '320px', - maxHeight: '350px', + minWidth: props.customwidth ? props.customwidth : '320px', + maxWidth: props.customwidth ? props.customwidth : '350px', + minHeight: props.customheight ? props.customheight : '320px', + maxHeight: props.customheight ? props.customheight : '350px', display: 'flex', flexDirection: 'column', alignItems: 'center', @@ -41,7 +47,7 @@ const Container = styled('div', (props: ThemeProps & { backdropFilter: 'blur(16px)', paddingTop: `${props.theme.itemPadding * 2}px`, paddingBottom: '0px', - margin: '20px 20px 0px 20px', + margin: props.custommargin ? props.custommargin : '20px 20px 0px 20px', backgroundColor: props.backgroundcolor ? props.backgroundcolor : props.theme.backgroundColor, borderRadius: `${props.theme.itemPadding * 4}px`, border: `1px solid ${props.theme.borderColor}`, @@ -119,7 +125,10 @@ export class Card extends React.Component { backgroundposition={backgroundPosition} backgroundsize={backgroundSize} hoverbackgroundsize={hoverBackgroundSize} + customwidth={this.props.customwidth} + customheight={this.props.customheight} onClick={onClick} + custommargin={this.props.custommargin} >
{title}
From 682cfb19565ec66498cb66c1b8bbaeb157485008 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 23 Apr 2026 14:06:45 -0500 Subject: [PATCH 05/55] Dialog pop up for selecting what students get assigned created assignment --- src/components/Dialog/AssignToDialog.tsx | 154 +++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/components/Dialog/AssignToDialog.tsx diff --git a/src/components/Dialog/AssignToDialog.tsx b/src/components/Dialog/AssignToDialog.tsx new file mode 100644 index 00000000..6675be42 --- /dev/null +++ b/src/components/Dialog/AssignToDialog.tsx @@ -0,0 +1,154 @@ +import { AsyncClassroom } from "../../state/State/Classroom"; +import * as React from 'react'; +import { styled } from 'styletron-react'; +import Async from '../../state/State/Async'; +import { Dialog } from './Dialog'; +import DialogBar from './DialogBar'; +import { ThemeProps, GREEN, RED } from '../constants/theme'; + +import tr from '@i18n'; +import LocalizedString from '../../util/LocalizedString'; + +import { connect } from 'react-redux'; +import { State as ReduxState, State } from '../../state'; +import Dict from '../../util/objectOps/Dict'; +import { sprintf } from 'sprintf-js'; +import { StyleProps } from "../../util/style"; +import Input from "../interface/Input"; + + +export interface AssignToDialogPublicProps extends StyleProps, ThemeProps { + onClose: () => void; + classroom: AsyncClassroom; + selectedStudents: (students: Dict<{ id: string, displayName: string }>) => void; +} + +export interface AssignToDialogPrivateProps extends ThemeProps { + locale: LocalizedString.Language; +} + +type Props = AssignToDialogPublicProps & AssignToDialogPrivateProps; + +const Container = styled('div', (props: ThemeProps) => ({ + display: 'flex', + flexDirection: 'column', + backgroundColor: props.theme.backgroundColor, + color: props.theme.color, + height: 'auto', + margin: '1em', + zIndex: 100, +})); + +const CheckboxRow = styled('div', (props: ThemeProps) => ({ + gap: '0.5em', + fontWeight: 500, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +})); + +const StyledCheckbox = styled(Input, (props: ThemeProps) => ({ + transform: 'scale(1.4)', + width: 'auto' +})); + +const ButtonContainer = styled('div', (theme: ThemeProps) => ({ + display: 'flex', + flexDirection: 'row', + marginTop: `${theme.theme.itemPadding * 4}px`, + marginBottom: `${theme.theme.itemPadding * 2}px`, +})); + +const Finalize = styled('div', (props: ThemeProps & { disabled?: boolean }) => ({ + flex: '1 1', + borderRadius: `${props.theme.itemPadding * 2}px`, + padding: `${props.theme.itemPadding * 2}px`, + backgroundColor: GREEN.standard, + ':hover': { + backgroundColor: GREEN.hover, + }, + fontWeight: 400, + fontSize: '1.1em', + textAlign: 'center', + cursor: 'pointer', +})); + +const AssignTo = ({ + onClose, + classroom, + theme, + locale, + selectedStudents +}: Props) => { + const [selectedIds, setSelectedIds] = React.useState>({}); + const loadedClassroom = Async.latestValue(classroom); + const allSelected = + loadedClassroom && + Object.keys(selectedIds).length === Object.keys(loadedClassroom.studentIds || {}).length; + + const students = loadedClassroom?.studentIds || {}; + console.log("AssignToDialog loadedClassroom: ", loadedClassroom); + //const allSelected = Object.keys(students).length > 0 && Object.keys(selectedIds).length === Object.keys(students).length; + + const toggleAll = () => { + if (allSelected) { + setSelectedIds({}); + } else { + setSelectedIds(loadedClassroom.studentIds || {}); + } + }; + + const toggleStudent = (studentId: string) => { + setSelectedIds(prev => + prev.hasOwnProperty(studentId) + ? Object.fromEntries( + Object.entries(prev).filter(([id]) => id !== studentId) + ) + : { ...prev, [studentId]: loadedClassroom.studentIds[studentId] } + ); + }; + + return ( + + +
+ + { toggleAll(); }} /> + + + { + Object.values(loadedClassroom?.studentIds || {}).map(student => ( + + toggleStudent(student.id)} /> + + + )) + } +
+ + + { console.log("Selected student IDs: ", selectedIds); selectedStudents(selectedIds); onClose(); }} + > + {LocalizedString.lookup(tr('Done'), locale)} + + +
+ +
+ ) +} + +export default connect((state: State) => { + return { + locale: state.i18n.locale, + classroomList: state.classrooms.entities, + } +}, (dispatch, ownProps) => ({ + +}))(AssignTo) as React.ComponentType; \ No newline at end of file From 9c094cf4ef7ab5a8f2ed7ec155ac20f5005575fd Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 23 Apr 2026 14:07:12 -0500 Subject: [PATCH 06/55] Added various teacher tabs for teacher view including Home, Assignments, People and Grades --- src/components/Classrooms/TeacherTabs.tsx | 111 ++++++++++++++++++++-- 1 file changed, 102 insertions(+), 9 deletions(-) diff --git a/src/components/Classrooms/TeacherTabs.tsx b/src/components/Classrooms/TeacherTabs.tsx index a4dbdab1..be99e1bf 100644 --- a/src/components/Classrooms/TeacherTabs.tsx +++ b/src/components/Classrooms/TeacherTabs.tsx @@ -1,14 +1,23 @@ -import { ThemeProps } from '../constants/theme'; +import { Theme, ThemeProps } from '../constants/theme'; import { StyleProps } from '../../util/style'; import LocalizedString from '../../util/LocalizedString'; import * as React from 'react'; import { styled } from 'styletron-react'; import { TabBar } from '..//Layout/TabBar'; import tr from '@i18n'; +import { faHome, faSchool, faPeopleGroup, faFileCircleCheck } from '@fortawesome/free-solid-svg-icons'; +import { State } from '../../state'; +import { connect } from 'react-redux'; +import { FontAwesome } from '../FontAwesome'; +import PeopleView from './PeopleView'; +import { AsyncClassroom, Classroom } from '../../state/State/Classroom'; +import { current } from 'immer'; +import AssignmentsView from './AssignmentsView'; export interface TeacherTabsPublicProps extends ThemeProps, StyleProps { - + currentSelectedClassroom: AsyncClassroom | null; + onAssignmentAction: (currentSelectedClassroom: AsyncClassroom | null, action: 'edit' | 'delete') => void; } export interface TeacherTabsPrivateProps extends ThemeProps { @@ -16,32 +25,116 @@ export interface TeacherTabsPrivateProps extends ThemeProps { } type Props = TeacherTabsPublicProps & TeacherTabsPrivateProps; + +const Container = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + color: $theme.color, + //backgroundColor: $theme.backgroundColor, + backgroundColor: 'lightblue' + // minHeight: '100vh', +})); + +const Body = styled('div', { + flex: 1, + display: 'flex', + flexDirection: 'row', + '@screen and (max-width: 800px)': { + flexDirection: 'column', + }, +}); + +const TopBar = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + borderBottom: `1px solid ${$theme.borderColor}`, + height: '48px', +})); + +const StyledTabBar = styled(TabBar, ({ theme }: ThemeProps) => ({ + flex: 1, + borderTopLeftRadius: `${theme.itemPadding * 2}px`, + borderTopRightRadius: `${theme.itemPadding * 2}px`, + alignSelf: 'end', + borderTop: `1px solid ${theme.borderColor}`, + borderLeft: `1px solid ${theme.borderColor}`, + borderRight: `1px solid ${theme.borderColor}`, + backgroundColor: theme.backgroundColor, + ':last-child': { + marginRight: `${theme.itemPadding * 2}px`, + } + +})); + +const TopFa = styled(FontAwesome, ({ $theme }: { $theme: Theme }) => ({ + paddingLeft: `${$theme.itemPadding * 2}px`, + paddingRight: `${$theme.itemPadding * 2}px`, + fontSize: '32px', +})); + const TeacherTabs = ({ theme, locale, + currentSelectedClassroom, + onAssignmentAction, }: Props) => { const [tabIndex, setTabIndex] = React.useState(0); + const [peopleContextMenu, setPeopleContextMenu] = React.useState({ visible: false, x: 0, y: 0 }); const tabs: TabBar.TabDescription[] = [ { name: LocalizedString.lookup(tr('Home'), locale), - content:
Home
+ icon: faHome }, { name: LocalizedString.lookup(tr('Assignments'), locale), - content:
Assignments
+ icon: faSchool }, { name: LocalizedString.lookup(tr('People'), locale), - content:
People
+ icon: faPeopleGroup }, + { + name: LocalizedString.lookup(tr('Grades'), locale), + icon: faFileCircleCheck + } ]; return ( -
- yay! -
+ { setPeopleContextMenu({ ...peopleContextMenu, visible: false }); console.log("container click"); }}> + + + + + {currentSelectedClassroom ? ( + + {tabIndex === 0 &&
Home
} + {tabIndex === 1 && } + {tabIndex === 2 && } + {tabIndex === 3 &&
Grades
} + + ) : ( + +

{LocalizedString.lookup(tr('Select a classroom to view details'), locale)}

+ + )} +
) } -) \ No newline at end of file +export default connect((state: State) => { + return { + locale: state.i18n.locale, + } +}, (dispatch, ownProps) => ({ + +}))(TeacherTabs) as React.ComponentType; \ No newline at end of file From 47513c6db8cab17599722e9a2f75b7ee565c777d Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 23 Apr 2026 14:07:26 -0500 Subject: [PATCH 07/55] People view for teacher classroom view to see who is in the classroom --- src/components/Classrooms/PeopleView.tsx | 234 +++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/components/Classrooms/PeopleView.tsx diff --git a/src/components/Classrooms/PeopleView.tsx b/src/components/Classrooms/PeopleView.tsx new file mode 100644 index 00000000..79b3477e --- /dev/null +++ b/src/components/Classrooms/PeopleView.tsx @@ -0,0 +1,234 @@ + +import { Theme, ThemeProps } from '../constants/theme'; +import { StyleProps } from '../../util/style'; +import LocalizedString from '../../util/LocalizedString'; +import * as React from 'react'; +import { styled } from 'styletron-react'; +import { TabBar } from '..//Layout/TabBar'; +import tr from '@i18n'; +import { faPersonChalkboard, faUser, faEllipsisVertical } from '@fortawesome/free-solid-svg-icons'; +import { State } from '../../state'; +import { connect } from 'react-redux'; +import { FontAwesome } from '../FontAwesome'; +import { AsyncClassroom } from '../../state/State/Classroom'; +import Dict from '../../util/objectOps/Dict'; +import { current } from 'immer'; +import { get } from 'immer/dist/internal'; +import { useState } from 'react'; +import Async from 'state/State/Async'; + +export interface PeopleViewPublicProps extends ThemeProps, StyleProps { + currentSelectedClassroom: AsyncClassroom | null; + contextMenuVisible: boolean; + setContextMenuVisible: React.Dispatch>; +} + +export interface PeopleViewPrivateProps extends ThemeProps { + locale: LocalizedString.Language; + classroomList: Dict; +} + +type Props = PeopleViewPublicProps & PeopleViewPrivateProps; + +const Container = styled('div', (props: ThemeProps) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + color: props.theme.color, + backgroundColor: props.theme.backgroundColor, + //minHeight: '100vh', +})); + +const TeacherStudentContainer = styled('div', { + display: 'flex', + flexDirection: 'row', + '@screen and (max-width: 800px)': { + flexDirection: 'column', + }, + // backgroundColor: 'pink', + margin: '8px', +}); + +const TeacherContainer = styled('div', { + flex: 1, + display: 'flex', + flexDirection: 'column', + borderColor: 'lightblue', + alignItems: 'center', + padding: '8px', + borderWidth: '4px', + borderStyle: 'solid', +}); + +const StudentContainer = styled('div', { + flex: 1, + display: 'flex', + flexDirection: 'column', + borderColor: 'lightgreen', + borderWidth: '4px', + alignItems: 'center', + padding: '8px', + borderStyle: 'solid', +}); +const Icon = styled(FontAwesome, { + paddingRight: "5px", + height: "1.5em", +}); + +const StudentRow = styled('div', (props: ThemeProps) => ({ + display: 'flex', + flexDirection: 'row', + ':hover': { + cursor: 'pointer', + backgroundColor: props.theme.hoverFileBackground + }, + padding: '4px', + borderRadius: '4px', + width: '100%', + justifyContent: 'space-between', + alignItems: 'center', + zIndex: 0, +})); + +const ContextMenu = styled('div', (props: ThemeProps & { x: number; y: number }) => ({ + position: "absolute", + top: `${props.y}px`, + left: `${props.x}px`, + background: props.theme.contextMenuBackground, + border: `2px solid ${props.theme.borderColor}`, + borderRadius: "4px", + boxShadow: "0px 4px 6px hsla(0, 0.00%, 0.00%, 0.10)", + zIndex: 1000, +})); + +const ContextMenuItem = styled('div', (props: ThemeProps) => ({ + listStyle: "none", + padding: "10px", + color: props.theme.color, + margin: 0, + cursor: "pointer", + ':hover': { + cursor: 'pointer', + backgroundColor: `${props.theme.hoverFileBackground}` + }, +})); + +const PeopleView = ({ + theme, + locale, + classroomList, + currentSelectedClassroom, + contextMenuVisible, + setContextMenuVisible +}: Props) => { + console.log("people view contextMenuVisible: ", contextMenuVisible) + console.log("people view classroom list", classroomList) + const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 }); + console.log("contextMenu state: ", contextMenu); + function getTeachers(currentSelectedClassroom: AsyncClassroom | null) { + const teachers = Async.latestValue(currentSelectedClassroom)?.teacherDisplayName; + return ( +
+ + {teachers} +
+ ); + } + + function getStudents(currentSelectedClassroom: AsyncClassroom | null) { + + const students = Async.latestValue(currentSelectedClassroom)?.studentIds; + console.log("getStudents: ", students); + for (const student of Object.values(students || {})) { + console.log("student", student); + } + return ( +
+ {students ? ( + Object.values(students).map((student: { displayName: string, id: string }, index) => ( +
+ + {student.displayName} +
+ )) + + + ) : ( +
No students in this classroom.
+ )} + console.log("student row clicked!")}> +
+ + Student Test 1 +
+ { + e.stopPropagation(); + console.log("e.clientX, e.clientY", e.clientX, e.clientY); + setContextMenuVisible({ visible: true, x: e.clientX, y: e.clientY }); + setContextMenu({ visible: true, x: e.clientX, y: e.clientY }); + }} /> +
+ {/* + + Student Test 2 + */} +
+ ) + } + + function renderContextMenu(x: number, y: number) { + console.log("rendering context menu at", x, y); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + const menuWidth = 200; + const menuHeight = 185; + + const adjustedX = Math.min(x, viewportWidth - menuWidth); + const adjustedY = Math.min(y, viewportHeight - (menuHeight + 50)); + + return ( + console.log("context menu clicked")}> + +
  • { + console.log("remove user from classroom"); + setContextMenuVisible({ visible: false, x: adjustedX, y: adjustedY }); + }} + > + {LocalizedString.lookup(tr("Remove User from Classroom"), locale)} +
  • +
    +
    + ) + } + return ( + setContextMenu({ ...contextMenu, visible: false })}> + + +

    {LocalizedString.lookup(tr('Teachers'), locale)}

    + {currentSelectedClassroom && ( + getTeachers(currentSelectedClassroom) + )} +
    + +

    {LocalizedString.lookup(tr('Students'), locale)}

    + {currentSelectedClassroom && ( + getStudents(currentSelectedClassroom) + )} +
    +
    + {contextMenuVisible && renderContextMenu(contextMenu.x, contextMenu.y)} +
    + ) +} + +export default connect((state: State) => { + return { + locale: state.i18n.locale, + classroomList: state.classrooms.entities, + } +}, (dispatch, ownProps) => ({ + +}))(PeopleView) as React.ComponentType; \ No newline at end of file From e2ace5a4a6b6866a6a11be5e13a2474758bf6ae5 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 23 Apr 2026 14:07:40 -0500 Subject: [PATCH 08/55] Interface for creating classroom assignments --- .../Classrooms/CreateAssignmentView.tsx | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/components/Classrooms/CreateAssignmentView.tsx diff --git a/src/components/Classrooms/CreateAssignmentView.tsx b/src/components/Classrooms/CreateAssignmentView.tsx new file mode 100644 index 00000000..a8b63c78 --- /dev/null +++ b/src/components/Classrooms/CreateAssignmentView.tsx @@ -0,0 +1,335 @@ + +import { Theme, ThemeProps } from '../constants/theme'; +import { StyleProps } from '../../util/style'; +import LocalizedString from '../../util/LocalizedString'; +import * as React from 'react'; +import { styled } from 'styletron-react'; +import { TabBar } from '../Layout/TabBar'; +import tr from '@i18n'; +import { faFileLines, faXmark, faEllipsisVertical } from '@fortawesome/free-solid-svg-icons'; +import { State } from '../../state'; +import { connect } from 'react-redux'; +import { FontAwesome } from '../FontAwesome'; +import { AsyncClassroom, ClassroomAssignment } from '../../state/State/Classroom'; +import Dict from '../../util/objectOps/Dict'; +import { useEffect, useState } from 'react'; +import Input from '../interface/Input'; +import Async from 'state/State/Async'; +import AssignToDialog from '../Dialog/AssignToDialog'; +import TextArea from '../interface/TextArea'; + +import { Challenges } from '../../state/State'; +import ScrollArea from '../interface/ScrollArea'; + +export interface CreateAssignmentViewPublicProps extends ThemeProps, StyleProps { + onClose: () => void; + classroom: AsyncClassroom; +} + +export interface CreateAssignmentViewPrivateProps extends ThemeProps { + locale: LocalizedString.Language; + challenges: Challenges; +} +interface ClickProps { + onClick?: (event: React.MouseEvent) => void; + disabled?: boolean; +} +type Props = CreateAssignmentViewPublicProps & CreateAssignmentViewPrivateProps; + +const Container = styled('div', (props: ThemeProps) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + color: props.theme.color, + backgroundColor: props.theme.backgroundColor, + //zIndex: 100, + //minHeight: '100vh', +})); + +const TopRibbon = styled('div', (props: ThemeProps) => ({ + display: 'flex', + flexDirection: 'row', + margin: '8px', + alignItems: 'center', + justifyContent: 'space-between', +})); + +const Icon = styled(FontAwesome, { + paddingRight: "5px", +}); + +const AssignmentInputContainer = styled('div', (props: ThemeProps) => ({ + display: 'flex', + flexDirection: 'column', + borderColor: props.theme.borderColor, + borderWidth: '4px', + borderStyle: 'solid', + borderRadius: `${props.theme.itemPadding * 2}px`, + padding: '1em', + margin: '8px', + gap: '1.4em', + backgroundColor: 'lightgreen', + width: '100%', + height: '70vh' +})); + +const AssignmentInfoContainer = styled('div', (props: ThemeProps) => ({ + display: 'flex', + flexDirection: 'column', + borderColor: props.theme.borderColor, + borderWidth: '4px', + borderStyle: 'solid', + borderRadius: `${props.theme.itemPadding * 2}px`, + padding: '1em', + margin: '8px', + backgroundColor: 'lightgreen', + gap: '1.4em', + //width: '100%', + +})); + +const AssignmentInfoRow = styled('div', (props: ThemeProps) => ({ + gap: '0.5em', + fontWeight: 500, + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start' + +})); + +const AssignmentInfoContent = styled('div', (props: ThemeProps) => ({ + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + fontSize: '1.2em', + marginLeft: '1.4em' +})); + +const Button = styled('div', (props: ThemeProps & ClickProps) => ({ + display: 'flex', + alignItems: 'center', + fontSize: '1.2em', + flexDirection: 'row', + padding: '10px', + opacity: props.disabled ? "0.5" : "1.0", + backgroundColor: '#2c2c2cff', + borderBottom: `1px solid ${props.theme.borderColor}`, + ':last-child': { + borderBottom: 'none' + }, + //opacity: props.disabled ? '0.5' : '1.0', + fontWeight: 400, + ":hover": + props.onClick && !props.disabled + ? { + cursor: "pointer", + backgroundColor: `rgba(255, 255, 255, 0.1)`, + } + : {}, + userSelect: 'none', + transition: 'background-color 0.2s, opacity 0.2s', +})); + +const StyledCheckbox = styled(Input, (props: ThemeProps) => ({ + transform: 'scale(1.4)', + width: 'auto' +})); + +const StyledScrollArea = styled(ScrollArea, ({ theme }: ThemeProps) => ({ + flex: 1, +})); + +const CheckboxRow = styled('div', (props: ThemeProps) => ({ + gap: '0.5em', + fontWeight: 500, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +})); + + +const CreateAssignmentView = ({ + theme, + locale, + onClose, + classroom, + challenges +}: Props) => { + console.log("CreateAssignmentView rendered with classroom: ", classroom); + console.log("CreateAssignmentView prop challenges: ", challenges); + const loadedClassroom = Async.latestValue(classroom); + const [assignToMenuVisible, setAssignToMenuVisible] = useState(false); + const [selectedStudents, setSelectedStudents] = useState>({}); + const [enableAssign, setEnableAssign] = useState(false); + const [assignmentInfo, setAssignmentInfo] = useState>({ points: 100, dueDate: "No Due Date" }); + console.log("CreateAssignmentView selectedStudents: ", selectedStudents); + + // useEffect(() => { + // console.log("CreateAssignmentView assignmentInfo changed: ", assignmentInfo); + // }, []) + function handleAssign() { + assignmentInfo.dueDate ? console.log("Assignment due date: ", assignmentInfo.dueDate) : console.log("No due date set"); + console.log("Assigning with info: ", assignmentInfo); + }; + + function renderChallengeCheckboxes() { + + Object.values(challenges || {}).map(challenge => { + console.log("Rendering checkbox for challenge: ", Async.latestValue(challenge).description[locale]); + + }) + //
    + // { + // Object.values(challenges || {}).map(challenge => ( + + // + // console.log("Toggled challenge: ", LocalizedString.lookup(Async.latestValue(challenge).name, locale))} /> + // + // + // )) + // } + //
    + return ( +
    + { + Object.values(challenges || {}).map(challenge => ( + + + console.log("Toggled challenge: ", LocalizedString.lookup(Async.latestValue(challenge).name, locale))} /> + + + )) + } +
    ) + } + return ( + + +
    + +
    + + {LocalizedString.lookup(tr('Create Assignment'), locale)} +
    + +
    + +
    +
    + + + + + + + { setEnableAssign((e.target as HTMLInputElement).value.trim().length > 0); setAssignmentInfo({ ...assignmentInfo, title: (e.target as HTMLInputElement).value }); }} required={true} placeholder={LocalizedString.lookup(tr('*Required'), locale)} theme={theme} /> + + + + + + + +