lots of work done

This commit is contained in:
2026-03-02 16:23:29 -08:00
parent be4873283d
commit ef09a76bb4
25 changed files with 644 additions and 157 deletions
@@ -0,0 +1,66 @@
<div class="overlay" (click)="onCancel()"></div>
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Add Student</h2>
<button class="close-btn" (click)="onCancel()" aria-label="Close">&times;</button>
</div>
<form class="modal-body" (ngSubmit)="onSubmit()" #studentForm="ngForm">
<div class="field">
<label for="identifier">Identifier</label>
<input
id="identifier"
type="text"
[(ngModel)]="form.identifier"
name="identifier"
required
placeholder="e.g. Student123"
/>
</div>
<div class="field">
<label for="programYear">Program Year</label>
<input
id="programYear"
type="number"
[(ngModel)]="form.programYear"
name="programYear"
placeholder="e.g. 2025"
/>
</div>
<div class="field">
<label for="enrollmentDate">Enrollment Date</label>
<input
id="enrollmentDate"
type="date"
[(ngModel)]="form.enrollmentDate"
name="enrollmentDate"
/>
</div>
<div class="field">
<label for="expectedGrad">Expected Graduation</label>
<input
id="expectedGrad"
type="date"
[(ngModel)]="form.expectedGrad"
name="expectedGrad"
/>
</div>
@if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
}
<div class="modal-actions">
<button type="button" class="btn btn-secondary" (click)="onCancel()">Cancel</button>
<button type="submit" class="btn btn-primary" [disabled]="studentForm.invalid || isSubmitting()">
{{ isSubmitting() ? 'Saving...' : 'Add Student' }}
</button>
</div>
</form>
</div>
@@ -0,0 +1,130 @@
:host {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
}
.modal {
position: relative;
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
width: 420px;
max-width: 95vw;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem 0;
}
.modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: #666;
padding: 0;
}
.close-btn:hover {
color: #111;
}
.modal-body {
padding: 1.25rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.field label {
font-size: 0.875rem;
font-weight: 500;
color: #333;
}
.field input {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9375rem;
outline: none;
}
.field input:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.15);
}
.error {
font-size: 0.875rem;
color: #dc2626;
margin: 0;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 0.25rem;
}
.btn {
padding: 0.5rem 1.125rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
}
.btn-secondary {
background: transparent;
border-color: #ddd;
color: #555;
}
.btn-secondary:hover {
background: #f5f5f5;
}
.btn-primary {
background: #4f46e5;
color: #fff;
border-color: #4f46e5;
}
.btn-primary:hover:not(:disabled) {
background: #4338ca;
border-color: #4338ca;
}
.btn-primary:disabled {
opacity: 0.55;
cursor: not-allowed;
}
@@ -0,0 +1,61 @@
import { Component, inject, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CreateStudentDto } from '../../../shared/classes/create-student.dto';
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentService } from '../../../shared/services/student.service';
@Component({
selector: 'app-add-student-modal',
imports: [FormsModule],
templateUrl: './add-student-modal.html',
styleUrl: './add-student-modal.scss',
})
export class AddStudentModal {
// ************************** Constructor **************************
// ************************** Declarations *************************
private readonly studentService = inject(StudentService);
readonly studentCreated = output<StudentCardDto>();
readonly cancelled = output<void>();
protected readonly isSubmitting = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected form: CreateStudentDto = {
identifier: '',
programYear: null,
enrollmentDate: null,
expectedGrad: null,
};
// ************************** Properties ***************************
// ************************ Public Methods *************************
// ************************ Event Handlers *************************
async onSubmit() {
this.errorMessage.set(null);
this.isSubmitting.set(true);
const result = await this.studentService.createStudent(this.form);
this.isSubmitting.set(false);
if (!result.success) {
this.errorMessage.set(result.message);
return;
}
this.studentCreated.emit(result.payload!);
}
onCancel() {
this.cancelled.emit();
}
// ********************** Support Procedures ***********************
}
@@ -2,6 +2,13 @@
<button class="toolbar-btn" (click)="onAddStudent()">+ Add a Student</button>
</div>
@if (showAddModal()) {
<app-add-student-modal
(studentCreated)="onStudentCreated($event)"
(cancelled)="onModalCancelled()"
/>
}
@if (displayMode() === 'card') {
<div class="card-grid">
@for (student of students(); track student.studentId) {
@@ -1,13 +1,15 @@
import { Component, inject, signal } from '@angular/core';
import { StudentCard } from '../student-card/student-card';
import { StudentService } from '../../../shared/services/dummy-student.service';
import { AddStudentModal } from '../add-student-modal/add-student-modal';
import { DummyStudentService } from '../../../shared/services/dummy-student.service';
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentService } from '../../../shared/services/student.service';
export type DisplayMode = 'card' | 'list';
@Component({
selector: 'app-student-card-list',
imports: [StudentCard],
imports: [StudentCard, AddStudentModal],
templateUrl: './student-card-list.html',
styleUrl: './student-card-list.scss',
})
@@ -24,6 +26,7 @@ export class StudentCardList {
private readonly studentService = inject(StudentService);
protected readonly students = signal<StudentCardDto[]>([]);
protected readonly displayMode = signal<DisplayMode>('card');
protected readonly showAddModal = signal(false);
public errorMessage = signal<String | null>(null);
@@ -34,7 +37,16 @@ export class StudentCardList {
// ************************ Event Handlers *************************
onAddStudent() {
// TODO: navigate to add-student form
this.showAddModal.set(true);
}
onStudentCreated(student: StudentCardDto) {
this.students.update(list => [...list, student]);
this.showAddModal.set(false);
}
onModalCancelled() {
this.showAddModal.set(false);
}
// ********************** Support Procedures ***********************
@@ -43,7 +55,7 @@ export class StudentCardList {
// Loads students from the service and populates the students signal.
// *****************************************************************
private loadStudents() {
this.studentService.getStudentCards().then(data => {
this.studentService.getMyStudents().then(data => {
if(!data.success)
{
@@ -2,7 +2,7 @@
<h2 class="identifier">🎓 {{ student().identifier }}</h2>
<div class="meta">
<span class="badge">Grad Date: {{ student().expectedGradDate }}</span>
<span class="last-entry">Grad Date: {{ student().expectedGradDate | date:'M/d/yy'}}</span>
<span class="last-entry">
@if (student().lastEntryDate) {
Last entry: {{ student().lastEntryDate | date:'M/d/yy' }}
@@ -1,4 +1,4 @@
import { Component, input } from '@angular/core';
import { Component, computed, input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { StudentCardDto } from '../../../shared/classes/student-card.dto';