@@ -80,21 +82,73 @@ import { LocaleStore } from './translations/locale.service'; `, styles: [ ` - .navbar { - display: flex; - align-items: center; - justify-content: space-between; - background-color: var(--primary-color); - padding: 0 var(--spacing-lg); - height: 4rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); - position: sticky; - top: 0; - z-index: 1000; - transition: background-color 0.3s; + :host ::ng-deep { + .custom-menubar { + background: var(--primary-color); + border: none; + border-radius: 0; + padding: 0 var(--spacing-lg); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); + position: sticky; + top: 0; + z-index: 1000; + transition: background-color 0.3s; + + .p-menubar-root-list { + background: transparent; + border: none; + padding: 0; + } + + .p-menuitem-link { + color: rgba(255, 255, 255, 0.95) !important; + font-weight: 500; + padding: var(--spacing-sm) var(--spacing-md) !important; + border-radius: var(--input-radius); + transition: all 0.2s; + + &:hover { + background: rgba(255, 255, 255, 0.15) !important; + color: #fff !important; + } + + &.p-highlight { + background: rgba(255, 255, 255, 0.25) !important; + color: #fff !important; + } + + .p-menuitem-icon { + color: rgba(255, 255, 255, 0.95) !important; + font-weight: 600; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + } + + .p-menubar-button { + color: #fff !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; + + &:hover { + background: rgba(255, 255, 255, 0.1) !important; + } + } + } } - .navbar-left .app-title { + .logo-container { + display: flex; + align-items: center; + gap: var(--spacing-sm); + } + + .logo-icon { + font-size: 1.5rem; + color: #fff; + } + + .logo-text { color: #fff; font-size: 1.5rem; font-weight: 700; @@ -102,78 +156,107 @@ import { LocaleStore } from './translations/locale.service'; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); } - .navbar-center a { - color: rgba(255, 255, 255, 0.85); - margin: 0 var(--spacing-md); - text-decoration: none; - font-weight: 500; - padding: var(--spacing-sm) 0; - position: relative; - transition: color 0.2s; + .user-actions { + display: flex; + align-items: center; + gap: var(--spacing-md); + } + + .theme-toggle-btn { + background: rgba(255, 255, 255, 0.15) !important; + color: #fff !important; + border: 1px solid rgba(255, 255, 255, 0.25) !important; + width: 2.5rem; + height: 2.5rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - &.active, &:hover { - color: #fff; - - &::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 100%; - height: 2px; - background-color: #fff; - border-radius: 2px; - } + background: rgba(255, 255, 255, 0.25) !important; + border-color: rgba(255, 255, 255, 0.35) !important; } } - .navbar-right { + .language-selector { + background: rgba(255, 255, 255, 0.15) !important; + color: #fff !important; + border: 1px solid rgba(255, 255, 255, 0.25) !important; + min-width: 3rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + + &:hover { + background: rgba(255, 255, 255, 0.25) !important; + border-color: rgba(255, 255, 255, 0.35) !important; + } + } + + .user-profile { display: flex; align-items: center; - gap: 1rem; + gap: var(--spacing-sm); } .avatar { - width: 2.5rem; - height: 2.5rem; + width: 2rem; + height: 2rem; border-radius: 50%; - margin-right: var(--spacing-sm); object-fit: cover; border: 2px solid rgba(255, 255, 255, 0.3); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); background: #fff; } .username { color: #fff; font-weight: 500; - margin-left: 0.25rem; + font-size: 0.9rem; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); } - .theme-toggle-btn { - background: #fff !important; - color: var(--primary-color) !important; - border-radius: 50%; - cursor: pointer; + .language-options { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + min-width: 120px; } - .language-selector { - background: #fff !important; - color: var(--primary-color) !important; - cursor: pointer; - } + :host ::ng-deep { + .p-popover { + background-color: var(--card-background) !important; + border: 1px solid var(--table-border-color) !important; + box-shadow: var(--card-shadow) !important; + border-radius: var(--border-radius) !important; - .dark-theme .language-selector { - background: #fff !important; - color: var(--primary-color) !important; - border: 1.5px solid var(--primary-light); - } + .p-popover-content { + background-color: var(--card-background) !important; + color: var(--text-color) !important; + padding: var(--spacing-md) !important; + } - :root .theme-toggle-btn { - --theme-toggle-bg: #f3f4f8; - --theme-toggle-icon: #23272f; + .p-popover-arrow { + background-color: var(--card-background) !important; + border-color: var(--table-border-color) !important; + } + } + + .p-button-text { + color: var(--text-color) !important; + background: transparent !important; + border: none !important; + padding: var(--spacing-sm) var(--spacing-md) !important; + border-radius: var(--input-radius) !important; + transition: all 0.2s !important; + text-align: left !important; + width: 100% !important; + + &:hover { + background-color: var(--primary-lighter) !important; + color: var(--primary-color) !important; + } + + &.active { + background-color: var(--primary-color) !important; + color: #ffffff !important; + } + } } .main-content { @@ -190,19 +273,14 @@ import { LocaleStore } from './translations/locale.service'; } @media (max-width: 768px) { - .navbar { - flex-direction: column; - height: auto; - padding: var(--spacing-md); + .user-actions { + gap: var(--spacing-sm); } - .navbar-center { - margin: var(--spacing-md) 0; - } - - .navbar-right { - width: 100%; - justify-content: flex-end; + .user-profile { + .username { + display: none; + } } .main-content { @@ -217,13 +295,21 @@ export class App implements OnInit { key: 'theme', }); private readonly locale = inject(LocaleStore); + private readonly t = injectAppT(); currentLocale = computed(() => this.locale.getCurrentLocale()); - isDarkMode = computed(() => this.theme() === 'dark'); - overviewUrl = computed(() => `/${this.currentLocale()}/students`); - addStudentUrl = computed(() => `/${this.currentLocale()}/students/add`); + menuItems = computed(() => [ + { + label: this.t('app.overview'), + routerLink: `/${this.currentLocale()}/students`, + }, + { + label: this.t('app.addStudent'), + routerLink: `/${this.currentLocale()}/students/add`, + }, + ]); ngOnInit() { this.applyTheme(); diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 764da27..22e0067 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -31,4 +31,3 @@ export const appConfig: ApplicationConfig = { }), ], }; - diff --git a/src/app/edit-student.component.ts b/src/app/edit-student.component.ts index c6d4fb5..c226c37 100644 --- a/src/app/edit-student.component.ts +++ b/src/app/edit-student.component.ts @@ -1,11 +1,19 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject, OnInit, signal } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { Component, computed, inject, OnInit } from '@angular/core'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { Router } from '@angular/router'; import { queryParam } from '@mmstack/router-core'; +import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; +import { CardModule } from 'primeng/card'; import { InputTextModule } from 'primeng/inputtext'; import { MultiSelectModule } from 'primeng/multiselect'; +import { ToastModule } from 'primeng/toast'; import { StudentService } from './student.service'; import { LocaleStore } from './translations/locale.service'; @@ -14,131 +22,333 @@ import { LocaleStore } from './translations/locale.service'; standalone: true, imports: [ CommonModule, - FormsModule, + ReactiveFormsModule, InputTextModule, MultiSelectModule, ButtonModule, + CardModule, + ToastModule, ], + providers: [MessageService], template: ` -
-

Edit Student

+
@if (student()) { -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ + +
+ +

Edit Student

+
+
+ +
+
+ + +
+ + {{ getFieldError('name') }} +
+
+ +
+ + +
+ + {{ getFieldError('email') }} +
+
+ +
+ + +
+ + {{ getFieldError('courses') }} +
+
+ +
+ + +
+
+
+
} @if (!student()) { -
- -

Student not found. Please return to the overview page.

- -
+ + +
+ +

Student Not Found

+

+ The student you're looking for doesn't exist or has been removed. +

+ +
+
+
}
+ + `, styles: [ ` + .form-container { + max-width: 600px; + margin: 2rem auto; + padding: 0 var(--spacing-md); + } + :host ::ng-deep { - .p-inputtext:disabled { - opacity: 0.8; - background-color: #f5f5f5; - color: var(--text-secondary); + .form-card { + border-radius: var(--border-radius); + box-shadow: var(--card-shadow); + background: var(--card-background); + border: 1px solid var(--table-border-color); + + .p-card-header { + background: linear-gradient( + 135deg, + var(--primary-color), + var(--primary-light) + ); + color: #ffffff !important; + padding: var(--spacing-lg); + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + + .p-card-body { + padding: var(--spacing-xl); + } } - .p-inputtext, - .p-multiselect { + .error-card { + border-radius: var(--border-radius); + box-shadow: var(--card-shadow); + background: var(--card-background); + border: 1px solid #e24c4c; + + .p-card-body { + padding: var(--spacing-xl); + } + } + + .form-input, + .form-multiselect { + width: 100%; background-color: var(--card-background) !important; color: var(--text-color) !important; border-radius: var(--input-radius) !important; border: 1px solid var(--table-border-color) !important; transition: border-color 0.2s, box-shadow 0.2s, background-color 0.3s, color 0.3s; + padding: var(--spacing-sm) var(--spacing-md) !important; + + &:enabled:focus { + border-color: var(--primary-color) !important; + box-shadow: var(--focus-ring) !important; + } + + &:enabled:hover { + border-color: var(--primary-light) !important; + } + + &.ng-invalid.ng-dirty { + border-color: #e24c4c !important; + box-shadow: 0 0 0 2px rgba(226, 76, 76, 0.2) !important; + } } - .p-inputtext:enabled:focus, - .p-multiselect:not(.p-disabled).p-focus { - border-color: var(--primary-color); - box-shadow: var(--focus-ring); - } + .p-multiselect { + min-height: 2.5rem; - .p-inputtext:enabled:hover, - .p-multiselect:not(.p-disabled):hover { - border-color: var(--primary-light); + .p-multiselect-label { + color: var(--text-color) !important; + } + + .p-multiselect-trigger { + color: var(--text-secondary) !important; + } } } - .card-container { - max-width: 480px; - margin: 2rem auto; + .card-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + } + + .header-icon { + font-size: 1.5rem; + color: #ffffff !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + .page-title { + margin: 0; + font-weight: 700; + font-size: 1.5rem; + letter-spacing: 0.5px; + color: #ffffff !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + .student-form { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); + } + + .form-field { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + } + + .form-label { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-weight: 600; + color: var(--text-color); + letter-spacing: 0.2px; + } + + .field-icon { + color: var(--primary-color); + font-size: 0.875rem; + } + + .error-message { + display: flex; + align-items: center; + gap: var(--spacing-xs); + color: #e24c4c; + font-size: 0.875rem; + font-weight: 500; + margin-top: var(--spacing-xs); } .form-actions { - margin-top: var(--spacing-lg); + display: flex; + justify-content: flex-end; + gap: var(--spacing-md); + margin-top: var(--spacing-xl); + padding-top: var(--spacing-lg); + border-top: 1px solid var(--table-border-color); } - .not-found-message { - text-align: center; - padding: 2rem; - color: #d32f2f; + .not-found-content { display: flex; flex-direction: column; align-items: center; - gap: 1rem; + text-align: center; + gap: var(--spacing-lg); } - .not-found-message p { - font-size: 1.2rem; - margin: 1rem 0; + .error-icon { + font-size: 3rem; + color: #e24c4c; + } + + .not-found-content h3 { + margin: 0; + color: var(--text-color); + font-weight: 600; + font-size: 1.5rem; + } + + .not-found-content p { + margin: 0; + color: var(--text-secondary); + font-size: 1rem; + max-width: 400px; + } + + @media (max-width: 768px) { + .form-container { + margin: 1rem auto; + padding: 0 var(--spacing-sm); + } + + :host ::ng-deep { + .form-card .p-card-body, + .error-card .p-card-body { + padding: var(--spacing-lg); + } + } + + .form-actions { + flex-direction: column; + + .p-button { + width: 100%; + } + } } `, ], @@ -148,16 +358,20 @@ export class EditStudentComponent implements OnInit { private readonly localeStore = inject(LocaleStore); private readonly router = inject(Router); private readonly queryId = queryParam('id'); + private readonly fb = inject(FormBuilder); + private readonly messageService = inject(MessageService); + + protected isSubmitting = false; protected student = computed(() => this.studentService.getStudentById(this.queryId() ?? '') ); - protected editedStudent = signal({ - id: '', - name: '', - email: '', - courses: [] as string[], + protected studentForm: FormGroup = this.fb.group({ + id: [''], + name: ['', [Validators.required, Validators.minLength(2)]], + email: ['', [Validators.required, Validators.email]], + courses: [[], [Validators.required, Validators.minLength(1)]], }); protected courseOptions = computed(() => { @@ -170,26 +384,71 @@ export class EditStudentComponent implements OnInit { 'History', 'Literature', 'Physical Education', + 'Art', + 'Music', + 'Geography', + 'Economics', ].map((course) => ({ label: course, value: course })); }); ngOnInit() { const currentStudent = this.student(); if (currentStudent) { - this.editedStudent.set({ + this.studentForm.patchValue({ id: currentStudent.id, name: currentStudent.name, email: currentStudent.email, courses: [...currentStudent.courses], }); - } else { - console.error('Student not found'); } } + protected isFieldInvalid(fieldName: string): boolean { + const field = this.studentForm.get(fieldName); + return field ? field.invalid && field.dirty : false; + } + + protected getFieldError(fieldName: string): string { + const field = this.studentForm.get(fieldName); + if (!field || !field.errors) return ''; + + if (field.errors['required']) return `${fieldName} is required`; + if (field.errors['email']) return 'Please enter a valid email address'; + if (field.errors['minlength']) { + const requiredLength = field.errors['minlength'].requiredLength; + return `${fieldName} must be at least ${requiredLength} characters`; + } + if (field.errors['minLength']) return `Please select at least one course`; + + return 'Invalid input'; + } + save() { - this.studentService.updateStudent(this.editedStudent()); - this.router.navigate([`/${this.localeStore.getCurrentLocale()}/students`]); + if (this.studentForm.valid) { + this.isSubmitting = true; + + const formValue = this.studentForm.value; + this.studentService.updateStudent(formValue); + + this.messageService.add({ + severity: 'success', + summary: 'Success', + detail: `Student ${formValue.name} has been updated successfully`, + }); + + setTimeout(() => { + this.router.navigate([ + `/${this.localeStore.getCurrentLocale()}/students`, + ]); + }, 1000); + } else { + this.studentForm.markAllAsTouched(); + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'Please fix the errors in the form', + }); + } } cancel() { diff --git a/src/app/overview.component.ts b/src/app/overview.component.ts index 34d4154..901ef74 100644 --- a/src/app/overview.component.ts +++ b/src/app/overview.component.ts @@ -1,25 +1,55 @@ import { CommonModule } from '@angular/common'; import { Component, computed, inject } from '@angular/core'; import { Router } from '@angular/router'; +import { ConfirmationService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { TableModule } from 'primeng/table'; +import { TagModule } from 'primeng/tag'; +import { TooltipModule } from 'primeng/tooltip'; import { StudentService } from './student.service'; +import { injectAppT } from './translations/app.t'; import { LocaleStore } from './translations/locale.service'; import { AppTranslatePipe } from './translations/translate.pipe'; @Component({ selector: 'app-overview', standalone: true, - imports: [CommonModule, TableModule, ButtonModule, AppTranslatePipe], + imports: [ + CommonModule, + TableModule, + ButtonModule, + ConfirmDialogModule, + TagModule, + TooltipModule, + AppTranslatePipe, + ], + providers: [ConfirmationService], template: ` -
-
-

{{ 'app.studentOverview' | translate }}

-
+
+
+
+
+ +

{{ 'app.studentOverview' | translate }}

+
+
+
+ +
+ {{ students().length }} + {{ + 'app.totalStudents' | translate + }} +
+
+
+
+
@@ -46,41 +78,86 @@ import { AppTranslatePipe } from './translations/translate.pipe'; [stripedRows]="true" styleClass="p-datatable-rounded" [responsiveLayout]="'stack'" + [loading]="false" + [showLoader]="false" > - Name - Email - Courses - Actions + +
+ + {{ 'app.name' | translate }} +
+ + +
+ + {{ 'app.email' | translate }} +
+ + +
+ + {{ 'app.courses' | translate }} +
+ + +
+ + {{ 'app.actions' | translate }} +
+
- + - Name{{ st.name }} - Email{{ st.email }} - Courses{{ st.courses.join(', ') }} + {{ 'app.name' | translate }} +
+ + {{ student.name }} +
- Actions + {{ 'app.email' | translate }} +
+ + {{ student.email }} +
+ + + {{ + 'app.courses' | translate + }} +
+ +
+ + + {{ + 'app.actions' | translate + }}
@@ -88,50 +165,130 @@ import { AppTranslatePipe } from './translations/translate.pipe';
- - No students found. Add your first student! + +
+ +

{{ 'app.noStudentsFound' | translate }}

+

{{ 'app.startByAdding' | translate }}

+ +
+ + `, styles: [ ` - .header-actions { + .overview-container { display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - gap: 1rem; + flex-direction: column; + gap: var(--spacing-xl); } - .action-buttons-header { + .header-section { display: flex; - gap: 1rem; + justify-content: space-between; + align-items: flex-start; + gap: var(--spacing-lg); + flex-wrap: wrap; + } + + .header-content { + display: flex; + align-items: center; + gap: var(--spacing-xl); + } + + .title-section { + display: flex; + align-items: center; + gap: var(--spacing-md); + } + + .title-icon { + font-size: 2rem; + color: var(--primary-color); } .page-title { - margin-bottom: 0; + margin: 0; + color: var(--primary-color); + font-weight: 700; + font-size: 2rem; + letter-spacing: 0.5px; + } + + .stats-section { + display: flex; + gap: var(--spacing-md); + } + + .stat-card { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md); + background: var(--card-background); + border-radius: var(--border-radius); + box-shadow: var(--card-shadow); + border: 1px solid var(--table-border-color); + } + + .stat-icon { + font-size: 1.5rem; + color: var(--primary-color); + } + + .stat-content { + display: flex; + flex-direction: column; + } + + .stat-number { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-color); + } + + .stat-label { + font-size: 0.875rem; + color: var(--text-secondary); + font-weight: 500; } .action-buttons { display: flex; - gap: 0.5rem; - justify-content: center; + gap: var(--spacing-md); } :host ::ng-deep { .p-datatable { border-radius: var(--border-radius); overflow: hidden; - box-shadow: var(--card-shadow); background-color: var(--card-background); .p-datatable-header { background-color: var(--card-background) !important; border: none; - padding: 1.25rem 1.5rem; + padding: 0 !important; + border-bottom: 1px solid var(--table-border-color); } .p-datatable-thead > tr > th { @@ -175,9 +332,9 @@ import { AppTranslatePipe } from './translations/translate.pipe'; } &.p-highlight { - background-color: #000000 !important; + background-color: var(--primary-color) !important; color: #ffffff !important; - border-color: #000000 !important; + border-color: var(--primary-color) !important; font-weight: 600; } } @@ -200,13 +357,101 @@ import { AppTranslatePipe } from './translations/translate.pipe'; transition: background 0.2s, color 0.2s, box-shadow 0.2s; box-shadow: none; } + + .p-tag { + margin-right: var(--spacing-xs); + margin-bottom: var(--spacing-xs); + } + } + + .column-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + } + + .column-icon { + font-size: 0.875rem; + color: var(--text-secondary); + } + + .student-name { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-weight: 600; + } + + .student-icon { + color: var(--primary-color); + font-size: 0.875rem; + } + + .student-email { + display: flex; + align-items: center; + gap: var(--spacing-sm); + } + + .email-icon { + color: var(--text-secondary); + font-size: 0.875rem; + } + + .courses-container { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); + } + + .action-buttons { + display: flex; + gap: var(--spacing-sm); + justify-content: center; + } + + .empty-state { + text-align: center; + padding: var(--spacing-xl) !important; + } + + .empty-content { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-md); + } + + .empty-icon { + font-size: 3rem; + color: var(--text-secondary); + } + + .empty-content h3 { + margin: 0; + color: var(--text-color); + font-weight: 600; + } + + .empty-content p { + margin: 0; + color: var(--text-secondary); } @media screen and (max-width: 768px) { - .header-actions { + .header-section { flex-direction: column; align-items: stretch; - gap: 1rem; + } + + .header-content { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + } + + .action-buttons { + justify-content: stretch; } :host ::ng-deep { @@ -232,7 +477,11 @@ export class OverviewComponent { private readonly router = inject(Router); private readonly studentsService = inject(StudentService); private readonly localeStore = inject(LocaleStore); + private readonly confirmationService = inject(ConfirmationService); + private readonly t = injectAppT(); + protected readonly students = computed(() => this.studentsService.students()); + protected generateMock = () => this.studentsService.generateMockData(); goToAdd() { @@ -252,6 +501,15 @@ export class OverviewComponent { ); } + confirmDelete(student: any) { + this.confirmationService.confirm({ + message: `Are you sure you want to delete ${student.name}?`, + accept: () => { + this.deleteStudent(student.id); + }, + }); + } + deleteStudent(id: string) { this.studentsService.deleteSutdent(id); } diff --git a/src/app/student.service.ts b/src/app/student.service.ts index 8f6728a..1fa59da 100644 --- a/src/app/student.service.ts +++ b/src/app/student.service.ts @@ -8,6 +8,8 @@ export type Student = { name: string; email: string; courses: string[]; + createdAt?: Date; + updatedAt?: Date; }; @Injectable({ @@ -18,41 +20,117 @@ export class StudentService { key: 'students', }); - courses = ['Mathematics', 'Physics', 'Chemistry', 'History', 'Literature']; + courses = [ + 'Mathematics', + 'Physics', + 'Chemistry', + 'Biology', + 'Computer Science', + 'History', + 'Literature', + 'Physical Education', + 'Art', + 'Music', + 'Geography', + 'Economics', + ]; - addStudent({ name, email, courses }: Omit) { + addStudent({ + name, + email, + courses, + }: Omit) { const newStudent: Student = { id: uuid(), - name, - email, - courses, + name: name.trim(), + email: email.trim().toLowerCase(), + courses: courses.filter((course) => course.trim() !== ''), + createdAt: new Date(), + updatedAt: new Date(), }; this.students.update((students) => [...students, newStudent]); + return newStudent; } - getStudentById(id: string) { + getStudentById(id: string): Student | undefined { return this.students().find((student) => student.id === id); } - updateStudent(student: Partial) { + updateStudent(student: Partial & { id: string }) { this.students.update((students) => - students.map((s) => (s.id === student.id ? { ...s, ...student } : s)) + students.map((s) => + s.id === student.id + ? { + ...s, + ...student, + name: student.name?.trim() || s.name, + email: student.email?.trim().toLowerCase() || s.email, + courses: + student.courses?.filter((course) => course.trim() !== '') || + s.courses, + updatedAt: new Date(), + } + : s + ) ); } generateMockData() { - const mockData: Student[] = generateRandom(); + const mockData: Student[] = generateRandom().map((student) => ({ + ...student, + createdAt: new Date(), + updatedAt: new Date(), + })); this.students.set(mockData); } updateStudentCourse(id: string, courses: string[]) { this.students.update((students) => - students.map((s) => (s.id === id ? { ...s, courses } : s)) + students.map((s) => + s.id === id + ? { + ...s, + courses: courses.filter((course) => course.trim() !== ''), + updatedAt: new Date(), + } + : s + ) ); } deleteSutdent(id: string) { this.students.update((students) => students.filter((s) => s.id !== id)); } + + getStudentsByCourse(course: string): Student[] { + return this.students().filter((student) => + student.courses.some((c) => + c.toLowerCase().includes(course.toLowerCase()) + ) + ); + } + + searchStudents(query: string): Student[] { + const searchTerm = query.toLowerCase(); + return this.students().filter( + (student) => + student.name.toLowerCase().includes(searchTerm) || + student.email.toLowerCase().includes(searchTerm) || + student.courses.some((course) => + course.toLowerCase().includes(searchTerm) + ) + ); + } + + getTotalStudents(): number { + return this.students().length; + } + + getStudentsByDateRange(startDate: Date, endDate: Date): Student[] { + return this.students().filter((student) => { + const createdAt = student.createdAt || new Date(); + return createdAt >= startDate && createdAt <= endDate; + }); + } } diff --git a/src/app/translations/app-sl.translation.ts b/src/app/translations/app-sl.translation.ts index 12764bf..78c38e9 100644 --- a/src/app/translations/app-sl.translation.ts +++ b/src/app/translations/app-sl.translation.ts @@ -2,5 +2,55 @@ import { createAppTranslation } from './app.namespace'; export default createAppTranslation('sl', { studentOverview: 'Pregled študentov', + addNewStudent: 'Dodaj novega študenta', + generateMockData: 'Ustvari testne podatke', + generateSampleData: 'Ustvari vzorčne podatke študentov', + totalStudents: 'Skupaj študentov', + editStudent: 'Uredi študenta', + deleteStudent: 'Izbriši študenta', + confirmDelete: 'Potrdi brisanje', + confirmDeleteMessage: 'Ali ste prepričani, da želite izbrisati {name}?', + delete: 'Izbriši', + cancel: 'Prekliči', + noStudentsFound: 'Ni najdenih študentov', + startByAdding: 'Začnite z dodajanjem prvega študenta v sistem.', + addFirstStudent: 'Dodaj prvega študenta', + showingStudents: 'Prikazujem {first} do {last} od {totalRecords} študentov', + name: 'Ime', + email: 'E-pošta', + courses: 'Predmeti', + actions: 'Akcije', + fullName: 'Polno ime', + emailAddress: 'E-poštni naslov', + enterStudentName: 'Vnesite polno ime študenta', + enterStudentEmail: 'Vnesite e-poštni naslov študenta', + selectCourses: 'Izberite predmete za študenta', + saveChanges: 'Shrani spremembe', + studentNotFound: 'Študent ni najden', + studentNotFoundMessage: + 'Študent, ki ga iščete, ne obstaja ali je bil odstranjen.', + backToOverview: 'Nazaj na pregled', + validationErrors: { + nameRequired: 'Ime je obvezno', + nameMinLength: 'Ime mora vsebovati vsaj {length} znakov', + emailRequired: 'E-pošta je obvezna', + emailInvalid: 'Vnesite veljaven e-poštni naslov', + coursesRequired: 'Izberite vsaj en predmet', + }, + success: { + studentAdded: 'Študent {name} je bil uspešno dodan', + studentUpdated: 'Študent {name} je bil uspešno posodobljen', + }, + error: { + validationError: 'Napaka pri preverjanju', + fixFormErrors: 'Prosimo, popravite napake v obrazcu', + }, + overview: 'Pregled', + overviewDescription: 'Ogled in upravljanje vseh študentov', + addStudent: 'Dodaj študenta', + addStudentDescription: 'Dodaj novega študenta v sistem', + toggleTheme: 'Preklopi temo', + changeLanguage: 'Spremeni jezik', + english: 'Angleščina', + slovenian: 'Slovenščina', }); - diff --git a/src/app/translations/app.namespace.ts b/src/app/translations/app.namespace.ts index 945e3d0..341b303 100644 --- a/src/app/translations/app.namespace.ts +++ b/src/app/translations/app.namespace.ts @@ -2,6 +2,57 @@ import { createNamespace } from '@mmstack/translate'; const ns = createNamespace('app', { studentOverview: 'Student Overview', + addNewStudent: 'Add New Student', + generateMockData: 'Generate Mock Data', + generateSampleData: 'Generate sample student data', + totalStudents: 'Total Students', + editStudent: 'Edit Student', + deleteStudent: 'Delete Student', + confirmDelete: 'Confirm Delete', + confirmDeleteMessage: 'Are you sure you want to delete {name}?', + delete: 'Delete', + cancel: 'Cancel', + noStudentsFound: 'No students found', + startByAdding: 'Start by adding your first student to the system.', + addFirstStudent: 'Add First Student', + showingStudents: 'Showing {first} to {last} of {totalRecords} students', + name: 'Name', + email: 'Email', + courses: 'Courses', + actions: 'Actions', + fullName: 'Full Name', + emailAddress: 'Email Address', + enterStudentName: "Enter student's full name", + enterStudentEmail: "Enter student's email address", + selectCourses: 'Select courses for the student', + saveChanges: 'Save Changes', + studentNotFound: 'Student Not Found', + studentNotFoundMessage: + "The student you're looking for doesn't exist or has been removed.", + backToOverview: 'Back to Overview', + validationErrors: { + nameRequired: 'Name is required', + nameMinLength: 'Name must be at least {length} characters', + emailRequired: 'Email is required', + emailInvalid: 'Please enter a valid email address', + coursesRequired: 'Please select at least one course', + }, + success: { + studentAdded: 'Student {name} has been added successfully', + studentUpdated: 'Student {name} has been updated successfully', + }, + error: { + validationError: 'Validation Error', + fixFormErrors: 'Please fix the errors in the form', + }, + overview: 'Overview', + overviewDescription: 'View and manage all students', + addStudent: 'Add Student', + addStudentDescription: 'Add a new student to the system', + toggleTheme: 'Toggle theme', + changeLanguage: 'Change language', + english: 'English', + slovenian: 'Slovenian', }); export default ns.translation; @@ -9,4 +60,3 @@ export default ns.translation; export type AppLocale = (typeof ns)['translation']; export const createAppTranslation = ns.createTranslation; - diff --git a/src/styles.scss b/src/styles.scss index 7f2b71c..f8c2f15 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -3,83 +3,96 @@ html, body { margin: 0; - font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; + font-family: 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif; background-color: var(--background-color); color: var(--text-color); transition: background-color 0.3s, color 0.3s; min-height: 100vh; + line-height: 1.6; } :root { - // Base colors - --primary-color: #6200ee; - --primary-light: #7f39fb; - --primary-dark: #3700b3; - --accent-color: #03dac6; - --text-color: #23272f; - --text-secondary: #555a64; - --background-color: #f4f6fa; + --primary-color: #6366f1; + --primary-light: #818cf8; + --primary-dark: #4f46e5; + --primary-lighter: #e0e7ff; + --accent-color: #10b981; + --accent-light: #34d399; + --text-color: #1f2937; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --background-color: #f9fafb; + --background-secondary: #f3f4f6; - // Card & containers - --card-background: #fff; - --card-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + --card-background: #ffffff; + --card-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --card-shadow-hover: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - // Table colors - --table-header-bg: #f3f4f8; - --table-row-bg: #fff; - --table-row-hover-bg: #f0f0f5; - --table-border-color: #e0e3ea; + --table-header-bg: #f8fafc; + --table-row-bg: #ffffff; + --table-row-hover-bg: #f1f5f9; + --table-border-color: #e2e8f0; - // Pagination - --paginator-button-selected-bg: #000000; + --paginator-button-hover-bg: rgba(99, 102, 241, 0.1); + --paginator-button-selected-bg: #6366f1; --paginator-button-selected-text: #ffffff; - --paginator-button-hover-bg: rgba(98, 0, 238, 0.08); - // Spacing --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; + --spacing-2xl: 3rem; - // Border radius --border-radius: 12px; + --border-radius-sm: 8px; + --border-radius-lg: 16px; --input-radius: 8px; - // Focus - --focus-ring: 0 0 0 2px var(--primary-light); + --focus-ring: 0 0 0 3px rgba(99, 102, 241, 0.1); + --focus-ring-error: 0 0 0 3px rgba(239, 68, 68, 0.1); + + --transition-fast: 0.15s ease; + --transition-normal: 0.3s ease; + --transition-slow: 0.5s ease; } .dark-theme { - --primary-color: #bb86fc; - --primary-light: #e2b9ff; - --primary-dark: #3700b3; - --accent-color: #03dac6; - --text-color: #e0e0e0; - --text-secondary: #b0b0b0; - --background-color: #181a20; - --card-background: #23242a; - --card-shadow: 0 4px 16px rgba(0, 0, 0, 0.32); - --table-header-bg: #23242a; - --table-row-bg: #181a20; - --table-row-hover-bg: #23242a; - --table-border-color: #33343a; - --paginator-button-hover-bg: rgba(187, 134, 252, 0.12); - --paginator-button-selected-bg: #000000; + --primary-color: #a855f7; + --primary-light: #c084fc; + --primary-dark: #9333ea; + --primary-lighter: #f3e8ff; + --accent-color: #10b981; + --accent-light: #34d399; + --text-color: #f9fafb; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --background-color: #111827; + --background-secondary: #1f2937; + + --card-background: #1f2937; + --card-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); + --card-shadow-hover: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + + --table-header-bg: #374151; + --table-row-bg: #1f2937; + --table-row-hover-bg: #374151; + --table-border-color: #374151; + + --paginator-button-hover-bg: rgba(168, 85, 247, 0.2); + --paginator-button-selected-bg: #a855f7; --paginator-button-selected-text: #ffffff; } -// Typography .page-title { margin-bottom: var(--spacing-lg); color: var(--primary-color); font-weight: 700; font-size: 2rem; - letter-spacing: 0.5px; - transition: color 0.3s; + letter-spacing: -0.025em; + transition: color var(--transition-normal); } -// Layout patterns .flex-row { display: flex; align-items: center; @@ -95,17 +108,20 @@ body { gap: var(--spacing-md); } -// Cards and containers .card-container { background-color: var(--card-background); border-radius: var(--border-radius); padding: var(--spacing-xl) var(--spacing-lg); box-shadow: var(--card-shadow); margin-bottom: var(--spacing-xl); - transition: background-color 0.3s, box-shadow 0.3s; + transition: background-color var(--transition-normal), box-shadow var(--transition-normal); + border: 1px solid var(--table-border-color); +} + +.card-container:hover { + box-shadow: var(--card-shadow-hover); } -// Forms .form-field { display: flex; flex-direction: column; @@ -116,7 +132,8 @@ body { font-weight: 600; margin-bottom: var(--spacing-sm); color: var(--text-secondary); - letter-spacing: 0.2px; + letter-spacing: 0.025em; + font-size: 0.875rem; } .form-actions { @@ -135,9 +152,9 @@ input[type='email'], padding: var(--spacing-sm) var(--spacing-md) !important; background-color: var(--card-background) !important; color: var(--text-color) !important; - transition: border-color 0.2s, box-shadow 0.2s, background-color 0.3s, - color 0.3s; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-normal), color var(--transition-normal); box-shadow: none !important; + font-size: 0.875rem; } input[type='text']:focus, @@ -157,15 +174,15 @@ input[type='email']:focus, color: var(--text-color) !important; } -// PrimeNG component styling .p-component-styles { - // Buttons .p-button { border-radius: var(--input-radius); font-weight: 600; - letter-spacing: 0.2px; - transition: background 0.2s, color 0.2s, box-shadow 0.2s; + letter-spacing: 0.025em; + transition: all var(--transition-fast); box-shadow: none; + font-size: 0.875rem; + padding: var(--spacing-sm) var(--spacing-md); &.p-button-primary { background-color: var(--primary-color); @@ -176,18 +193,20 @@ input[type='email']:focus, background-color: var(--primary-light); color: #fff; box-shadow: var(--focus-ring); + transform: translateY(-1px); } } &.p-button-secondary { background-color: var(--accent-color); border: none; - color: #23272f; + color: #fff; &:hover, &:focus { - background-color: #26ffe6; - color: #23272f; + background-color: var(--accent-light); + color: #fff; box-shadow: var(--focus-ring); + transform: translateY(-1px); } } @@ -197,20 +216,34 @@ input[type='email']:focus, color: var(--primary-color); &:hover, &:focus { - background: var(--primary-light); + background: var(--primary-color); color: #fff; box-shadow: var(--focus-ring); + transform: translateY(-1px); } } &.p-button-danger { - background-color: #e53935; + background-color: #ef4444; border: none; color: #fff; &:hover, &:focus { - background-color: #b71c1c; + background-color: #dc2626; + box-shadow: var(--focus-ring-error); + transform: translateY(-1px); + } + } + + &.p-button-success { + background-color: var(--accent-color); + border: none; + color: #fff; + &:hover, + &:focus { + background-color: var(--accent-light); box-shadow: var(--focus-ring); + transform: translateY(-1px); } } @@ -219,25 +252,31 @@ input[type='email']:focus, color: var(--primary-color); &:hover, &:focus { - background: var(--primary-light); - color: #fff; + background: var(--primary-lighter); + color: var(--primary-color); box-shadow: var(--focus-ring); } } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; + } } - // Tables .p-datatable { margin-top: var(--spacing-lg); background-color: var(--card-background); border-radius: var(--border-radius); overflow: hidden; box-shadow: var(--card-shadow); - transition: background-color 0.3s, box-shadow 0.3s; + transition: background-color var(--transition-normal), box-shadow var(--transition-normal); + border: 1px solid var(--table-border-color); .p-datatable-header { background-color: var(--card-background); - padding: var(--spacing-md); + padding: 0; border-bottom: 1px solid var(--table-border-color); } @@ -247,14 +286,14 @@ input[type='email']:focus, border-color: var(--table-border-color); padding: var(--spacing-md); font-weight: 700; - font-size: 1rem; - letter-spacing: 0.1px; + font-size: 0.875rem; + letter-spacing: 0.025em; } .p-datatable-tbody > tr { background-color: var(--table-row-bg); color: var(--text-color); - transition: background-color 0.2s; + transition: background-color var(--transition-fast); &:hover { background-color: var(--table-row-hover-bg); @@ -265,6 +304,7 @@ input[type='email']:focus, color: var(--text-color); border-color: var(--table-border-color); padding: var(--spacing-md); + font-size: 0.875rem; } } @@ -274,37 +314,178 @@ input[type='email']:focus, border-color: var(--table-border-color); padding: var(--spacing-md); - .p-paginator-element.p-highlight { - background-color: #000000 !important; - color: #ffffff !important; - border-color: #000000 !important; + .p-paginator-element { + color: var(--text-color) !important; + + &.p-highlight { + background-color: var(--primary-color) !important; + color: #ffffff !important; + border-color: var(--primary-color) !important; + font-weight: 600; + } + + &:hover:not(.p-highlight) { + background-color: var(--paginator-button-hover-bg); + color: var(--text-color) !important; + } } - .p-paginator-element:hover { - background-color: var(--paginator-button-hover-bg); + .p-paginator-pages { + .p-paginator-page { + color: var(--text-color) !important; + + &.p-highlight { + background-color: var(--primary-color) !important; + color: #ffffff !important; + border-color: var(--primary-color) !important; + font-weight: 600; + } + + &:hover:not(.p-highlight) { + background-color: var(--paginator-button-hover-bg); + color: var(--text-color) !important; + } + } } + + .p-paginator-first, + .p-paginator-prev, + .p-paginator-next, + .p-paginator-last { + color: var(--text-color) !important; + + &:hover { + background-color: var(--paginator-button-hover-bg); + color: var(--text-color) !important; + } + } + } + } + + .p-card { + border-radius: var(--border-radius); + box-shadow: var(--card-shadow); + background: var(--card-background); + border: 1px solid var(--table-border-color); + transition: box-shadow var(--transition-normal); + + &:hover { + box-shadow: var(--card-shadow-hover); + } + + .p-card-header { + background: linear-gradient(135deg, var(--primary-color), var(--primary-light)); + color: white; + padding: var(--spacing-lg); + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + + .p-card-body { + padding: var(--spacing-xl); + } + } + + .p-toast { + .p-toast-message { + border-radius: var(--border-radius-sm); + box-shadow: var(--card-shadow); + border: none; + } + + .p-toast-message-success { + background: var(--accent-color); + color: white; + } + + .p-toast-message-error { + background: #ef4444; + color: white; + } + + .p-toast-message-info { + background: var(--primary-color); + color: white; + } + + .p-toast-message-warn { + background: #f59e0b; + color: white; + } + } + + .p-confirmdialog { + .p-dialog-header { + background: var(--card-background); + color: var(--text-color); + } + + .p-dialog-content { + background: var(--card-background); + color: var(--text-color); + } + + .p-dialog-footer { + background: var(--card-background); + } + } + + .p-tag { + border-radius: var(--border-radius-sm); + font-size: 0.75rem; + font-weight: 600; + padding: var(--spacing-xs) var(--spacing-sm); + color: var(--text-color) !important; + background-color: var(--primary-lighter) !important; + border: 1px solid var(--primary-color) !important; + + &.p-tag-info { + background-color: var(--primary-lighter) !important; + color: var(--primary-color) !important; + border-color: var(--primary-color) !important; + } + + &.p-tag-success { + background-color: rgba(16, 185, 129, 0.1) !important; + color: var(--accent-color) !important; + border-color: var(--accent-color) !important; + } + + &.p-tag-warning { + background-color: rgba(245, 158, 11, 0.1) !important; + color: #f59e0b !important; + border-color: #f59e0b !important; + } + + &.p-tag-danger { + background-color: rgba(239, 68, 68, 0.1) !important; + color: #ef4444 !important; + border-color: #ef4444 !important; } } } -// Apply these styles globally body { @extend .p-component-styles; } ::-webkit-scrollbar { - width: 10px; + width: 8px; background: var(--background-color); } + ::-webkit-scrollbar-thumb { background: var(--table-border-color); - border-radius: 8px; + border-radius: var(--border-radius-sm); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); } @media (max-width: 768px) { .card-container { - padding: 1.5rem; - margin: 0 1rem; + padding: var(--spacing-lg); + margin: 0 var(--spacing-sm); } .form-actions { @@ -314,4 +495,19 @@ body { width: 100%; } } + + .page-title { + font-size: 1.5rem; + } +} + +@media (max-width: 480px) { + :root { + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; + } + + .card-container { + padding: var(--spacing-md); + } }