diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 69347f7..e468c86 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; -import { provideRouter, withViewTransitions } from '@angular/router'; +import { TitleStrategy, provideRouter, withViewTransitions } from '@angular/router'; import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import { providePrimeNG } from 'primeng/config'; import { ConfirmationService, MessageService } from 'primeng/api'; @@ -8,6 +8,7 @@ import { routes } from './app.routes'; import { authInterceptor } from './core/auth/auth.interceptor'; import { HEBREW_TRANSLATION } from './core/i18n/primeng-hebrew'; import { TalmidonPreset } from './core/theme/talmidon-preset'; +import { AppTitleStrategy } from './core/title/app-title.strategy'; export const appConfig: ApplicationConfig = { providers: [ @@ -15,6 +16,7 @@ export const appConfig: ApplicationConfig = { // מעבר חלק בין מסכים במקום החלפה חדה; דפדפנים בלי התמיכה מתעלמים בשקט provideRouter(routes, withViewTransitions()), provideHttpClient(withFetch(), withInterceptors([authInterceptor])), + { provide: TitleStrategy, useClass: AppTitleStrategy }, MessageService, ConfirmationService, providePrimeNG({ diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 76f29a2..affcfb3 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -14,18 +14,21 @@ export const routes: Routes = [ }, { path: 'teachers/:id', + title: 'פרופיל מורה', loadComponent: () => import('./features/public/teacher-profile/teacher-profile.component').then(m => m.TeacherProfileComponent) }, - { path: 'login', component: LoginComponent }, - { path: 'register', component: RegisterComponent }, + { path: 'login', title: 'התחברות', component: LoginComponent }, + { path: 'register', title: 'הרשמה כמורה', component: RegisterComponent }, { path: 'forgot-password', + title: 'שחזור סיסמה', loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) }, { path: 'set-password', + title: 'קביעת סיסמה', loadComponent: () => import('./features/auth/set-password/set-password.component').then(m => m.SetPasswordComponent) }, { @@ -38,49 +41,60 @@ export const routes: Routes = [ // מסך ההקמה עצמו מחוץ ל-profileSetupGuard, אחרת הוא היה מפנה אל עצמו בלולאה { path: 'setup', + title: 'הקמת הפרופיל', loadComponent: () => import('./features/teacher/profile-setup/profile-setup.component').then(m => m.ProfileSetupComponent) }, - { path: 'dashboard', component: DashboardComponent }, + { path: 'dashboard', title: 'ראשי', component: DashboardComponent }, { path: 'students', + title: 'תלמידים', loadComponent: () => import('./features/students/students-list/students-list.component').then(m => m.StudentsListComponent) }, { path: 'students/:id', + title: 'כרטיס תלמיד', loadComponent: () => import('./features/students/student-detail/student-detail.component').then(m => m.StudentDetailComponent) }, { path: 'lessons', + title: 'יומן שיעורים', loadComponent: () => import('./features/lessons/lessons-list/lessons-list.component').then(m => m.LessonsListComponent) }, { path: 'payments', + title: 'תשלומים', loadComponent: () => import('./features/payments/payments-list/payments-list.component').then(m => m.PaymentsListComponent) }, { path: 'reports', + title: 'דוחות', loadComponent: () => import('./features/reports/reports.component').then(m => m.ReportsComponent) }, { path: 'contact-requests', + title: 'פניות', loadComponent: () => import('./features/contact-requests/contact-requests.component').then(m => m.ContactRequestsComponent) }, { path: 'messages', + title: 'הודעות', loadComponent: () => import('./features/messages/messages.component').then(m => m.MessagesComponent) }, { path: 'lesson-plan', + title: 'בניית מערך שיעור', loadComponent: () => import('./features/ai/lesson-plan.component').then(m => m.LessonPlanComponent) }, { path: 'profile', + title: 'הגדרות פרופיל', loadComponent: () => import('./features/teacher/profile/profile.component').then(m => m.TeacherProfileSettingsComponent) }, { path: 'account', + title: 'הגדרות חשבון', loadComponent: () => import('./features/teacher/account-settings/account-settings.component').then(m => m.AccountSettingsComponent) } @@ -94,28 +108,34 @@ export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', + title: 'ראשי', loadComponent: () => import('./features/parent-portal/parent-dashboard/parent-dashboard.component').then(m => m.ParentDashboardComponent) }, { path: 'lessons', + title: 'יומן שיעורים', loadComponent: () => import('./features/parent-portal/parent-lessons/parent-lessons.component').then(m => m.ParentLessonsComponent) }, { path: 'notes', + title: 'הערות', loadComponent: () => import('./features/parent-portal/parent-notes/parent-notes.component').then(m => m.ParentNotesComponent) }, { path: 'payments', + title: 'תשלומים', loadComponent: () => import('./features/parent-portal/parent-payments/parent-payments.component').then(m => m.ParentPaymentsComponent) }, { path: 'resources', + title: 'חומרי לימוד', loadComponent: () => import('./features/parent-portal/parent-resources/parent-resources.component').then(m => m.ParentResourcesComponent) }, { path: 'messages', + title: 'הודעות', loadComponent: () => import('./features/messages/portal-messages.component').then(m => m.PortalMessagesComponent) } @@ -129,29 +149,35 @@ export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', + title: 'ראשי', loadComponent: () => import('./features/student-portal/student-dashboard/student-dashboard.component').then(m => m.StudentDashboardComponent) }, { path: 'lessons', + title: 'יומן שיעורים', loadComponent: () => import('./features/student-portal/student-lessons/student-lessons.component').then(m => m.StudentLessonsComponent) }, { path: 'notes', + title: 'הערות', loadComponent: () => import('./features/student-portal/student-notes/student-notes.component').then(m => m.StudentNotesComponent) }, { path: 'resources', + title: 'חומרי לימוד', loadComponent: () => import('./features/student-portal/student-resources/student-resources.component').then(m => m.StudentResourcesComponent) }, { path: 'games', + title: 'משחקים', loadComponent: () => import('./features/student-portal/games/games.component').then(m => m.StudentGamesComponent) }, { path: 'messages', + title: 'הודעות', loadComponent: () => import('./features/messages/portal-messages.component').then(m => m.PortalMessagesComponent) } @@ -165,14 +191,17 @@ export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'teachers' }, { path: 'teachers', + title: 'מורות', loadComponent: () => import('./features/admin/admin-teachers/admin-teachers.component').then(m => m.AdminTeachersComponent) }, { path: 'subjects', + title: 'תחומי לימוד', loadComponent: () => import('./features/admin/admin-subjects/admin-subjects.component').then(m => m.AdminSubjectsComponent) }, { path: 'feedback', + title: 'הודעות מהאתר', loadComponent: () => import('./features/admin/admin-feedback/admin-feedback.component').then(m => m.AdminFeedbackComponent) } ] diff --git a/frontend/src/app/core/title/app-title.strategy.ts b/frontend/src/app/core/title/app-title.strategy.ts new file mode 100644 index 0000000..e0b1a71 --- /dev/null +++ b/frontend/src/app/core/title/app-title.strategy.ts @@ -0,0 +1,25 @@ +import { Injectable, inject } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; + +const APP_NAME = 'תלמידון'; + +/** + * מעדכן את כותרת הדף לפי המסלול. + * + * באפליקציה של עמוד יחיד הכותרת לא מתחלפת מעצמה, וכל המסכים נראו בלשונית + * הדפדפן ובהיסטוריה בשם אחד — "תלמידון". למי שעובדת עם קורא מסך זו גם ההודעה + * היחידה שאומרת "עברת מסך", כי אין טעינה מחדש שתכריז על זה. + * + * הכותרת של כל מסלול זהה לכותרת שרואים על המסך, כדי שלשונית והמסך יאמרו + * אותו דבר. + */ +@Injectable({ providedIn: 'root' }) +export class AppTitleStrategy extends TitleStrategy { + private readonly title = inject(Title); + + override updateTitle(snapshot: RouterStateSnapshot): void { + const page = this.buildTitle(snapshot); + this.title.setTitle(page ? `${page} · ${APP_NAME}` : APP_NAME); + } +} diff --git a/frontend/src/app/features/contact-requests/contact-requests.component.html b/frontend/src/app/features/contact-requests/contact-requests.component.html index 6572498..9e743ad 100644 --- a/frontend/src/app/features/contact-requests/contact-requests.component.html +++ b/frontend/src/app/features/contact-requests/contact-requests.component.html @@ -63,7 +63,7 @@

{{ contact.fullName }}

@if (contact.status !== Status.Closed) { } - + } diff --git a/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts b/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts index 9e343a2..d84f16d 100644 --- a/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts +++ b/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts @@ -41,6 +41,7 @@ import { CalendarEventsService } from '../calendar-events.service'; import { CalendarEventItem } from '../calendar-events.models'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; import { LessonAmountSuggestion, lessonDurationMinutes, suggestLessonAmountDetailed } from '../lesson-pricing.util'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-lessons-list', @@ -57,7 +58,7 @@ import { LessonAmountSuggestion, lessonDurationMinutes, suggestLessonAmountDetai SelectModule, TagModule, TextareaModule, - LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe], + LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './lessons-list.component.html' }) export class LessonsListComponent implements OnInit { diff --git a/frontend/src/app/features/messages/messages.component.ts b/frontend/src/app/features/messages/messages.component.ts index 0566003..360e40d 100644 --- a/frontend/src/app/features/messages/messages.component.ts +++ b/frontend/src/app/features/messages/messages.component.ts @@ -15,6 +15,7 @@ import { ConversationComponent } from './conversation.component'; import { MessageAuthor, MessageRecipient, ThreadDetail, ThreadSummary } from './messages.models'; import { MessagesService } from './messages.service'; import { ThreadListComponent } from './thread-list.component'; +import { RestoreFocusOnCloseDirective } from '../../shared/a11y/restore-focus.directive'; /** נמען עם התווית המוכנה — p-select מקבל שם שדה, לא פונקציה. */ type RecipientOption = MessageRecipient & { label: string }; @@ -34,7 +35,7 @@ type RecipientOption = MessageRecipient & { label: string }; PageHeaderComponent, ThreadListComponent, ConversationComponent - ], + , RestoreFocusOnCloseDirective], templateUrl: './messages.component.html', styleUrls: ['./messages.scss', './messages-screen.scss'] }) diff --git a/frontend/src/app/features/messages/portal-messages.component.ts b/frontend/src/app/features/messages/portal-messages.component.ts index 4151563..81358a6 100644 --- a/frontend/src/app/features/messages/portal-messages.component.ts +++ b/frontend/src/app/features/messages/portal-messages.component.ts @@ -16,6 +16,7 @@ import { ConversationComponent } from './conversation.component'; import { MessageAuthor, ThreadDetail, ThreadSummary } from './messages.models'; import { MessagesService } from './messages.service'; import { ThreadListComponent } from './thread-list.component'; +import { RestoreFocusOnCloseDirective } from '../../shared/a11y/restore-focus.directive'; /** * תיבת ההודעות של התלמידה ושל ההורה — אותו מסך לשניהם. @@ -37,7 +38,7 @@ import { ThreadListComponent } from './thread-list.component'; PageHeaderComponent, ThreadListComponent, ConversationComponent - ], + , RestoreFocusOnCloseDirective], templateUrl: './portal-messages.component.html', styleUrls: ['./messages.scss', './messages-screen.scss'] }) diff --git a/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts b/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts index b429547..6af8236 100644 --- a/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts +++ b/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts @@ -31,6 +31,7 @@ import { import { MyChild } from '../parent-portal.models'; import { ParentPortalService } from '../parent-portal.service'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-parent-lessons', @@ -42,7 +43,7 @@ import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; InputTextModule, SelectModule, TagModule, - LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe], + LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './parent-lessons.component.html' }) export class ParentLessonsComponent implements OnInit { diff --git a/frontend/src/app/features/payments/payments-list/payments-list.component.html b/frontend/src/app/features/payments/payments-list/payments-list.component.html index 6e843dc..38797e1 100644 --- a/frontend/src/app/features/payments/payments-list/payments-list.component.html +++ b/frontend/src/app/features/payments/payments-list/payments-list.component.html @@ -240,7 +240,7 @@ - + diff --git a/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts b/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts index 6194d6a..5983cfc 100644 --- a/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts +++ b/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts @@ -19,6 +19,7 @@ import { LESSON_STATUS_CLASS, LESSON_STATUS_LABELS, LESSON_STATUS_SEVERITY, Less import { StudentLesson } from '../student-portal.models'; import { StudentPortalService } from '../student-portal.service'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-student-lessons', @@ -29,7 +30,7 @@ import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; TagModule, TextareaModule, LessonCalendarComponent, - PageHeaderComponent, IsraelDatePipe], + PageHeaderComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './student-lessons.component.html' }) export class StudentLessonsComponent implements OnInit { diff --git a/frontend/src/app/features/students/student-detail/student-detail.component.html b/frontend/src/app/features/students/student-detail/student-detail.component.html index a335804..6ce7d49 100644 --- a/frontend/src/app/features/students/student-detail/student-detail.component.html +++ b/frontend/src/app/features/students/student-detail/student-detail.component.html @@ -54,12 +54,12 @@

הורים מקושרים

{{ parent.fullName }} · {{ parent.email }}
@if (hasWhatsapp(parent.phone)) { - + } @if (hasEmail(parent.email)) { - + } - +
} @@ -109,8 +109,8 @@

הערות פדגוגיות

{{ note.content }}

- - + +
diff --git a/frontend/src/app/features/students/student-detail/student-detail.component.ts b/frontend/src/app/features/students/student-detail/student-detail.component.ts index 0952cbf..c23a03f 100644 --- a/frontend/src/app/features/students/student-detail/student-detail.component.ts +++ b/frontend/src/app/features/students/student-detail/student-detail.component.ts @@ -32,6 +32,7 @@ import { ResourcesService } from '../../resources/resources.service'; import { ParentSummary, StudentDetail } from '../students.models'; import { StudentsService } from '../students.service'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-student-detail', @@ -50,7 +51,7 @@ import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; TextareaModule, TooltipModule, EmptyStateComponent, - ResourceListComponent, IsraelDatePipe], + ResourceListComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './student-detail.component.html' }) export class StudentDetailComponent implements OnInit { diff --git a/frontend/src/app/features/students/students-list/students-list.component.html b/frontend/src/app/features/students/students-list/students-list.component.html index b3db2fe..e13ce0f 100644 --- a/frontend/src/app/features/students/students-list/students-list.component.html +++ b/frontend/src/app/features/students/students-list/students-list.component.html @@ -91,7 +91,7 @@ {{ parent.phone || '—' }} {{ parent.studentCount }} - + diff --git a/frontend/src/app/features/students/students-list/students-list.component.ts b/frontend/src/app/features/students/students-list/students-list.component.ts index 3e3d907..df71c98 100644 --- a/frontend/src/app/features/students/students-list/students-list.component.ts +++ b/frontend/src/app/features/students/students-list/students-list.component.ts @@ -24,6 +24,7 @@ import { Parent } from '../../parents/parents.models'; import { ParentsService } from '../../parents/parents.service'; import { StudentListItem } from '../students.models'; import { StudentsService } from '../students.service'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-students-list', @@ -36,7 +37,7 @@ import { StudentsService } from '../students.service'; SelectModule, TableModule, TabsModule, - TagModule, PageHeaderComponent, EmptyStateComponent], + TagModule, PageHeaderComponent, EmptyStateComponent, RestoreFocusOnCloseDirective], templateUrl: './students-list.component.html' }) export class StudentsListComponent implements OnInit { diff --git a/frontend/src/app/features/teacher/profile/profile.component.html b/frontend/src/app/features/teacher/profile/profile.component.html index f52e0b1..2fb5356 100644 --- a/frontend/src/app/features/teacher/profile/profile.component.html +++ b/frontend/src/app/features/teacher/profile/profile.component.html @@ -216,10 +216,25 @@

שעות זמינות

@for (win of windowsForDay(day); track $index) {
- + עד - - + +
} @empty { לא זמין diff --git a/frontend/src/app/shared/a11y/restore-focus.directive.ts b/frontend/src/app/shared/a11y/restore-focus.directive.ts new file mode 100644 index 0000000..540421f --- /dev/null +++ b/frontend/src/app/shared/a11y/restore-focus.directive.ts @@ -0,0 +1,86 @@ +import { DOCUMENT, Directive, ElementRef, HostListener, Injectable, inject } from '@angular/core'; + +/** + * זוכר לאן המיקוד הלך לאחרונה, כדי שנדע להחזיר אותו כשדיאלוג נסגר. + * + * מאזין אחד לכל האפליקציה, נרשם בפעם הראשונה שדיאלוג כלשהו מבקש אותו. + * שומר היסטוריה קצרה ולא רק את האחרון, כדי שדיאלוג שנפתח מתוך דיאלוג יוכל + * למצוא את מה שהיה ממוקד לפניו ולא את מה שהיה לפני שניהם. + */ +@Injectable({ providedIn: 'root' }) +export class DialogFocusHistory { + private readonly doc = inject(DOCUMENT); + private readonly history: HTMLElement[] = []; + private listening = false; + + start(): void { + if (this.listening) return; + this.listening = true; + // בשלב הלכידה, כי ‎focusin‎ מבועבע אבל אנחנו רוצים לראות גם מה שנעצר בדרך + this.doc.addEventListener( + 'focusin', + (event) => { + const el = event.target as HTMLElement | null; + if (!el || typeof el.focus !== 'function') return; + this.history.push(el); + if (this.history.length > 5) this.history.shift(); + }, + true, + ); + } + + /** האחרון שהיה ממוקד מחוץ ל-‎container‎, אם הוא עדיין קיים במסמך. */ + lastOutside(container: HTMLElement): HTMLElement | null { + for (let i = this.history.length - 1; i >= 0; i--) { + const el = this.history[i]; + if (el.isConnected && !container.contains(el)) return el; + } + return null; + } +} + +/** + * מחזיר את המיקוד לכפתור שפתח את הדיאלוג, כשהדיאלוג נסגר. + * + * ‎p-dialog‎ מעביר את המיקוד לתוך הדיאלוג בפתיחה (‎focusOnShow‎) אבל לא מחזיר + * אותו בסגירה — אין לו בכלל הגדרה כזאת. התוצאה היא שמי שעובדת במקלדת מאבדת + * את המקום: אחרי Escape המיקוד חוזר ל-‎body‎, וצריך לטאב מראש הדף בכל פעם + * מחדש. מי שמשתמשת בעכבר לא מרגישה בזה בכלל. + * + * הבורר הוא ‎p-dialog‎ עצמו, כך שכל דיאלוג בקומפוננטה שמייבאת את ההנחיה מקבל + * את ההתנהגות בלי לשנות את התבנית. + */ +@Directive({ selector: 'p-dialog' }) +export class RestoreFocusOnCloseDirective { + private readonly doc = inject(DOCUMENT); + private readonly host = inject(ElementRef).nativeElement as HTMLElement; + private readonly focusHistory = inject(DialogFocusHistory); + private trigger: HTMLElement | null = null; + + constructor() { + this.focusHistory.start(); + } + + @HostListener('onShow') + protected rememberTrigger(): void { + this.trigger = this.focusHistory.lastOutside(this.host); + } + + @HostListener('onHide') + protected returnFocus(): void { + const target = this.trigger; + this.trigger = null; + if (!target) return; + + // אחרי סגירה יש עוד סיבוב של אנימציה והסרה מה-DOM; מחכים לו לפני שמחזירים + setTimeout(() => { + // הכפתור שפתח כבר לא קיים — מחיקה, למשל. עדיף להשאיר כמו שהוא מאשר + // לקפוץ למקום שרירותי. + if (!target.isConnected) return; + // מישהו כבר העביר את המיקוד למקום אמיתי — לא גוזלים אותו ממנו + const active = this.doc.activeElement; + if (active && active !== this.doc.body && this.doc.contains(active)) return; + target.focus(); + }); + } +} diff --git a/frontend/src/app/shared/ui/beta-notice.component.ts b/frontend/src/app/shared/ui/beta-notice.component.ts index 261d81f..7f8273d 100644 --- a/frontend/src/app/shared/ui/beta-notice.component.ts +++ b/frontend/src/app/shared/ui/beta-notice.component.ts @@ -8,6 +8,7 @@ import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { environment } from '../../../environments/environment'; import { extractErrorMessage } from '../../core/http/extract-error-message'; +import { RestoreFocusOnCloseDirective } from '../../shared/a11y/restore-focus.directive'; /** * תג "בהרצה" קבוע בפינה השמאלית התחתונה, ובלחיצה — טופס לשליחת רעיון או תקלה. @@ -25,7 +26,7 @@ import { extractErrorMessage } from '../../core/http/extract-error-message'; */ @Component({ selector: 'app-beta-notice', - imports: [FormsModule, ButtonModule, DialogModule, InputTextModule, TextareaModule], + imports: [FormsModule, ButtonModule, DialogModule, InputTextModule, TextareaModule, RestoreFocusOnCloseDirective], template: `