mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 14:37:34 +00:00
Latest
This commit is contained in:
@@ -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 ***********************
|
||||
|
||||
Reference in New Issue
Block a user