v21 updates

This commit is contained in:
ivan-pelly
2026-04-26 19:11:14 -07:00
parent 1d684827a3
commit cef42540a6
7 changed files with 1624 additions and 1127 deletions
+2 -2
View File
@@ -45,8 +45,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
"maximumWarning": "6kB",
"maximumError": "10kB"
}
],
"outputHashing": "all"
+1374 -1105
View File
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -20,27 +20,27 @@
},
"private": true,
"dependencies": {
"@angular/common": "^20.1.0",
"@angular/compiler": "^20.1.0",
"@angular/core": "^20.1.0",
"@angular/forms": "^20.1.0",
"@angular/platform-browser": "^20.1.0",
"@angular/router": "^20.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
"@angular/common": "^21.2.10",
"@angular/compiler": "^21.2.10",
"@angular/core": "^21.2.10",
"@angular/forms": "^21.2.10",
"@angular/platform-browser": "^21.2.10",
"@angular/router": "^21.2.10",
"rxjs": "~7.8.2",
"tslib": "^2.8.1",
"zone.js": "~0.16.1"
},
"devDependencies": {
"@angular/build": "^20.1.5",
"@angular/cli": "^20.1.5",
"@angular/compiler-cli": "^20.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.8.0",
"karma": "~6.4.0",
"@angular/build": "^21.2.8",
"@angular/cli": "^21.2.8",
"@angular/compiler-cli": "^21.2.10",
"@types/jasmine": "~6.0.0",
"jasmine-core": "~6.2.0",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.8.2"
"karma-jasmine-html-reporter": "~2.2.0",
"typescript": "~6.0.3"
}
}
@@ -15,9 +15,9 @@ export class ModalShell implements OnInit, OnDestroy {
ngOnInit() {
this.previousFocus = document.activeElement as HTMLElement;
requestAnimationFrame(() => {
const focusable = this.el.nativeElement.querySelector<HTMLElement>(
const focusable = this.el.nativeElement.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
) as HTMLElement | null;
focusable?.focus();
});
}
@@ -0,0 +1,123 @@
:host {
display: flex;
justify-content: center;
align-items: center;
min-height: 100dvh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 1rem;
}
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 2rem;
width: 100%;
max-width: 480px;
}
h2 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
}
.subtitle {
margin: 0 0 1rem;
color: #666;
font-size: 0.875rem;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
fieldset {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
legend {
font-size: 0.8125rem;
font-weight: 600;
color: #4338CA;
padding: 0 0.5rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
color: #333;
}
input, textarea {
padding: 0.625rem 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9375rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
resize: vertical;
}
input:focus, textarea:focus {
border-color: #4f46e5;
}
button[type='submit'] {
padding: 0.625rem 1rem;
background: #4f46e5;
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
margin-top: 0.5rem;
}
button[type='submit']:hover {
background: #4338ca;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error {
background: #fef2f2;
color: #dc2626;
padding: 0.625rem 0.75rem;
border-radius: 6px;
font-size: 0.875rem;
margin: 0.5rem 0;
}
.link-button {
display: inline-block;
background: none;
border: none;
color: #4f46e5;
cursor: pointer;
font-size: 0.875rem;
padding: 0.5rem 0;
margin-top: 0.75rem;
text-decoration: none;
}
.link-button:hover {
text-decoration: underline;
}
@@ -0,0 +1,103 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';
import { ResponseResult } from '../classes/auth.models';
import { firstValueFrom } from 'rxjs';
export interface ProgramDto {
programId: string;
name: string;
description: string;
createdAt: string;
}
export interface DistrictUserDto {
userId: string;
email: string;
name: string;
programId: string;
programName: string;
roleId: string;
roleName: string;
createdAt: string;
}
export interface RoleDto {
roleId: string;
name: string;
internalName: string;
description: string;
}
@Injectable({
providedIn: 'root',
})
export class AdminService {
private readonly http = inject(HttpClient);
private readonly base = environment.apiBaseUrl;
// ************************ Programs *************************
async getPrograms(): Promise<ProgramDto[]> {
const result = await firstValueFrom(
this.http.get<ResponseResult<ProgramDto[]>>(`${this.base}/api/Admin/programs`)
);
return result.success && result.data ? result.data : [];
}
async createProgram(name: string, description?: string): Promise<ResponseResult<object>> {
return await firstValueFrom(
this.http.post<ResponseResult<object>>(`${this.base}/api/Admin/programs`, { name, description })
);
}
async updateProgram(programId: string, name: string, description?: string): Promise<ResponseResult<object>> {
return await firstValueFrom(
this.http.put<ResponseResult<object>>(`${this.base}/api/Admin/programs/${programId}`, { name, description })
);
}
// ************************ Users *************************
async getUsers(): Promise<DistrictUserDto[]> {
const result = await firstValueFrom(
this.http.get<ResponseResult<DistrictUserDto[]>>(`${this.base}/api/Admin/users`)
);
return result.success && result.data ? result.data : [];
}
async createUser(email: string, name: string, password: string, programId: string, roleId: string): Promise<ResponseResult<object>> {
return await firstValueFrom(
this.http.post<ResponseResult<object>>(`${this.base}/api/Admin/users`, { email, name, password, programId, roleId })
);
}
// ************************ Roles *************************
async getRoles(): Promise<RoleDto[]> {
const result = await firstValueFrom(
this.http.get<ResponseResult<RoleDto[]>>(`${this.base}/api/Admin/roles`)
);
return result.success && result.data ? result.data : [];
}
// ************************ Backup *************************
// *****************************************************************
// Downloads a full database backup as a .sql file. The API streams
// the dump as a blob; this method triggers a browser file-save
// dialog using a temporary anchor element.
// *****************************************************************
async backupDatabase(): Promise<void> {
const blob = await firstValueFrom(
this.http.get(`${this.base}/api/Admin/backup`, { responseType: 'blob' })
);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
a.download = `winstudentgoaltracker_backup_${timestamp}.sql`;
a.click();
window.URL.revokeObjectURL(url);
}
}