diff --git a/.prettierrc b/.prettierrc
index 544138b..c25aec9 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,3 +1,7 @@
{
- "singleQuote": true
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "useTabs": true,
+ "tabWidth": 2,
+ "endOfLine": "crlf"
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..253e8ee
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,38 @@
+{
+ "files.eol": "\n",
+ "editor.tabSize": 2,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit",
+ "source.organizeImports": "explicit",
+ "source.sortMembers": "explicit"
+ },
+ "editor.formatOnSave": true,
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[javascriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "eslint.options": {
+ "extensions": [".js", ".ts", ".html"]
+ },
+ "eslint.validate": ["javascript", "typescript", "html"],
+ "angular.enable-strict-mode-prompt": false,
+ "search.exclude": {
+ "": true,
+ "**/node_modules": true,
+ ".angular": true,
+ ".nx": true
+ },
+ "search.useIgnoreFiles": false,
+ "nxConsole.generateAiAgentRules": true
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 60bd6d9..da08a15 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,52 +1,71 @@
-import nx from '@nx/eslint-plugin';
+import { FlatCompat } from '@eslint/eslintrc';
+import js from '@eslint/js';
+import nxEslintPlugin from '@nx/eslint-plugin';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const compat = new FlatCompat({
+ baseDirectory: dirname(fileURLToPath(import.meta.url)),
+ recommendedConfig: js.configs.recommended,
+});
export default [
- ...nx.configs['flat/base'],
- ...nx.configs['flat/typescript'],
- ...nx.configs['flat/javascript'],
{
ignores: ['**/dist'],
},
+ { plugins: { '@nx': nxEslintPlugin } },
{
- files: [
- '**/*.ts',
- '**/*.tsx',
- '**/*.cts',
- '**/*.mts',
- '**/*.js',
- '**/*.jsx',
- '**/*.cjs',
- '**/*.mjs',
- ],
- // Override or add rules here
- rules: {},
- },
- ...nx.configs['flat/angular'],
- ...nx.configs['flat/angular-template'],
- {
- files: ['**/*.ts'],
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
- '@angular-eslint/directive-selector': [
+ '@angular-eslint/no-input-rename': 'off',
+ '@nx/enforce-module-boundaries': [
'error',
{
- type: 'attribute',
- prefix: 'app',
- style: 'camelCase',
- },
- ],
- '@angular-eslint/component-selector': [
- 'error',
- {
- type: 'element',
- prefix: 'app',
- style: 'kebab-case',
+ enforceBuildableLibDependency: true,
+ allow: [],
+ depConstraints: [
+ {
+ sourceTag: '*',
+ onlyDependOnLibsWithTags: ['*'],
+ },
+ ],
},
],
},
},
- {
- files: ['**/*.html'],
- // Override or add rules here
- rules: {},
- },
+ ...compat
+ .config({
+ extends: ['plugin:@nx/typescript'],
+ })
+ .map((config) => ({
+ ...config,
+ files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
+ rules: {
+ ...config.rules,
+ },
+ })),
+ ...compat
+ .config({
+ extends: ['plugin:@nx/javascript'],
+ })
+ .map((config) => ({
+ ...config,
+ files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
+ rules: {
+ ...config.rules,
+ },
+ })),
+ ...compat
+ .config({
+ env: {
+ jest: true,
+ },
+ })
+ .map((config) => ({
+ ...config,
+ files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'],
+ rules: {
+ ...config.rules,
+ },
+ })),
];
diff --git a/package-lock.json b/package-lock.json
index 5d93783..c862f51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@angular-devkit/build-angular": "~20.0.0",
+ "@angular/animations": "^20.0.3",
"@angular/common": "~20.0.0",
"@angular/compiler": "~20.0.0",
"@angular/core": "~20.0.0",
@@ -18,8 +19,13 @@
"@angular/platform-server": "~20.0.0",
"@angular/router": "~20.0.0",
"@angular/ssr": "~20.0.0",
+ "@mmstack/primitives": "^20.0.0",
+ "@primeng/themes": "^19.1.3",
"express": "^4.21.2",
+ "primeicons": "^7.0.0",
+ "primeng": "^19.1.3",
"rxjs": "~7.8.0",
+ "uuid": "^11.1.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
@@ -1908,6 +1914,22 @@
"typescript": "*"
}
},
+ "node_modules/@angular/animations": {
+ "version": "20.0.3",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.0.3.tgz",
+ "integrity": "sha512-R6yv2RmrH49nW1ybgoOMw5pWzqaRYo8Kn3VtvrUDBty4TXjwc0addaw/t89n0smO3/lmBB4vnlRScePAEQZ/3w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "20.0.3",
+ "@angular/core": "20.0.3"
+ }
+ },
"node_modules/@angular/build": {
"version": "20.0.2",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.0.2.tgz",
@@ -6143,6 +6165,29 @@
"win32"
]
},
+ "node_modules/@mmstack/object": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@mmstack/object/-/object-20.0.0.tgz",
+ "integrity": "sha512-cLhbkhU+S6jNm+7RNJ8DxvprBtX45pE5ymw58Pj3PoB+UOCA7pZPBDiubQQWqKBpoQJO0X/LyT/uTDV1YKs1Jg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ }
+ },
+ "node_modules/@mmstack/primitives": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@mmstack/primitives/-/primitives-20.0.0.tgz",
+ "integrity": "sha512-V3cetKpVP4NtGdAr0GiovLkeNtuY6Scksne3jaKIz5/C6cIhhfh7BcdGx6PRsI5slc8xCgCWoDBVDJ3/u9tKZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@mmstack/object": "^20.0.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "~20.0.3",
+ "@angular/core": "~20.0.3"
+ }
+ },
"node_modules/@modern-js/node-bundle-require": {
"version": "2.67.6",
"resolved": "https://registry.npmjs.org/@modern-js/node-bundle-require/-/node-bundle-require-2.67.6.tgz",
@@ -8574,6 +8619,36 @@
"node": ">=14"
}
},
+ "node_modules/@primeng/themes": {
+ "version": "19.1.3",
+ "resolved": "https://registry.npmjs.org/@primeng/themes/-/themes-19.1.3.tgz",
+ "integrity": "sha512-y4VryHHUTPWlmfR56NBANC0QPIxEngTUE/J3pGs4SJquq1n5EE/U16dxa1qW/wXqLF3jn3l/AO/4KZqGj5UuAA==",
+ "license": "SEE LICENSE IN LICENSE.md",
+ "dependencies": {
+ "@primeuix/styled": "^0.3.2"
+ }
+ },
+ "node_modules/@primeuix/styled": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
+ "integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/utils": "^0.3.2"
+ },
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
+ "node_modules/@primeuix/utils": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
+ "integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@@ -18446,6 +18521,33 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/primeicons": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
+ "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
+ "license": "MIT"
+ },
+ "node_modules/primeng": {
+ "version": "19.1.3",
+ "resolved": "https://registry.npmjs.org/primeng/-/primeng-19.1.3.tgz",
+ "integrity": "sha512-KsrvJFblKg28kA6d4npnnABjKClmJv0CgDT/kOxFq5onQNBy4547DJzRGMba4+CMLKjHWWkYWuC+XSkPMNFrZg==",
+ "license": "SEE LICENSE IN LICENSE.md",
+ "dependencies": {
+ "@primeuix/styled": "^0.3.2",
+ "@primeuix/utils": "^0.3.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/animations": "^19.0.0",
+ "@angular/cdk": "^19.0.0",
+ "@angular/common": "^19.0.0",
+ "@angular/core": "^19.0.0",
+ "@angular/forms": "^19.0.0",
+ "@angular/platform-browser": "^19.0.0",
+ "@angular/router": "^19.0.0",
+ "rxjs": "^6.0.0 || ^7.8.1"
+ }
+ },
"node_modules/proc-log": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
@@ -19913,6 +20015,15 @@
"websocket-driver": "^0.7.4"
}
},
+ "node_modules/sockjs/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/socks": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
@@ -21146,12 +21257,16 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist/esm/bin/uuid"
}
},
"node_modules/validate-npm-package-license": {
diff --git a/package.json b/package.json
index 32e9ca5..402fd39 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"private": true,
"dependencies": {
"@angular-devkit/build-angular": "~20.0.0",
+ "@angular/animations": "^20.0.3",
"@angular/common": "~20.0.0",
"@angular/compiler": "~20.0.0",
"@angular/core": "~20.0.0",
@@ -18,8 +19,13 @@
"@angular/platform-server": "~20.0.0",
"@angular/router": "~20.0.0",
"@angular/ssr": "~20.0.0",
+ "@mmstack/primitives": "^20.0.0",
+ "@primeng/themes": "^19.1.3",
"express": "^4.21.2",
+ "primeicons": "^7.0.0",
+ "primeng": "^19.1.3",
"rxjs": "~7.8.0",
+ "uuid": "^11.1.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
diff --git a/public/placeholder-user.jpg b/public/placeholder-user.jpg
new file mode 100644
index 0000000..6fa7543
Binary files /dev/null and b/public/placeholder-user.jpg differ
diff --git a/src/app/add-student.component.ts b/src/app/add-student.component.ts
new file mode 100644
index 0000000..1077831
--- /dev/null
+++ b/src/app/add-student.component.ts
@@ -0,0 +1,127 @@
+import { CommonModule } from '@angular/common';
+import { Component, computed, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { ButtonModule } from 'primeng/button';
+import { InputTextModule } from 'primeng/inputtext';
+import { MultiSelectModule } from 'primeng/multiselect';
+import { StudentService } from './student.service';
+
+@Component({
+ selector: 'app-add-student',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ InputTextModule,
+ MultiSelectModule,
+ ButtonModule,
+ ],
+ template: `
+
+ `,
+ styles: [
+ `
+ :host ::ng-deep {
+ .p-inputtext:enabled:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 1px var(--primary-light);
+ }
+
+ .p-multiselect:not(.p-disabled).p-focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 1px var(--primary-light);
+ }
+ }
+ `,
+ ],
+})
+export class AddStudentComponent {
+ private readonly studentService = inject(StudentService);
+ private readonly router = inject(Router);
+
+ protected name = '';
+ protected email = '';
+ protected selectedCourses: string[] = [];
+
+ protected courseOptions = computed(() => {
+ return [
+ 'Mathematics',
+ 'Physics',
+ 'Chemistry',
+ 'Biology',
+ 'Computer Science',
+ 'History',
+ 'Literature',
+ 'Physical Education',
+ ].map((course) => ({ label: course, value: course }));
+ });
+
+ submit() {
+ this.studentService.addStudent({
+ name: this.name,
+ email: this.email,
+ courses: this.selectedCourses,
+ });
+ this.router.navigate(['/students']);
+ }
+
+ cancel() {
+ this.router.navigate(['/students']);
+ }
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
new file mode 100644
index 0000000..db34f9a
--- /dev/null
+++ b/src/app/app.component.ts
@@ -0,0 +1,196 @@
+import { isPlatformBrowser } from '@angular/common';
+import {
+ Component,
+ computed,
+ inject,
+ OnInit,
+ PLATFORM_ID,
+} from '@angular/core';
+import { RouterLink, RouterOutlet } from '@angular/router';
+import { stored } from '@mmstack/primitives';
+import { ButtonModule } from 'primeng/button';
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [RouterOutlet, RouterLink, ButtonModule],
+ template: `
+
+
+
+
+
+
+ `,
+ styles: [
+ `
+ .navbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: var(--primary-color);
+ padding: 0 1.5rem;
+ height: 4rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+ }
+
+ .navbar-left .app-title {
+ color: #fff;
+ font-size: 1.4rem;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ }
+
+ .navbar-center a {
+ color: rgba(255, 255, 255, 0.8);
+ margin: 0 1rem;
+ text-decoration: none;
+ font-weight: 500;
+ padding: 0.5rem 0;
+ position: relative;
+ transition: color 0.2s ease;
+ }
+
+ .navbar-center a.active,
+ .navbar-center a:hover {
+ color: #fff;
+ }
+
+ .navbar-center a.active::after,
+ .navbar-center a:hover::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background-color: #fff;
+ }
+
+ .navbar-right {
+ display: flex;
+ align-items: center;
+ }
+
+ .avatar {
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 50%;
+ margin-right: 0.75rem;
+ object-fit: cover;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .username {
+ color: #fff;
+ font-weight: 500;
+ }
+
+ .theme-toggle-btn {
+ background: transparent;
+ color: #fff;
+ border-radius: 50%;
+ width: 2.5rem;
+ height: 2.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ margin-left: 1rem;
+ }
+
+ .theme-toggle-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+ }
+
+ .main-content {
+ min-height: calc(100vh - 4rem);
+ padding: 2rem 0;
+ }
+
+ .container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 1.5rem;
+ }
+
+ @media (max-width: 768px) {
+ .navbar {
+ flex-direction: column;
+ height: auto;
+ padding: 1rem;
+ }
+
+ .navbar-center {
+ margin: 1rem 0;
+ }
+
+ .navbar-right {
+ width: 100%;
+ justify-content: flex-end;
+ }
+
+ .main-content {
+ padding: 1rem 0;
+ }
+ }
+ `,
+ ],
+})
+export class App implements OnInit {
+ private readonly theme = stored('light', {
+ key: 'theme',
+ });
+ private readonly platformId = inject(PLATFORM_ID);
+ private readonly isBrowser = isPlatformBrowser(this.platformId);
+
+ isDarkMode = computed(() => this.theme() === 'dark');
+
+ ngOnInit() {
+ if (this.isBrowser) {
+ this.applyTheme();
+ }
+ }
+
+ toggleTheme() {
+ this.theme.set(this.isDarkMode() ? 'light' : 'dark');
+ if (this.isBrowser) {
+ this.applyTheme();
+ }
+ }
+
+ applyTheme() {
+ if (!this.isBrowser) return;
+
+ if (this.isDarkMode()) {
+ document.body.classList.add('dark-theme');
+ } else {
+ document.body.classList.remove('dark-theme');
+ }
+ }
+}
+
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 699144b..97ed14c 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,20 +1,30 @@
import {
- ApplicationConfig,
- provideBrowserGlobalErrorListeners,
- provideZoneChangeDetection,
+ ApplicationConfig,
+ provideBrowserGlobalErrorListeners,
+ provideZoneChangeDetection,
} from '@angular/core';
-import { provideRouter } from '@angular/router';
-import { appRoutes } from './app.routes';
import {
- provideClientHydration,
- withEventReplay,
+ provideClientHydration,
+ withEventReplay,
} from '@angular/platform-browser';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import { provideRouter } from '@angular/router';
+import Aura from '@primeng/themes/aura';
+import { providePrimeNG } from 'primeng/config';
+import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
- providers: [
- provideClientHydration(withEventReplay()),
- provideBrowserGlobalErrorListeners(),
- provideZoneChangeDetection({ eventCoalescing: true }),
- provideRouter(appRoutes),
- ],
+ providers: [
+ provideClientHydration(withEventReplay()),
+ provideBrowserGlobalErrorListeners(),
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ provideAnimationsAsync(),
+ providePrimeNG({
+ theme: {
+ preset: Aura,
+ },
+ }),
+ ],
};
+
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 8762dfe..6398996 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,3 +1,20 @@
-import { Route } from '@angular/router';
+import { Routes } from '@angular/router';
+export const routes: Routes = [
+ { path: '', redirectTo: '/students', pathMatch: 'full' },
+ {
+ path: 'students',
+ loadComponent: () =>
+ import('./overview.component').then((m) => m.OverviewComponent),
+ },
+ {
+ path: 'students/add',
+ loadComponent: () =>
+ import('./add-student.component').then((m) => m.AddStudentComponent),
+ },
+ {
+ path: 'students/edit',
+ loadComponent: () =>
+ import('./edit-student.component').then((m) => m.EditStudentComponent),
+ },
+];
-export const appRoutes: Route[] = [];
diff --git a/src/app/app.ts b/src/app/app.ts
deleted file mode 100644
index 8e890bd..0000000
--- a/src/app/app.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Component } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { NxWelcome } from './nx-welcome';
-
-@Component({
- imports: [NxWelcome, RouterModule],
- selector: 'app-root',
- templateUrl: './app.html',
- styleUrl: './app.scss',
-})
-export class App {
- protected title = 'student-app';
-}
diff --git a/src/app/edit-student.component.ts b/src/app/edit-student.component.ts
new file mode 100644
index 0000000..74abef6
--- /dev/null
+++ b/src/app/edit-student.component.ts
@@ -0,0 +1,159 @@
+import { CommonModule } from '@angular/common';
+import { Component, computed, inject, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { ButtonModule } from 'primeng/button';
+import { InputTextModule } from 'primeng/inputtext';
+import { MultiSelectModule } from 'primeng/multiselect';
+import { StudentService } from './student.service';
+
+@Component({
+ selector: 'app-edit-student',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ InputTextModule,
+ MultiSelectModule,
+ ButtonModule,
+ ],
+ template: `
+
+
Edit Student
+
+
+
+
+
+
Student not found. Please return to the overview page.
+
+
+
+
+ `,
+ styles: [
+ `
+ :host ::ng-deep {
+ .p-inputtext:disabled {
+ opacity: 0.8;
+ background-color: #f5f5f5;
+ }
+
+ .p-multiselect:not(.p-disabled).p-focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 1px var(--primary-light);
+ }
+ }
+
+ .not-found-message {
+ text-align: center;
+ padding: 2rem;
+ color: #d32f2f;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ .not-found-message p {
+ font-size: 1.2rem;
+ margin: 1rem 0;
+ }
+ `,
+ ],
+})
+export class EditStudentComponent implements OnInit {
+ private readonly studentService = inject(StudentService);
+ private readonly route = inject(ActivatedRoute);
+ private readonly router = inject(Router);
+
+ protected student = computed(() => {
+ const id = this.route.snapshot.queryParams['id'];
+ return this.studentService.getStudentById(id);
+ });
+
+ protected courseOptions = computed(() => {
+ return [
+ 'Mathematics',
+ 'Physics',
+ 'Chemistry',
+ 'Biology',
+ 'Computer Science',
+ 'History',
+ 'Literature',
+ 'Physical Education',
+ ].map((course) => ({ label: course, value: course }));
+ });
+
+ ngOnInit() {
+ if (!this.student()) {
+ console.error('Student not found');
+ }
+ }
+
+ save() {
+ this.studentService.updateStudent(this.student()!);
+ this.router.navigate(['/students']);
+ }
+
+ cancel() {
+ this.router.navigate(['/students']);
+ }
+}
diff --git a/src/app/mock.ts b/src/app/mock.ts
new file mode 100644
index 0000000..fde9bf6
--- /dev/null
+++ b/src/app/mock.ts
@@ -0,0 +1,29 @@
+import { v4 as uuid } from 'uuid';
+import { Student } from './student.service';
+
+const courses = [
+ 'Mathematics',
+ 'Physics',
+ 'Chemistry',
+ 'History',
+ 'Literature',
+];
+
+function generateRandom() {
+ const mockData: Student[] = [];
+
+ for (let i = 0; i < 100; i++) {
+ const student: Student = {
+ id: uuid(),
+ name: `Student ${i}`,
+ email: `student${i}@gmail.com`,
+ courses: courses.slice(0, Math.floor(Math.random() * courses.length) + 1),
+ };
+ mockData.push(student);
+ }
+
+ return mockData;
+}
+
+export default generateRandom;
+
diff --git a/src/app/nx-welcome.ts b/src/app/nx-welcome.ts
deleted file mode 100644
index 1b52d8a..0000000
--- a/src/app/nx-welcome.ts
+++ /dev/null
@@ -1,872 +0,0 @@
-import { Component, ViewEncapsulation } from '@angular/core';
-import { CommonModule } from '@angular/common';
-
-@Component({
- selector: 'app-nx-welcome',
- imports: [CommonModule],
- template: `
-
-
-
-
-
-
-
- Hello there,
- Welcome student-app 👋
-
-
-
-
-
-
-
-
-
Next steps
-
Here are some things you can do with Nx:
-
-
-
- Build, test and lint your app
-
- # Build
-nx build
-# Test
-nx test
-# Lint
-nx lint
-# Run them together!
-nx run-many -t build test lint
-
-
-
-
- View project details
-
- nx show project student-app
-
-
-
-
-
- View interactive project graph
-
- nx graph
-
-
-
-
-
- Add UI library
-
- # Generate UI lib
-nx g @nx/angular:lib ui
-# Add a component
-nx g @nx/angular:component ui/src/lib/button
-
-
-
- Carefully crafted with
-
-
-
-
- `,
- styles: [],
- encapsulation: ViewEncapsulation.None,
-})
-export class NxWelcome {}
diff --git a/src/app/overview.component.ts b/src/app/overview.component.ts
new file mode 100644
index 0000000..dcd7b05
--- /dev/null
+++ b/src/app/overview.component.ts
@@ -0,0 +1,242 @@
+import { CommonModule } from '@angular/common';
+import { Component, computed, inject } from '@angular/core';
+import { Router } from '@angular/router';
+import { ButtonModule } from 'primeng/button';
+import { TableModule } from 'primeng/table';
+import { StudentService } from './student.service';
+
+@Component({
+ selector: 'app-overview',
+ standalone: true,
+ imports: [CommonModule, TableModule, ButtonModule],
+ template: `
+
+
+
+
+
+
+ | Name |
+ Email |
+ Courses |
+ Actions |
+
+
+
+
+ | Name{{ st.name }} |
+ Email{{ st.email }} |
+
+ Courses{{ st.courses.join(', ') }}
+ |
+
+ Actions
+
+
+
+
+ |
+
+
+
+
+ |
+ No students found. Add your first student!
+ |
+
+
+
+
+ `,
+ styles: [
+ `
+ .header-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+ }
+
+ .action-buttons-header {
+ display: flex;
+ gap: 1rem;
+ }
+
+ .page-title {
+ margin-bottom: 0;
+ }
+
+ .action-buttons {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+ }
+
+ :host ::ng-deep {
+ .p-datatable {
+ border-radius: 8px;
+ overflow: hidden;
+
+ .p-datatable-header {
+ background-color: var(--card-background) !important;
+ border: none;
+ padding: 1.25rem 1.5rem;
+ }
+
+ .p-datatable-thead > tr > th {
+ background-color: var(--table-header-bg) !important;
+ color: var(--text-color) !important;
+ padding: 1rem 1.5rem;
+ font-weight: 600;
+ border-width: 0 0 1px 0;
+ border-color: var(--table-border-color);
+ }
+
+ .p-datatable-tbody > tr {
+ background-color: var(--table-row-bg) !important;
+ color: var(--text-color) !important;
+
+ &:hover {
+ background-color: var(--table-row-hover-bg) !important;
+ }
+
+ > td {
+ background-color: transparent !important;
+ color: var(--text-color) !important;
+ padding: 1rem 1.5rem;
+ border-width: 0 0 1px 0;
+ border-color: var(--table-border-color);
+ }
+
+ &:last-child > td {
+ border-bottom: none;
+ }
+ }
+
+ .p-paginator {
+ border-width: 0;
+ padding: 1.25rem;
+ background-color: var(--card-background) !important;
+
+ .p-paginator-page {
+ &:hover:not(.p-highlight) {
+ background-color: var(--paginator-button-hover-bg) !important;
+ }
+
+ &.p-highlight {
+ background-color: var(
+ --paginator-button-selected-bg
+ ) !important;
+ color: var(--paginator-button-selected-text) !important;
+ border-color: var(--paginator-button-selected-bg) !important;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .p-column-title {
+ display: none;
+ }
+
+ .p-datatable-emptymessage td {
+ color: var(--text-color) !important;
+ background-color: var(--table-row-bg) !important;
+ }
+ }
+ }
+
+ @media screen and (max-width: 768px) {
+ .header-actions {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 1rem;
+ }
+
+ :host ::ng-deep {
+ .p-datatable {
+ .p-datatable-tbody > tr > td {
+ padding: 1rem;
+ background-color: transparent !important;
+
+ .p-column-title {
+ display: inline-block;
+ font-weight: bold;
+ margin-right: 0.5rem;
+ color: var(--text-color);
+ }
+ }
+ }
+ }
+ }
+ `,
+ ],
+})
+export class OverviewComponent {
+ private readonly router = inject(Router);
+ private readonly studentsService = inject(StudentService);
+
+ protected readonly students = computed(() => this.studentsService.students());
+ protected generateMock = () => this.studentsService.generateMockData();
+
+ goToAdd() {
+ this.router.navigate(['/students/add']);
+ }
+
+ editStudent(id: number) {
+ this.router.navigate([`/students/edit`], {
+ queryParams: {
+ id,
+ },
+ });
+ }
+
+ deleteStudent(id: string) {
+ this.studentsService.deleteSutdent(id);
+ }
+}
+
diff --git a/src/app/student.service.ts b/src/app/student.service.ts
new file mode 100644
index 0000000..65ff46b
--- /dev/null
+++ b/src/app/student.service.ts
@@ -0,0 +1,59 @@
+import { Injectable } from '@angular/core';
+import { stored } from '@mmstack/primitives';
+import { v4 as uuid } from 'uuid';
+import generateRandom from './mock';
+
+export type Student = {
+ id: string;
+ name: string;
+ email: string;
+ courses: string[];
+};
+
+@Injectable({
+ providedIn: 'root',
+})
+export class StudentService {
+ students = stored([], {
+ key: 'students',
+ });
+
+ courses = ['Mathematics', 'Physics', 'Chemistry', 'History', 'Literature'];
+
+ addStudent({ name, email, courses }: Omit) {
+ const newStudent: Student = {
+ id: uuid(),
+ name,
+ email,
+ courses,
+ };
+
+ this.students.update((students) => [...students, newStudent]);
+ }
+
+ getStudentById(id: string) {
+ return this.students().find((student) => student.id === id);
+ }
+
+ updateStudent(student: Student) {
+ this.students.update((students) =>
+ students.map((s) => (s.id === student.id ? student : s))
+ );
+ }
+
+ generateMockData() {
+ const mockData: Student[] = generateRandom();
+ this.students.set(mockData);
+ }
+
+ updateStudentCourse(id: string, courses: string[]) {
+ this.students.update((students) =>
+ students.map((s) => (s.id === id ? { ...s, courses } : s))
+ );
+ }
+
+ deleteSutdent(id: string) {
+ this.students.update((students) => students.filter((s) => s.id !== id));
+ }
+}
+
diff --git a/src/main.server.ts b/src/main.server.ts
index 154ce1c..49e8e09 100644
--- a/src/main.server.ts
+++ b/src/main.server.ts
@@ -1,7 +1,8 @@
import { bootstrapApplication } from '@angular/platform-browser';
-import { App } from './app/app';
+import { App } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(App, config);
export default bootstrap;
+
diff --git a/src/main.ts b/src/main.ts
index 190f341..1650c64 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
+import { App } from './app/app.component';
import { appConfig } from './app/app.config';
-import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
+
diff --git a/src/styles.scss b/src/styles.scss
index 90d4ee0..9dba783 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1 +1,172 @@
-/* You can add global styles to this file, and also import other style files */
+@import 'primeicons/primeicons.css';
+
+html,
+body {
+ margin: 0;
+ font-family: Roboto, 'Helvetica Neue', sans-serif;
+ background-color: var(--background-color);
+ color: var(--text-color);
+ transition: background-color 0.3s, color 0.3s;
+}
+
+:root {
+ --primary-color: #6200ee;
+ --primary-light: #9951ff;
+ --text-color: #333333;
+ --background-color: #ffffff;
+ --card-background: #ffffff;
+ --table-header-bg: #f8f9fa;
+ --table-row-bg: #ffffff;
+ --table-row-hover-bg: #f5f5f5;
+ --table-border-color: #e9ecef;
+ --paginator-button-selected-bg: var(--primary-color);
+ --paginator-button-selected-text: #ffffff;
+ --paginator-button-hover-bg: rgba(
+ 98,
+ 0,
+ 238,
+ 0.1
+ ); /* Light purple for hover state */
+}
+
+.dark-theme {
+ --primary-color: #bb86fc;
+ --primary-light: #e2b9ff;
+ --text-color: #e0e0e0;
+ --background-color: #121212;
+ --card-background: #1e1e1e;
+ --table-header-bg: #2a2a2a;
+ --table-row-bg: #1e1e1e;
+ --table-row-hover-bg: #333333;
+ --table-border-color: #444444;
+ --paginator-button-selected-bg: var(--primary-color);
+ --paginator-button-selected-text: #ffffff;
+ --paginator-button-hover-bg: rgba(
+ 187,
+ 134,
+ 252,
+ 0.2
+ ); /* Light purple for hover state in dark mode */
+}
+
+.page-title {
+ margin-bottom: 1.5rem;
+ color: var(--primary-color);
+ font-weight: 600;
+}
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 1.5rem;
+}
+
+.form-field label {
+ font-weight: 500;
+ margin-bottom: 0.5rem;
+ color: var(--text-color);
+}
+
+.form-actions {
+ margin-top: 2rem;
+ display: flex;
+ justify-content: flex-end;
+ gap: 1rem;
+}
+
+:host ::ng-deep,
+body {
+ .p-component {
+ font-family: var(--font-family);
+ }
+
+ .p-button {
+ &.p-button-primary {
+ background-color: var(--primary-color);
+ &:hover {
+ background-color: var(--primary-light);
+ }
+ }
+ }
+
+ .p-inputtext,
+ .p-multiselect {
+ width: 100%;
+ }
+
+ .p-datatable {
+ margin-top: 1.5rem;
+ background-color: var(--card-background) !important;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ .p-datatable-header {
+ background-color: var(--card-background) !important;
+ padding: 1rem;
+ border-bottom: 1px solid var(--table-border-color);
+ }
+
+ .p-datatable-thead > tr > th {
+ background-color: var(--table-header-bg) !important;
+ color: var(--text-color) !important;
+ border-color: var(--table-border-color);
+ padding: 1rem;
+ font-weight: 600;
+ }
+
+ .p-datatable-tbody > tr {
+ background-color: var(--table-row-bg) !important;
+ color: var(--text-color) !important;
+ transition: background-color 0.2s;
+
+ &:hover {
+ background-color: var(--table-row-hover-bg) !important;
+ }
+
+ > td {
+ background-color: transparent !important;
+ color: var(--text-color) !important;
+ border-color: var(--table-border-color);
+ padding: 1rem;
+ }
+ }
+
+ .p-paginator {
+ background-color: var(--card-background) !important;
+ color: var(--text-color) !important;
+ border-color: var(--table-border-color);
+ padding: 1rem;
+
+ .p-paginator-element.p-highlight {
+ background-color: var(--paginator-button-selected-bg) !important;
+ color: var(--paginator-button-selected-text) !important;
+ }
+
+ .p-paginator-element:hover {
+ background-color: var(--paginator-button-hover-bg) !important;
+ }
+ }
+
+ .p-datatable-emptymessage td {
+ background-color: var(--table-row-bg) !important;
+ color: var(--text-color) !important;
+ }
+ }
+}
+
+@media (max-width: 768px) {
+ .card-container {
+ padding: 1.5rem;
+ margin: 0 1rem;
+ }
+
+ .form-actions {
+ flex-direction: column;
+
+ .p-button {
+ width: 100%;
+ }
+ }
+}
+