This commit is contained in:
ivan-pelly
2026-03-01 18:21:51 -08:00
parent d9a7b6c21e
commit 41e7120012
35 changed files with 1194 additions and 97 deletions
@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
// *****************************************************************
// TODO: This dummy service should be replaced by MobileHomeMeta,
// which will fetch real data from the API.
// *****************************************************************
export interface MobileHomeMeta {
programName: string; // program.name — varchar(255)
userName: string; // user.name — varchar(255)
}
@Injectable({
providedIn: 'root',
})
export class DummyMobileHomeMeta {
// ************************** Constructor **************************
// ************************** Declarations *************************
// ************************** Properties ***************************
// ************************ Public Methods *************************
// *****************************************************************
// TODO: DUMMY DATA — Returns hardcoded program and user info.
// Replace with MobileHomeMeta service that calls
// GET /api/mobile/home-meta (or similar).
// *****************************************************************
getMeta(): Observable<MobileHomeMeta> {
return of({
programName: 'WIN Program',
userName: 'Polly Balsillie',
});
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
}
@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { ApiResult } from '../classes/api-result';
// *****************************************************************
// TODO: This dummy service should be replaced by SaveProgressEvent,
// which will POST real data to the API.
// *****************************************************************
@Injectable({
providedIn: 'root',
})
export class DummySaveProgressEvent {
// ************************** Constructor **************************
// ************************** Declarations *************************
// ************************** Properties ***************************
// ************************ Public Methods *************************
// *****************************************************************
// TODO: DUMMY — Always returns success. Replace with
// SaveProgressEvent calling POST /api/progress-events
// *****************************************************************
save(studentId: string, goalId: string, content: string): Observable<ApiResult> {
return of(ApiResult.empty());
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
}
@@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
// *****************************************************************
// TODO: This dummy service should be replaced by StudentGoalService,
// which will fetch real data from the API.
// *****************************************************************
export interface StudentGoalSummary {
studentIdentifier: string; // student.identifier — varchar(50)
goals: StudentGoalItem[];
}
export interface StudentGoalItem {
goalId: string; // goal.id_goal — char(36)
title: string; // goal.title — varchar(255)
description: string; // goal.description — text
category: string; // goal.category — varchar(100)
progressEventCount: number; // count of progress_event rows for this goal
}
@Injectable({
providedIn: 'root',
})
export class DummyStudentGoalService {
// ************************** Constructor **************************
// ************************** Declarations *************************
// *****************************************************************
// TODO: DUMMY DATA — Maps studentId to identifier and goals.
// Replace with StudentGoalService calling
// GET /api/students/:id/goals
// *****************************************************************
private readonly data: Record<string, StudentGoalSummary> = {
'1': {
studentIdentifier: 'J.B',
goals: [
{ goalId: 'g1', title: 'Improve reading comprehension', description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', progressEventCount: 5 },
{ goalId: 'g2', title: 'Complete algebra module', description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', progressEventCount: 2 },
{ goalId: 'g3', title: 'Weekly journal entries', description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', progressEventCount: 8 },
],
},
'2': {
studentIdentifier: 'M.K',
goals: [
{ goalId: 'g4', title: 'Pass certification exam', description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', progressEventCount: 3 },
{ goalId: 'g5', title: 'Attendance above 90%', description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', progressEventCount: 0 },
{ goalId: 'g6', title: 'Complete internship hours', description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', progressEventCount: 12 },
{ goalId: 'g7', title: 'Portfolio project', description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', progressEventCount: 1 },
],
},
'3': {
studentIdentifier: 'A.R',
goals: [
{ goalId: 'g8', title: 'GED preparation', description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', progressEventCount: 6 },
{ goalId: 'g9', title: 'Resume workshop', description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', progressEventCount: 0 },
],
},
'4': {
studentIdentifier: 'T.W',
goals: [
{ goalId: 'g10', title: 'Public speaking practice', description: 'Present in front of the class at least once per month.', category: 'Communication', progressEventCount: 4 },
{ goalId: 'g11', title: 'Math placement improvement', description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', progressEventCount: 7 },
{ goalId: 'g12', title: 'Conflict resolution strategies', description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', progressEventCount: 2 },
{ goalId: 'g13', title: 'Daily attendance streak', description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', progressEventCount: 0 },
{ goalId: 'g14', title: 'Job shadow experience', description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', progressEventCount: 1 },
],
},
'5': {
studentIdentifier: 'L.C',
goals: [
{ goalId: 'g15', title: 'Improve typing speed', description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', progressEventCount: 3 },
],
},
};
// ************************** Properties ***************************
// ************************ Public Methods *************************
// *****************************************************************
// Returns the student's identifier and their list of goals,
// given a student ID.
// *****************************************************************
getGoalsForStudent(studentId: string): Observable<StudentGoalSummary | null> {
return of(this.data[studentId] ?? null);
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
}
@@ -7,67 +7,104 @@ export type FormFactor = 'mobile' | 'desktop';
providedIn: 'root',
})
export class PlatformService {
// ************************** Constructor **************************
private readonly router = inject(Router);
// ──── Raw Hardware Signals ────
// ************************** Declarations *************************
// *****************************************************************
// Checks if the device uses a touch screen (like a phone or tablet)
// rather than a mouse or trackpad.
// *****************************************************************
private readonly isCoarsePointer =
typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
// *****************************************************************
// Captures the screen width and height so we can figure out what
// kind of device the user is on. Defaults to a large desktop size
// if running on the server.
// *****************************************************************
private readonly screenWidth =
typeof window !== 'undefined' ? window.innerWidth : 1920;
private readonly screenHeight =
typeof window !== 'undefined' ? window.innerHeight : 1080;
// *****************************************************************
// The shortest and longest edges of the screen, regardless of
// whether the device is held in portrait or landscape.
// *****************************************************************
private readonly minDimension = Math.min(this.screenWidth, this.screenHeight);
private readonly maxDimension = Math.max(this.screenWidth, this.screenHeight);
// ──── Override Layer ────
/** When non-null, overrides auto-detection. */
// *****************************************************************
// Lets the user (or the app) manually force mobile or desktop mode
// instead of relying on automatic detection. When set to null, the
// app just figures it out on its own.
// *****************************************************************
private readonly formFactorOverride = signal<FormFactor | null>(null);
// ──── Public API ────
// ************************** Properties ***************************
/** The resolved form factor — auto-detected or overridden. */
// *****************************************************************
// The final answer: are we showing the "mobile" or "desktop"
// experience? Uses the manual override if one was set, otherwise
// auto-detects based on the device hardware.
// *****************************************************************
readonly formFactor = computed<FormFactor>(() =>
this.formFactorOverride() ?? this.resolveFormFactor(),
);
/** True when the user has manually toggled away from auto-detection. */
// *****************************************************************
// True when the current mode was manually chosen by the user,
// false when it was detected automatically.
// *****************************************************************
readonly isOverridden = computed(() => this.formFactorOverride() !== null);
/**
* Switch to a specific form factor at runtime.
* Forces a route re-evaluation so the user immediately sees the other experience.
*/
// ************************ Public Methods *************************
// *****************************************************************
// Switches between mobile and desktop mode on the fly. After
// switching, the page reloads so the correct layout appears
// immediately.
// *****************************************************************
switchTo(target: FormFactor): void {
this.formFactorOverride.set(target);
this.router.navigateByUrl(this.router.url);
}
/** Clear the override and return to auto-detected form factor. */
// *****************************************************************
// Clears any manual override and goes back to letting the device
// decide which mode to show.
// *****************************************************************
resetToAuto(): void {
this.formFactorOverride.set(null);
this.router.navigateByUrl(this.router.url);
}
// ──── Device Classification Policy ────
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
// *****************************************************************
// The rules for deciding whether a device gets the mobile or
// desktop experience:
// - Mouse/trackpad users always get desktop
// - Large tablets (like iPad Pro) get desktop
// - Medium tablets held sideways get desktop
// - Everything else (phones, small tablets) gets mobile
// *****************************************************************
private resolveFormFactor(): FormFactor {
// Non-touch devices are always desktop
if (!this.isCoarsePointer) return 'desktop';
// Large tablets (iPad Pro 12.9" portrait = 1024px) → desktop
if (this.minDimension >= 1024) return 'desktop';
// Medium tablets in landscape with sufficient width → desktop
if (this.maxDimension >= 1180 && this.screenWidth > this.screenHeight) {
return 'desktop';
}
// Phones and small/portrait tablets → mobile
return 'mobile';
}
}
@@ -48,6 +48,22 @@ export class StudentService {
]);
}
// *****************************************************************
// TODO: DUMMY DATA — Replace with getStudentsPerUser, which will
// call GET /api/users/:id/students to return real data.
// Returns students assigned to the current user with their
// identifier, age, goal count, and progress event count.
// *****************************************************************
getDummyStudentsForUser(): Observable<StudentCardDto[]> {
return of([
{ studentId: '1', identifier: 'J.B', age: 21, lastEntryDate: '2026-02-21', goalCount: 3, progressEventCount: 5 },
{ studentId: '2', identifier: 'M.K', age: 19, lastEntryDate: '2026-02-25', goalCount: 4, progressEventCount: 8 },
{ studentId: '3', identifier: 'A.R', age: 22, lastEntryDate: null, goalCount: 2, progressEventCount: 0 },
{ studentId: '4', identifier: 'T.W', age: 20, lastEntryDate: '2026-02-18', goalCount: 5, progressEventCount: 12 },
{ studentId: '5', identifier: 'L.C', age: 18, lastEntryDate: '2026-02-27', goalCount: 1, progressEventCount: 2 },
]);
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************