@@ -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
+
+
+
`,
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: `
-
-