Compare commits

..

No commits in common. "ac01b5c94ccb62c989b397639c7769fe873b9555" and "0a0bbe22c90ef80aedded36dc1fb01a1c5892f49" have entirely different histories.

27 changed files with 150 additions and 453 deletions

View File

@ -1,15 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router' import { RouterLink, RouterView, useRouter } from 'vue-router'
import { useAuthStore } from './stores/auth' import { useAuthStore } from './stores/auth'
import AlertDialog from './components/AlertDialog.vue' import AlertDialog from './components/AlertDialog.vue'
import AnnouncementsBanner from './components/AnnouncementsBanner.vue' import AnnouncementsBanner from './components/AnnouncementsBanner.vue'
import { computed } from 'vue'
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const route = useRoute()
const onLoginPage = computed(() => route.path === '/login')
async function logOut() { async function logOut() {
authStore.logOut() authStore.logOut()
@ -20,19 +16,17 @@ async function logOut() {
<template> <template>
<header> <header>
<div> <div>
<nav class="global-navbar" v-if="!onLoginPage"> <nav class="global-navbar">
<div> <div>
<span style="font-weight: bold; font-size: large;">Teacher Tools</span> <RouterLink to="/">Home</RouterLink>
<RouterLink class="link" to="/">Apps</RouterLink> <RouterLink to="/my-account" v-if="authStore.state">My Account</RouterLink>
<RouterLink class="link" to="/my-account" v-if="authStore.state">My Account</RouterLink> <RouterLink to="/admin-dashboard" v-if="authStore.admin">Admin</RouterLink>
<RouterLink class="link" to="/admin-dashboard" v-if="authStore.admin">Admin</RouterLink>
</div> </div>
<div> <div>
<RouterLink class="link" to="/login" v-if="!authStore.state">Login</RouterLink> <RouterLink to="/login" v-if="!authStore.state">Login</RouterLink>
<span v-if="authStore.state" style="margin-right: 0.25em; font-style: italic; font-size: smaller;"> <span v-if="authStore.state" style="margin-right: 0.5em">
Logged in as <span v-text="authStore.state.username" Logged in as <span v-text="authStore.state.username" style="font-weight: bold;"></span>
style="font-weight: bold; font-style: normal; font-size: medium;"></span>
</span> </span>
<button type="button" @click="logOut" v-if="authStore.state">Log out</button> <button type="button" @click="logOut" v-if="authStore.state">Log out</button>
</div> </div>
@ -58,7 +52,7 @@ async function logOut() {
display: inline-block; display: inline-block;
} }
.global-navbar>div>*+* { .global-navbar>div>a+a {
margin-left: 1em; margin-left: 0.5em;
} }
</style> </style>

View File

@ -2,31 +2,16 @@
* Shows a simple message dialog, with a 'close' button. It uses a pre-rendered * Shows a simple message dialog, with a 'close' button. It uses a pre-rendered
* dialog element that's globally defined for the whole app. * dialog element that's globally defined for the whole app.
* @param msg The message to show. * @param msg The message to show.
* @param alertType The type of alert. "info", "warning", "error"
* @returns A promise that resolves when the alert is closed. * @returns A promise that resolves when the alert is closed.
*/ */
export function showAlert(msg: string, alertType: string = 'info'): Promise<void> { export function showAlert(msg: string): Promise<void> {
const dialog = document.getElementById('alert-dialog') as HTMLDialogElement const dialog = document.getElementById('alert-dialog') as HTMLDialogElement
dialog.classList.remove('alert-dialog-error', 'alert-dialog-warning') // Clear old styling classes if they're present.
if (alertType === 'warning') {
dialog.classList.add('alert-dialog-warning')
} else if (alertType === 'error') {
dialog.classList.add('alert-dialog-error')
}
const messageBox = document.getElementById('alert-dialog-message') as HTMLParagraphElement const messageBox = document.getElementById('alert-dialog-message') as HTMLParagraphElement
const closeButton = document.getElementById('alert-dialog-close-button') as HTMLButtonElement const closeButton = document.getElementById('alert-dialog-close-button') as HTMLButtonElement
closeButton.addEventListener('click', closeAlertDialog) closeButton.addEventListener('click', () => dialog.close())
messageBox.innerText = msg messageBox.innerText = msg
dialog.showModal() dialog.showModal()
return new Promise((resolve) => { return new Promise((resolve) => {
dialog.addEventListener('close', () => { dialog.addEventListener('close', () => resolve())
closeButton.removeEventListener('click', closeAlertDialog)
resolve()
})
}) })
} }
function closeAlertDialog() {
const dialog = document.getElementById('alert-dialog') as HTMLDialogElement
dialog.close()
}

View File

@ -30,29 +30,12 @@ export class APIResponse<T> {
async handleErrorsWithAlert(): Promise<T | null> { async handleErrorsWithAlert(): Promise<T | null> {
const value = await this.result const value = await this.result
if (value instanceof APIError) { if (value instanceof APIError) {
if (value instanceof BadRequestError || value instanceof AuthenticationError) { await showAlert(value.message)
await showAlert(value.message, 'warning')
} else {
await showAlert(value.message, 'error')
}
return null return null
} }
return value return value
} }
async handleErrorsWithAlertNoBody(): Promise<boolean> {
const value = await this.result
if (value instanceof APIError) {
if (value instanceof BadRequestError || value instanceof AuthenticationError) {
await showAlert(value.message, 'warning')
} else {
await showAlert(value.message, 'error')
}
return false
}
return true
}
async getOrThrow(): Promise<T> { async getOrThrow(): Promise<T> {
const value = await this.result const value = await this.result
if (value instanceof APIError) throw value if (value instanceof APIError) throw value

View File

@ -24,8 +24,6 @@ const router = useRouter()
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
max-width: 500px; max-width: 500px;
margin-left: auto;
margin-right: auto;
} }
.class-item:hover { .class-item:hover {

View File

@ -78,9 +78,9 @@ async function resetStudentDesks() {
</script> </script>
<template> <template>
<div v-if="cls"> <div v-if="cls">
<h1 class="align-center" style="margin-bottom: 0;">Class <span v-text="cls.number"></span></h1> <h1>Class <span v-text="cls.number"></span></h1>
<p class="align-center" style="margin-top: 0; margin-bottom: 2em;">For the {{ cls.schoolYear }} school year.</p> <p>School Year: <span v-text="cls.schoolYear"></span></p>
<div class="button-bar align-center" style="margin-bottom: 1em;"> <div class="button-bar" style="margin-bottom: 1em;">
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add <button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/edit-student`)">Add
Student</button> Student</button>
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)">Import <button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)">Import
@ -89,17 +89,15 @@ async function resetStudentDesks() {
<button type="button" @click="deleteThisClass">Delete this Class</button> <button type="button" @click="deleteThisClass">Delete this Class</button>
</div> </div>
<EntriesTable :classId="cls.id" ref="entries-table" /> <h3>Notes</h3>
<div>
<h3 style="margin-bottom: 0.25em;">Notes</h3>
<form @submit.prevent="submitNote"> <form @submit.prevent="submitNote">
<textarea style="min-height: 50px; min-width: 300px;" maxlength="2000" minlength="1" <textarea style="min-height: 50px; min-width: 300px;" maxlength="2000" minlength="1"
v-model="noteContent"></textarea> v-model="noteContent"></textarea>
<button style="vertical-align: top; margin-left: 0.5em;" type="submit">Add Note</button> <button style="vertical-align: top; margin-left: 0.5em;" type="submit">Add Note</button>
</form> </form>
<ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" /> <ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" />
</div>
<EntriesTable :classId="cls.id" ref="entries-table" />
<!-- Confirmation dialog used for attempts at deleting this class. --> <!-- Confirmation dialog used for attempts at deleting this class. -->
<ConfirmDialog ref="deleteClassDialog"> <ConfirmDialog ref="deleteClassDialog">
@ -111,3 +109,4 @@ async function resetStudentDesks() {
</ConfirmDialog> </ConfirmDialog>
</div> </div>
</template> </template>
<style scoped></style>

View File

@ -11,7 +11,7 @@ const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const apiClient = new ClassroomComplianceAPIClient(authStore) const apiClient = new ClassroomComplianceAPIClient(authStore)
onMounted(() => { onMounted(async () => {
apiClient.getClasses().handleErrorsWithAlert().then(result => { apiClient.getClasses().handleErrorsWithAlert().then(result => {
if (result) classes.value = result if (result) classes.value = result
}) })
@ -19,7 +19,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div> <div>
<div class="button-bar align-center"> <div class="button-bar">
<button type="button" @click="router.push('/classroom-compliance/edit-class')">Add Class</button> <button type="button" @click="router.push('/classroom-compliance/edit-class')">Add Class</button>
</div> </div>
<div> <div>

View File

@ -54,16 +54,13 @@ function resetForm() {
} }
</script> </script>
<template> <template>
<div class="centered-content"> <div>
<h2 class="align-center"> <h2>
<span v-if="cls" v-text="'Edit Class ' + cls.id"></span> <span v-if="cls" v-text="'Edit Class ' + cls.id"></span>
<span v-if="!cls">Add New Class</span> <span v-if="!cls">Add New Class</span>
</h2> </h2>
<form @submit.prevent="submitForm" @reset.prevent="resetForm"> <form @submit.prevent="submitForm" @reset.prevent="resetForm">
<p>
Add a new class to your classroom compliance app.
</p>
<div> <div>
<label for="number-input">Number</label> <label for="number-input">Number</label>
<input id="number-input" type="number" v-model="formData.number" required /> <input id="number-input" type="number" v-model="formData.number" required />

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { showAlert } from '@/alerts';
import { APIError } from '@/api/base';
import { import {
ClassroomComplianceAPIClient, ClassroomComplianceAPIClient,
type Class, type Class,
@ -35,35 +37,24 @@ interface StudentFormData {
const formData: Ref<StudentFormData> = ref({ name: '', deskNumber: null, removed: false }) const formData: Ref<StudentFormData> = ref({ name: '', deskNumber: null, removed: false })
onMounted(async () => { onMounted(async () => {
if (!await loadClass()) return
await loadStudent()
})
async function loadClass(): Promise<boolean> {
const classIdNumber = parseInt(props.classId, 10) const classIdNumber = parseInt(props.classId, 10)
cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert() cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert()
if (!cls.value) { if (!cls.value) {
await router.replace('/classroom-compliance') await router.replace('/classroom-compliance')
return false return
} }
return true
}
async function loadStudent() {
const classId = cls.value?.id
if (!classId) return
if ('studentId' in route.query && typeof route.query.studentId === 'string') { if ('studentId' in route.query && typeof route.query.studentId === 'string') {
const studentId = parseInt(route.query.studentId, 10) const studentId = parseInt(route.query.studentId, 10)
student.value = await apiClient.getStudent(classId, studentId).handleErrorsWithAlert() student.value = await apiClient.getStudent(classIdNumber, studentId).handleErrorsWithAlert()
if (!student.value) { if (!student.value) {
await router.replace(`/classroom-compliance/classes/${classId}`) await router.replace(`/classroom-compliance/classes/${classIdNumber}`)
return return
} }
formData.value.name = student.value.name formData.value.name = student.value.name
formData.value.deskNumber = student.value.deskNumber formData.value.deskNumber = student.value.deskNumber
formData.value.removed = student.value.removed formData.value.removed = student.value.removed
} }
} })
async function submitForm() { async function submitForm() {
const classId = parseInt(props.classId, 10) const classId = parseInt(props.classId, 10)
@ -120,42 +111,37 @@ async function doMoveClass() {
if (!choice) return if (!choice) return
const newClassId = moveClassDialogClassSelection.value.id const newClassId = moveClassDialogClassSelection.value.id
const result = await apiClient.moveStudentToOtherClass(cls.value.id, student.value.id, newClassId) const result = await apiClient.moveStudentToOtherClass(cls.value.id, student.value.id, newClassId)
.handleErrorsWithAlertNoBody() if (result instanceof APIError) {
if (result) { await showAlert(result.message)
} else {
await router.replace(`/classroom-compliance/classes/${newClassId}/students/${student.value.id}`) await router.replace(`/classroom-compliance/classes/${newClassId}/students/${student.value.id}`)
} }
} }
</script> </script>
<template> <template>
<div v-if="cls" class="centered-content"> <div v-if="cls">
<h2 class="align-center"> <h2>
<span v-if="student" v-text="'Edit ' + student.name"></span> <span v-if="student" v-text="'Edit ' + student.name"></span>
<span v-if="!student">Add New Student</span> <span v-if="!student">Add New Student</span>
</h2> </h2>
<p class="align-center">In class <span v-text="cls.number + ', ' + cls.schoolYear"></span></p> <p>In class <span v-text="cls.number + ', ' + cls.schoolYear"></span></p>
<form @submit.prevent="submitForm" @reset.prevent="resetForm"> <form @submit.prevent="submitForm" @reset.prevent="resetForm">
<div> <div>
<label for="name-input">Name</label> <label for="name-input">Name</label>
<input id="name-input" type="text" v-model="formData.name" required /> <input id="name-input" type="text" v-model="formData.name" required />
<p class="form-input-hint">
The full name of the student, which should be unique among all students in the class.
</p>
</div> </div>
<div> <div>
<label for="desk-input">Desk Number</label> <label for="desk-input">Desk Number</label>
<input id="desk-input" type="number" v-model="formData.deskNumber" /> <input id="desk-input" type="number" v-model="formData.deskNumber" />
<p class="form-input-hint"> <p style="font-style: italic; font-size: smaller;">
Set desk number to zero to remove a student's desk assignment. Set desk number to zero to remove a student's desk assignment.
</p> </p>
</div> </div>
<div> <div>
<input id="removed-checkbox" type="checkbox" v-model="formData.removed" />
<label for="removed-checkbox" style="display: inline">Removed</label> <label for="removed-checkbox" style="display: inline">Removed</label>
<p class="form-input-hint"> <input id="removed-checkbox" type="checkbox" v-model="formData.removed" />
Check this if the student has been removed from the class.
</p>
</div> </div>
<div class="button-bar"> <div class="button-bar">
<button type="submit">Save</button> <button type="submit">Save</button>
@ -165,24 +151,19 @@ async function doMoveClass() {
</form> </form>
<!-- Dialog for moving the student to another class. --> <!-- Dialog for moving the student to another class. -->
<dialog id="move-class-dialog" ref="move-class-dialog" style="max-width: 500px;"> <dialog id="move-class-dialog" ref="move-class-dialog">
<p> <p>
Select a class to move {{ student?.name }} to: Select a class to move them to below:
</p> </p>
<select v-model="moveClassDialogClassSelection"> <select v-model="moveClassDialogClassSelection">
<option v-for="c in moveClassDialogClassChoices" :key="c.id" v-text="'Class ' + c.number" :value="c"></option> <option v-for="c in moveClassDialogClassChoices" :key="c.id" v-text="'Class ' + c.number" :value="c"></option>
</select> </select>
<p v-if="moveClassDialogClassSelection" class="form-input-hint"> <p v-if="moveClassDialogClassSelection">
Will move {{ student?.name }} from class {{ cls.number }} to class {{ moveClassDialogClassSelection.number }}. Will move {{ student?.name }} from class {{ cls.number }} to class {{ moveClassDialogClassSelection.number }}.
</p> </p>
<p> <div>
Note: All current entries for {{ student?.name }} in class {{ cls.number }} will be retained; no data is lost.
To add new entries for {{ student?.name }}, do so via their new class.
</p>
<div class="button-bar">
<button type="button" id="move-class-dialog-submit-button" @click="doMoveClass()">Submit</button> <button type="button" id="move-class-dialog-submit-button" @click="doMoveClass()">Submit</button>
<button type="button" id="move-class-dialog-cancel-button" @click="moveClassDialog?.close()">Cancel</button> <button type="button" id="move-class-dialog-cancel-button" @click="moveClassDialog?.close()">Cancel</button>
</div> </div>

View File

@ -30,7 +30,6 @@ enum TableView {
const students: Ref<EntriesResponseStudent[]> = ref([]) const students: Ref<EntriesResponseStudent[]> = ref([])
const sortingChoice: Ref<string> = ref('name') const sortingChoice: Ref<string> = ref('name')
const selectedView: Ref<TableView> = ref(TableView.FULL) const selectedView: Ref<TableView> = ref(TableView.FULL)
const hideRemovedStudents: Ref<boolean> = ref(false)
const lastSaveState: Ref<string | null> = ref(null) const lastSaveState: Ref<string | null> = ref(null)
const lastSaveStateTimestamp: Ref<number> = ref(0) const lastSaveStateTimestamp: Ref<number> = ref(0)
@ -217,13 +216,6 @@ function getVisibleDates(): string[] {
return [] return []
} }
function getVisibleStudents(): EntriesResponseStudent[] {
if (hideRemovedStudents.value === true) {
return students.value.filter(s => !s.removed && s.classId === props.classId)
}
return students.value
}
function getVisibleStudentEntries(student: EntriesResponseStudent): Record<string, Entry | null> { function getVisibleStudentEntries(student: EntriesResponseStudent): Record<string, Entry | null> {
if (selectedView.value === TableView.FULL) return student.entries if (selectedView.value === TableView.FULL) return student.entries
if (selectedView.value === TableView.TODAY) { if (selectedView.value === TableView.TODAY) {
@ -252,7 +244,7 @@ defineExpose({
</script> </script>
<template> <template>
<div> <div>
<div class="button-bar" style="text-align: left;"> <div class="button-bar">
<button type="button" @click="showPreviousWeek" :disabled="selectedView === TableView.TODAY">Previous <button type="button" @click="showPreviousWeek" :disabled="selectedView === TableView.TODAY">Previous
Week</button> Week</button>
<button type="button" @click="showThisWeek" :disabled="selectedView === TableView.TODAY">This Week</button> <button type="button" @click="showThisWeek" :disabled="selectedView === TableView.TODAY">This Week</button>
@ -275,12 +267,6 @@ defineExpose({
<option :value="TableView.WHITEBOARD">Whiteboard View</option> <option :value="TableView.WHITEBOARD">Whiteboard View</option>
<option :value="TableView.TODAY">Today View</option> <option :value="TableView.TODAY">Today View</option>
</select> </select>
<span>
<input type="checkbox" id="show-removed-students-checkbox" v-model="hideRemovedStudents" />
<label for="show-removed-students-checkbox" style="display: inline; font-size: small;">Hide Removed
Students</label>
</span>
</div> </div>
<p v-if="selectedView === TableView.GRADING"> <p v-if="selectedView === TableView.GRADING">
@ -289,7 +275,7 @@ defineExpose({
<table class="entries-table" :class="{ 'entries-table-reduced-view': selectedView !== TableView.FULL }"> <table class="entries-table" :class="{ 'entries-table-reduced-view': selectedView !== TableView.FULL }">
<thead> <thead>
<tr class="align-center"> <tr>
<th v-if="selectedView !== TableView.WHITEBOARD">#</th> <th v-if="selectedView !== TableView.WHITEBOARD">#</th>
<th>Student</th> <th>Student</th>
<th v-if="assignedDesks">Desk</th> <th v-if="assignedDesks">Desk</th>
@ -299,13 +285,16 @@ defineExpose({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(student, idx) in getVisibleStudents()" :key="student.id" style="height: 2em;"> <tr v-for="(student, idx) in students" :key="student.id" style="height: 2em;">
<td v-if="selectedView !== TableView.WHITEBOARD" style="text-align: right; padding-right: 0.5em;">{{ idx + 1 <td v-if="selectedView !== TableView.WHITEBOARD" style="text-align: right; padding-right: 0.5em;">{{ idx + 1
}}.</td> }}.</td>
<StudentNameCell :student="student" :class-id="classId" /> <StudentNameCell :student="student" :class-id="classId" />
<!-- Desk Number: -->
<td v-if="assignedDesks" v-text="student.deskNumber"></td> <td v-if="assignedDesks" v-text="student.deskNumber"></td>
<!-- A cell for each entry in the table's date range: -->
<EntryTableCell v-for="(entry, date) in getVisibleStudentEntries(student)" :key="date" <EntryTableCell v-for="(entry, date) in getVisibleStudentEntries(student)" :key="date"
v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" /> v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp" />
<!-- Score cell: -->
<StudentScoreCell :score="student.score" v-if="selectedView !== TableView.WHITEBOARD" /> <StudentScoreCell :score="student.score" v-if="selectedView !== TableView.WHITEBOARD" />
</tr> </tr>
</tbody> </tbody>

View File

@ -15,7 +15,7 @@ const sampleEntry: Ref<Entry | null> = ref({
</script> </script>
<template> <template>
<div class="align-left centered-content"> <div class="guide-page">
<h2>Guide</h2> <h2>Guide</h2>
<p> <p>
This page provides a quick guide to using the <em>Classroom Compliance</em> This page provides a quick guide to using the <em>Classroom Compliance</em>
@ -32,9 +32,7 @@ const sampleEntry: Ref<Entry | null> = ref({
school year to identify it. school year to identify it.
</p> </p>
<p> <p>
You can view all your classes with the <RouterLink class="link link-color" to="/classroom-compliance">View My You can view all your classes with the <RouterLink to="/classroom-compliance">View My Classes</RouterLink>
Classes
</RouterLink>
link at the top of this page. Click on any of the classes you see listed, link at the top of this page. Click on any of the classes you see listed,
and you'll go to that class' page. and you'll go to that class' page.
</p> </p>
@ -130,8 +128,7 @@ const sampleEntry: Ref<Entry | null> = ref({
<hr /> <hr />
<h3>Adding / Editing Students</h3> <h3>Adding / Editing Students</h3>
<p> <p>
As mentioned briefly in <a class="link link-color" href="#the-class-page">The Class Page</a>, you can add students As mentioned briefly in <a href="#the-class-page">The Class Page</a>, you can add students one-by-one, or in bulk.
one-by-one, or in bulk.
</p> </p>
<p> <p>
When adding a student individually, or editing an existing student, here's what you need to know: When adding a student individually, or editing an existing student, here's what you need to know:
@ -152,3 +149,10 @@ const sampleEntry: Ref<Entry | null> = ref({
</ul> </ul>
</div> </div>
</template> </template>
<style scoped>
.guide-page {
max-width: 50ch;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@ -32,8 +32,8 @@ async function doImport() {
} }
</script> </script>
<template> <template>
<div class="centered-content"> <div>
<h1 class="align-center">Import Students</h1> <h1>Import Students</h1>
<p> <p>
Import a large number of students to a class at once by pasting their names in the text box below, with one Import a large number of students to a class at once by pasting their names in the text box below, with one
student per line. student per line.

View File

@ -21,18 +21,16 @@ async function downloadExport() {
} }
</script> </script>
<template> <template>
<main class="app-header"> <main>
<h1>Classroom Compliance</h1> <h1>Classroom Compliance</h1>
<div class="button-bar"> <div class="button-bar">
<RouterLink class="link" to="/classroom-compliance">View My Classes</RouterLink> <RouterLink to="/classroom-compliance">View My Classes</RouterLink>
<RouterLink class="link" to="/classroom-compliance/guide">Guide</RouterLink> <RouterLink to="/classroom-compliance/guide">Guide</RouterLink>
<a class="link" @click.prevent="downloadExport()" href="#">Export All Data</a> <a @click.prevent="downloadExport()" href="#">Export All Data</a>
</div> </div>
<hr /> <hr />
<div class="align-left">
<RouterView :key="$route.fullPath" /> <RouterView :key="$route.fullPath" />
</div>
</main> </main>
</template> </template>

View File

@ -1,33 +0,0 @@
<script setup lang="ts">
import { ClassroomComplianceAPIClient, type Entry, type Student } from '@/api/classroom_compliance';
import { useAuthStore } from '@/stores/auth';
import { onMounted, ref, type Ref } from 'vue';
import StudentEntryItem from './StudentEntryItem.vue';
const props = defineProps<{
student: Student
}>()
const authStore = useAuthStore()
const entries: Ref<Entry[]> = ref([])
const apiClient = new ClassroomComplianceAPIClient(authStore)
onMounted(() => {
apiClient.getStudentEntries(props.student.classId, props.student.id).handleErrorsWithAlert()
.then(result => {
if (result !== null) {
entries.value = result
}
})
})
</script>
<template>
<div>
<h3 class="align-center">Entries</h3>
<div>
<StudentEntryItem v-for="entry in entries" :key="entry.id" :entry="entry" />
</div>
</div>
</template>

View File

@ -1,19 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, type Entry } from '@/api/classroom_compliance'; import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, type Entry } from '@/api/classroom_compliance';
const props = defineProps<{ defineProps<{
entry: Entry entry: Entry
}>() }>()
function getFormattedDate() {
const d = new Date(props.entry.date)
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
return days[d.getDay()] + ', ' + d.toLocaleDateString()
}
</script> </script>
<template> <template>
<div class="student-entry-item"> <div class="student-entry-item">
<h6>{{ getFormattedDate() }}</h6> <h6>{{ entry.date }}</h6>
<div class="icons-container"> <div class="icons-container">
<span v-if="entry.absent">{{ EMOJI_ABSENT }}</span> <span v-if="entry.absent">{{ EMOJI_ABSENT }}</span>
<span v-if="!entry.absent">{{ EMOJI_PRESENT }}</span> <span v-if="!entry.absent">{{ EMOJI_PRESENT }}</span>

View File

@ -4,7 +4,7 @@ import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { onMounted, ref, useTemplateRef, type Ref } from 'vue' import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import StudentEntriesList from './StudentEntriesList.vue' import StudentEntryItem from '@/apps/classroom_compliance/StudentEntryItem.vue'
const props = defineProps<{ const props = defineProps<{
classId: string classId: string
@ -53,54 +53,43 @@ async function deleteThisStudent() {
} }
</script> </script>
<template> <template>
<div v-if="student" class="centered-content"> <div v-if="student">
<h1 class="align-center" style="margin-bottom: 0;">{{ student.name }}</h1> <h2 v-text="student.name"></h2>
<p class="align-center" style="margin-top: 0;"> <p>
in From
<RouterLink class="link link-color" :to="'/classroom-compliance/classes/' + classId"> <RouterLink :to="'/classroom-compliance/classes/' + classId">
<span v-text="'class ' + cls?.number"></span> <span v-text="'class ' + cls?.number + ', ' + cls?.schoolYear"></span>
</RouterLink> </RouterLink>
</p> </p>
<ul>
<li>Internal ID: <span v-text="student.id"></span></li>
<li>Removed: <span v-text="student.removed"></span></li>
<li>Desk number: <span v-text="student.deskNumber"></span></li>
</ul>
<div class="button-bar align-center"> <div class="button-bar">
<button type="button" <button type="button"
@click="router.push(`/classroom-compliance/classes/${student.classId}/edit-student?studentId=${student.id}`)">Edit</button> @click="router.push(`/classroom-compliance/classes/${student.classId}/edit-student?studentId=${student.id}`)">Edit</button>
<button type="button" @click="deleteThisStudent">Delete</button> <button type="button" @click="deleteThisStudent">Delete</button>
</div> </div>
<table class="student-properties-table"> <div v-if="statistics">
<tbody> <h3>Statistics</h3>
<tr> <ul>
<th>Desk Number</th> <li>Attendance Rate: {{ (statistics.attendanceRate * 100).toFixed(1) }}%</li>
<td> <li>Phone Compliance Rate: {{ (statistics.phoneComplianceRate * 100).toFixed(1) }}%</li>
{{ student.deskNumber }} <li>Behavior Score: {{ (statistics.behaviorScore * 100).toFixed(1) }}%</li>
<span v-if="student.deskNumber === 0" style="font-style: italic; font-size: small;">*No assigned desk</span> <li>From a total of {{ statistics.entryCount }} entries</li>
</td> </ul>
</tr> </div>
<tr>
<th>Removed</th>
<td>{{ student.removed ? 'True' : 'False' }}</td>
</tr>
<tr v-if="statistics">
<th>Attendance Rate</th>
<td>{{ (statistics.attendanceRate * 100).toFixed(1) }}%</td>
</tr>
<tr v-if="statistics">
<th>Phone Compliance Rate</th>
<td>{{ (statistics.phoneComplianceRate * 100).toFixed(1) }}%</td>
</tr>
<tr v-if="statistics">
<th>Behavior Score</th>
<td>{{ (statistics.behaviorScore * 100).toFixed(1) }}%</td>
</tr>
<tr v-if="statistics">
<th>Total Entries</th>
<td>{{ statistics.entryCount }}</td>
</tr>
</tbody>
</table>
<StudentEntriesList :student="student" /> <h3>Entries</h3>
<p>
Below is a record of all entries saved for <span>{{ student.name }}</span>.
</p>
<div>
<StudentEntryItem v-for="entry in entries" :key="entry.id" :entry="entry" />
</div>
<ConfirmDialog ref="deleteConfirmDialog"> <ConfirmDialog ref="deleteConfirmDialog">
<p> <p>
@ -111,19 +100,3 @@ async function deleteThisStudent() {
</ConfirmDialog> </ConfirmDialog>
</div> </div>
</template> </template>
<style scoped>
.student-properties-table {
width: 100%;
border-collapse: collapse;
}
.student-properties-table :is(th, td) {
border: 1px solid gray;
padding: 0.25em;
}
.student-properties-table td {
text-align: right;
font-family: 'SourceCodePro', monospace;
}
</style>

View File

@ -5,9 +5,9 @@ defineProps<{
</script> </script>
<template> <template>
<td style="text-align: right; padding-right: 0.25em;"> <td style="text-align: right; padding-right: 0.25em;">
<span v-if="score !== null" class="text-mono"> <span v-if="score !== null" style="font-family: monospace; font-size: large;">
{{ (score * 100).toFixed(1) }}% {{ (score * 100).toFixed(1) }}%
</span> </span>
<span v-if="score === null" style="font-style: italic; color: gray;">No score</span> <span v-if="score === null" style="font-size: small; font-style: italic;">No score</span>
</td> </td>
</template> </template>

View File

@ -1,28 +1,6 @@
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans.ttf');
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.ttf');
font-style: italic;
}
@font-face {
font-family: 'SourceCodePro';
src: url('@/assets/fonts/SourceCodePro.ttf');
}
@font-face {
font-family: 'SourceCodePro';
src: url('@/assets/fonts/SourceCodePro-Italic.ttf');
font-style: italic;
}
:root { :root {
color-scheme: light dark; color-scheme: light dark;
font-family: 'Open Sans', sans-serif; font-family: sans-serif;
} }
.button-bar { .button-bar {
@ -34,80 +12,10 @@
margin-left: 0.5em; margin-left: 0.5em;
} }
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
.align-center {
text-align: center;
}
.centered-content {
max-width: 50ch;
margin-left: auto;
margin-right: auto;
}
code {
font-family: 'SourceCodePro', monospace;
color: lime;
}
.text-mono {
font-family: 'SourceCodePro', monospace;
}
label { label {
display: block; display: block;
} }
textarea {
font-family: 'SourceCodePro', monospace;
font-size: smaller;
}
button {
font-family: 'Open Sans', sans-serif;
}
.link {
text-decoration: none;
color: inherit;
}
.link:hover {
text-decoration: underline;
color: lightgray;
}
.link-color {
color: rgb(165, 210, 253);
}
.link-color:hover {
color: rgb(95, 121, 190);
}
.link-ext {
color: rgb(158, 255, 134);
}
.link-ext:hover {
color: rgb(110, 179, 93);
}
form > div + div { form > div + div {
margin-top: 0.5em; margin-top: 0.5em;
} }
.form-input-hint {
font-size: smaller;
font-style: italic;
margin-top: 0.25em;
}
.app-header {
text-align: center;
}
.app-header > h1 {
margin-bottom: 0.5em;
}

Binary file not shown.

View File

@ -7,12 +7,3 @@
</div> </div>
</dialog> </dialog>
</template> </template>
<style>
.alert-dialog-error {
border-color: red;
}
.alert-dialog-warning {
border-color: orange;
}
</style>

View File

@ -1,29 +0,0 @@
<script setup lang="ts">
defineProps<{
name: string,
route: string
}>()
</script>
<template>
<div class="app-list-item">
<h3>
<RouterLink class="link" :to="route">{{ name }}</RouterLink>
</h3>
<p>
<slot></slot>
</p>
</div>
</template>
<style scoped>
.app-list-item {
padding: 1em;
background-color: rgb(49, 49, 49);
border-radius: 20px;
}
.app-list-item>h3 {
margin-top: 0;
margin-bottom: 0;
font-size: x-large;
}
</style>

View File

@ -36,10 +36,20 @@ defineExpose({
<form> <form>
<slot></slot> <slot></slot>
<div class="button-bar align-right"> <div class="confirm-dialog-buttons">
<button @click.prevent="onConfirmClicked">Confirm</button> <button @click.prevent="onConfirmClicked">Confirm</button>
<button @click.prevent="onCancelClicked">Cancel</button> <button @click.prevent="onCancelClicked">Cancel</button>
</div> </div>
</form> </form>
</dialog> </dialog>
</template> </template>
<style>
.confirm-dialog-buttons {
text-align: right;
margin-top: 0.5em;
}
.confirm-dialog-buttons>button {
margin-left: 0.5em;
}
</style>

View File

@ -1,27 +1,25 @@
<script setup lang="ts"> <script setup lang="ts"></script>
import AppListItem from '@/components/AppListItem.vue';
import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore()
</script>
<template> <template>
<main class="centered-content"> <main>
<h1 class="align-center">My Applications</h1> <h1>Home Page</h1>
<p>
<div v-if="authStore.state"> Welcome to Teacher-Tools, a website with tools that help teachers to manage their classrooms.
<AppListItem name="Classroom Compliance" route="/classroom-compliance">
Track your students' phone usage and behavior, record notes, and automatically calculate weekly scores for each
student.
</AppListItem>
<p style="text-align: right; font-style: italic;">
... and more apps coming soon!
</p> </p>
</div> <hr>
<h2>Applications</h2>
<p v-if="!authStore.state" class="align-center"> <p>
Please <RouterLink to="/login">log in</RouterLink> to view your applications. The following list of applications are available for you:
</p> </p>
<ul>
<li>
<RouterLink to="/classroom-compliance">Classroom Compliance</RouterLink>
-
Track your students' phone usage and behavior patterns, and calculate weighted grades.
</li>
<li>
<em>... and more to come soon!</em>
</li>
</ul>
</main> </main>
</template> </template>

View File

@ -28,22 +28,23 @@ async function doLogin() {
} }
</script> </script>
<template> <template>
<main class="centered-content"> <main>
<h1 class="align-center">Login</h1> <h1>Login</h1>
<form> <form>
<div class="login-input-row"> <div>
<input id="username-input" name="username" type="text" v-model="credentials.username" placeholder="Username" /> <label for="username-input">Username</label>
<input id="username-input" name="username" type="text" v-model="credentials.username" />
</div> </div>
<div class="login-input-row"> <div>
<input id="password-input" name="password" type="password" v-model="credentials.password" <label for="password-input">Password</label>
placeholder="Password" /> <input id="password-input" name="password" type="password" v-model="credentials.password" />
</div> </div>
<div class="button-bar align-center"> <div class="button-bar">
<button type="button" @click="doLogin" style="font-size: large;">Login</button> <button type="button" @click="doLogin">Login</button>
</div> </div>
<div> <div>
<p> <p>
If you forgot your password or would like to create an account, please contact <a class="link link-ext" If you forgot your password or would like to create an account, please contact <a
href="https://andrewlalis.com/contact">Andrew Lalis</a> for assistance. href="https://andrewlalis.com/contact">Andrew Lalis</a> for assistance.
</p> </p>
<p> <p>
@ -53,20 +54,3 @@ async function doLogin() {
</form> </form>
</main> </main>
</template> </template>
<style scoped>
.login-input-row {
max-width: 30ch;
margin-left: auto;
margin-right: auto;
}
.login-input-row>input {
width: 100%;
font-size: medium;
padding: 0.5em;
}
.login-input-row+.login-input-row {
margin-top: 1em;
}
</style>

View File

@ -4,9 +4,9 @@ import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore() const authStore = useAuthStore()
</script> </script>
<template> <template>
<main v-if="authStore.state" class="centered-content"> <main v-if="authStore.state">
<h1 class="align-center">My Account</h1> <h1>My Account</h1>
<table class="account-properties-table"> <table>
<tbody> <tbody>
<tr> <tr>
<th>Internal ID</th> <th>Internal ID</th>
@ -33,32 +33,5 @@ const authStore = useAuthStore()
<p> <p>
Please contact Andrew Lalis for help with account administration or to report any bugs you find! Please contact Andrew Lalis for help with account administration or to report any bugs you find!
</p> </p>
<p>
Suggestions for new features are also highly encouraged!
</p>
<p>
Contact Andrew at <a class="link link-ext" href="https://www.andrewlalis.com/contact"
target="_blank">andrewlalis.com/contact</a>.
</p>
</main> </main>
</template> </template>
<style scoped>
.account-properties-table {
width: 100%;
border-collapse: collapse;
}
.account-properties-table :is(th, td) {
border: 1px solid gray;
padding: 0.25em;
}
.account-properties-table th {
text-align: left;
}
.account-properties-table td {
text-align: right;
font-family: 'SourceCodePro', monospace;
}
</style>