Commit 7c84e134 authored by Aravind RK's avatar Aravind RK

feat : CRUD for student

parent 09fd6813
.page-container { .page-container {
padding: 32px 40px; padding: 32px 40px;
max-width: 1400px; max-width: 1400px;
} }
.page-header { .page-header {
margin-bottom: 32px; margin-bottom: 32px;
} }
.page-header h1 { .page-header h1 {
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
margin: 0 0 8px; margin: 0 0 8px;
} }
.page-header p { .page-header p {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 15px; font-size: 15px;
margin: 0; margin: 0;
} }
.filter-tabs { .filter-tabs {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 24px; margin-bottom: 24px;
} }
.tab { .tab {
padding: 10px 20px; padding: 10px 20px;
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
background: transparent; background: transparent;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
font-family: inherit; font-family: inherit;
} }
.tab:hover { .tab:hover {
border-color: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.2);
color: var(--text-primary); color: var(--text-primary);
} }
.tab.active { .tab.active {
background: rgba(102, 126, 234, 0.15); background: rgba(102, 126, 234, 0.15);
border-color: rgba(102, 126, 234, 0.3); border-color: rgba(102, 126, 234, 0.3);
color: #667eea; color: #667eea;
} }
.loading-state { .loading-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 80px 0; padding: 80px 0;
gap: 16px; gap: 16px;
} }
.loader { .loader {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1); border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea; border-top-color: #667eea;
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.loading-state p { .loading-state p {
color: var(--text-secondary); color: var(--text-secondary);
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 80px 0; padding: 80px 0;
} }
.empty-icon { .empty-icon {
font-size: 48px; font-size: 48px;
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
.empty-state h3 { .empty-state h3 {
color: var(--text-primary); color: var(--text-primary);
font-size: 18px; font-size: 18px;
margin: 0 0 8px; margin: 0 0 8px;
} }
.empty-state p { .empty-state p {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
} }
.users-grid { .users-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 16px; gap: 16px;
} }
.user-card { .user-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 12px;
padding: 20px; padding: 16px 20px;
background: var(--bg-card);
background: var(--bg-card); border: 1px solid var(--border-color);
border: 1px solid var(--border-color); border-radius: 16px;
text-decoration: none;
border-radius: 16px; transition: all 0.25s;
text-decoration: none; cursor: pointer;
transition: all 0.25s; overflow: hidden;
cursor: pointer; min-width: 0;
} }
.user-card:hover { .user-card:hover {
background: var(--bg-hover); background: var(--bg-hover);
border-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px); transform: translateY(-2px);
} }
.user-card-avatar { .user-card-avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 14px; border-radius: 14px;
background: linear-gradient(135deg, #667eea, #764ba2); background: linear-gradient(135deg, #667eea, #764ba2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--text-primary); color: var(--text-primary);
font-weight: 700; font-weight: 700;
font-size: 20px; font-size: 20px;
flex-shrink: 0; flex-shrink: 0;
} }
.user-card-info { .user-card-info {
flex: 1; flex: 1;
} }
.name-row { .name-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 4px; margin-bottom: 4px;
} flex-wrap: nowrap;
overflow: hidden;
.user-card-info h4 { }
color: var(--text-primary);
font-size: 15px; .user-card-info h4 {
font-weight: 600; color: var(--text-primary);
margin: 0; font-size: 15px;
} font-weight: 600;
margin: 0;
.user-card-info p { white-space: nowrap;
color: var(--text-secondary); overflow: hidden;
font-size: 13px; text-overflow: ellipsis;
margin: 0 0 4px; }
}
.user-card-info p {
.user-joined { color: var(--text-secondary);
color: var(--text-muted); font-size: 13px;
font-size: 11px; margin: 0 0 4px;
} }
.status-dot { .user-joined {
width: 10px; color: var(--text-muted);
height: 10px; font-size: 11px;
border-radius: 50%; }
background: rgba(255, 255, 255, 0.2);
flex-shrink: 0; .status-dot {
} width: 10px;
height: 10px;
.status-dot.online { border-radius: 50%;
background: #4ade80; background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 8px rgba(74, 222, 128, 0.4); flex-shrink: 0;
} }
.view-arrow { .status-dot.online {
color: rgba(255, 255, 255, 0.2); background: #4ade80;
font-size: 20px; box-shadow: 0 0 8px rgba(74, 222, 128, 0.4);
transition: all 0.2s; }
}
.view-arrow {
.user-card:hover .view-arrow { color: rgba(255, 255, 255, 0.2);
color: var(--text-secondary); font-size: 20px;
transform: translateX(4px); transition: all 0.2s;
} }
.action-btn { .user-card:hover .view-arrow {
background: transparent; color: var(--text-secondary);
border: none; transform: translateX(4px);
border-radius: 8px; }
width: 32px;
height: 32px; .action-btn {
display: flex; background: transparent;
align-items: center; border: none;
justify-content: center; border-radius: 8px;
color: var(--text-muted); width: 30px;
cursor: pointer; height: 30px;
transition: all 0.2s; min-width: 30px;
margin-left: 8px; display: flex;
} align-items: center;
justify-content: center;
.action-btn .material-symbols-rounded { color: var(--text-muted);
font-size: 20px; cursor: pointer;
} transition: all 0.2s;
flex-shrink: 0;
.text-danger:hover { }
background: rgba(239, 68, 68, 0.1);
color: #ef4444; .action-btn .material-symbols-rounded {
} font-size: 20px;
}
.level-badge {
padding: 4px 12px; .text-danger:hover {
border-radius: 20px; background: rgba(239, 68, 68, 0.1);
font-size: 11px; color: #ef4444;
font-weight: 700; }
text-transform: uppercase;
letter-spacing: 0.5px; .level-badge {
flex-shrink: 0; padding: 3px 10px;
} border-radius: 20px;
font-size: 10px;
.level-badge[data-level="beginner"] { font-weight: 700;
background: rgba(148, 163, 184, 0.15); text-transform: uppercase;
color: #94a3b8; letter-spacing: 0.5px;
} flex-shrink: 0;
white-space: nowrap;
.level-badge[data-level="intermediate"] { }
background: rgba(59, 130, 246, 0.15);
color: #3b82f6; .level-badge[data-level="beginner"] {
} background: rgba(148, 163, 184, 0.15);
color: #94a3b8;
.level-badge[data-level="advanced"] { }
background: rgba(168, 85, 247, 0.15);
color: #a855f7; .level-badge[data-level="intermediate"] {
} background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
.level-badge[data-level="expert"] { }
background: rgba(245, 158, 11, 0.15);
color: #f59e0b; .level-badge[data-level="advanced"] {
} background: rgba(168, 85, 247, 0.15);
color: #a855f7;
@media (max-width: 768px) { }
.page-container {
padding: 24px 16px; .level-badge[data-level="expert"] {
} background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
.users-grid { }
grid-template-columns: 1fr;
} @media (max-width: 768px) {
} .page-container {
\ No newline at end of file padding: 24px 16px;
}
.users-grid {
grid-template-columns: 1fr;
}
}
/* ─── Student Modal Overlay ─────────────────────────────────── */
.student-modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
animation: fadeInOverlay 0.2s ease;
}
@keyframes fadeInOverlay {
from { opacity: 0; }
to { opacity: 1; }
}
.student-modal-container {
background: var(--bg-card, #fff);
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 20px;
width: 100%;
max-width: 480px;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.25);
animation: slideInModal 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
}
@keyframes slideInModal {
from { transform: scale(0.92) translateY(12px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
}
.student-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 28px 0;
}
.student-modal-header h2 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.student-modal-close {
background: transparent;
border: none;
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.2s;
}
.student-modal-close:hover {
background: var(--bg-hover, rgba(0,0,0,0.06));
color: var(--text-primary);
}
.student-modal-body {
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 16px;
}
.student-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 0 28px 24px;
}
.form-hint {
color: var(--text-secondary);
font-size: 12px;
margin-top: 4px;
display: block;
}
.text-primary:hover {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
}
\ No newline at end of file
<div class="page-container animate-fade-in"> <div class="page-container animate-fade-in">
<div class="page-header" style="display: flex; align-items: center; gap: 16px;"> <div class="page-header" style="display: flex; align-items: center; justify-content: space-between;">
<button class="icon-btn" (click)="goBack()" style="background: var(--bg-card); border: 1px solid var(--border-color); padding: 8px; border-radius: 12px; cursor: pointer; color: var(--text-primary); transition: all 0.2s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='var(--bg-card)'"> <div style="display: flex; align-items: center; gap: 16px;">
<span class="material-symbols-rounded">arrow_back</span> <button class="icon-btn" (click)="goBack()" style="background: var(--bg-card); border: 1px solid var(--border-color); padding: 8px; border-radius: 12px; cursor: pointer; color: var(--text-primary); transition: all 0.2s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='var(--bg-card)'">
</button> <span class="material-symbols-rounded">arrow_back</span>
<div> </button>
<h1 style="margin: 0; font-size: 24px;">Student Users</h1> <div>
<p style="margin: 4px 0 0;" class="page-subtitle">View and manage registered students</p> <h1 style="margin: 0; font-size: 24px;">Student Users</h1>
<p style="margin: 4px 0 0;" class="page-subtitle">View and manage registered students</p>
</div>
</div> </div>
<button class="btn btn-primary" (click)="openCreateModal()">
<span class="material-symbols-rounded">person_add</span> Create Student
</button>
</div> </div>
<div class="filter-tabs"> <div class="filter-tabs">
...@@ -43,7 +48,10 @@ ...@@ -43,7 +48,10 @@
<div class="status-dot" [class.online]="user.isLoggedIn" title="Online Status"></div> <div class="status-dot" [class.online]="user.isLoggedIn" title="Online Status"></div>
<button class="action-btn text-danger ml-auto" (click)="$event.preventDefault(); $event.stopPropagation(); deleteUser(user._id)" title="Delete Student"> <button class="action-btn text-primary ml-auto" style="margin-right: 8px;" (click)="$event.preventDefault(); $event.stopPropagation(); openEditModal(user)" title="Edit Student">
<span class="material-symbols-rounded">edit</span>
</button>
<button class="action-btn text-danger" (click)="$event.preventDefault(); $event.stopPropagation(); deleteUser(user._id)" title="Delete Student">
<span class="material-symbols-rounded">delete</span> <span class="material-symbols-rounded">delete</span>
</button> </button>
...@@ -52,4 +60,50 @@ ...@@ -52,4 +60,50 @@
} }
</div> </div>
} }
</div> </div>
<!-- ─── Create / Edit Student Modal ───────────────────────────── -->
@if (showUserModal()) {
<div class="student-modal-overlay" (click)="closeModal()">
<div class="student-modal-container" (click)="$event.stopPropagation()">
<div class="student-modal-header">
<h2>{{ isEditMode() ? 'Edit Student' : 'Create Student' }}</h2>
<button class="student-modal-close" (click)="closeModal()">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="student-modal-body">
<div class="form-group">
<label class="form-label">Full Name *</label>
<input class="form-input" [(ngModel)]="userForm.name" placeholder="Enter student's full name">
</div>
<div class="form-group">
<label class="form-label">Email Address *</label>
<input class="form-input" type="email" [(ngModel)]="userForm.email" placeholder="student@example.com">
</div>
<div class="form-group">
<label class="form-label">Password {{ isEditMode() ? '(Optional)' : '*' }}</label>
<input class="form-input" type="password" [(ngModel)]="userForm.password" [placeholder]="isEditMode() ? 'Leave blank to keep current password' : 'Enter initial password'">
@if (isEditMode()) {
<small class="form-hint">Change this only if the student forgot their password.</small>
}
</div>
</div>
<div class="student-modal-footer">
<button class="btn btn-outline" (click)="closeModal()">Cancel</button>
<button class="btn btn-primary" (click)="saveUser()" [disabled]="savingUser()">
@if (savingUser()) {
<div class="spinner spinner-sm"></div> Saving...
} @else {
<span class="material-symbols-rounded">save</span>
{{ isEditMode() ? 'Save Changes' : 'Create Student' }}
}
</button>
</div>
</div>
</div>
}
...@@ -2,13 +2,14 @@ import { Component, OnInit, signal } from '@angular/core'; ...@@ -2,13 +2,14 @@ import { Component, OnInit, signal } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { AuthService } from '../../../services/auth.service'; import { AuthService } from '../../../services/auth.service';
import { QuizService } from '../../../services/quiz.service'; import { QuizService } from '../../../services/quiz.service';
@Component({ @Component({
selector: 'app-admin-users', selector: 'app-admin-users',
standalone: true, standalone: true,
imports: [CommonModule, RouterLink], imports: [CommonModule, RouterLink, FormsModule],
templateUrl: './users.html', templateUrl: './users.html',
styleUrl: './users.css' styleUrl: './users.css'
}) })
...@@ -17,6 +18,18 @@ export class AdminUsersComponent implements OnInit { ...@@ -17,6 +18,18 @@ export class AdminUsersComponent implements OnInit {
loading = signal<boolean>(true); loading = signal<boolean>(true);
showAll = signal<boolean>(true); showAll = signal<boolean>(true);
// Modal State
showUserModal = signal<boolean>(false);
isEditMode = signal<boolean>(false);
savingUser = signal<boolean>(false);
editingUserId = signal<string | null>(null);
userForm = {
name: '',
email: '',
password: ''
};
constructor(public authService: AuthService, private quizService: QuizService, private location: Location) {} constructor(public authService: AuthService, private quizService: QuizService, private location: Location) {}
goBack(): void { goBack(): void {
...@@ -69,6 +82,72 @@ export class AdminUsersComponent implements OnInit { ...@@ -69,6 +82,72 @@ export class AdminUsersComponent implements OnInit {
} }
} }
openCreateModal(): void {
this.isEditMode.set(false);
this.editingUserId.set(null);
this.userForm = { name: '', email: '', password: '' };
this.showUserModal.set(true);
}
openEditModal(user: any): void {
this.isEditMode.set(true);
this.editingUserId.set(user._id);
this.userForm = { name: user.name, email: user.email, password: '' };
this.showUserModal.set(true);
}
closeModal(): void {
this.showUserModal.set(false);
}
saveUser(): void {
if (!this.userForm.name || !this.userForm.email) {
alert('Name and email are required');
return;
}
if (!this.isEditMode() && !this.userForm.password) {
alert('Password is required when creating a new student');
return;
}
this.savingUser.set(true);
if (this.isEditMode() && this.editingUserId()) {
// Edit User
const payload: any = { name: this.userForm.name, email: this.userForm.email };
if (this.userForm.password) {
payload.password = this.userForm.password;
}
this.quizService.editUser(this.editingUserId()!, payload).subscribe({
next: () => {
this.savingUser.set(false);
this.closeModal();
this.loadUsers();
},
error: (err) => {
console.error('Failed to update user', err);
alert(err.error?.message || 'Failed to update user');
this.savingUser.set(false);
}
});
} else {
// Create User
this.authService.register(this.userForm.name, this.userForm.email, this.userForm.password).subscribe({
next: () => {
this.savingUser.set(false);
this.closeModal();
this.loadUsers();
},
error: (err) => {
console.error('Failed to create user', err);
alert(err.error?.message || 'Failed to create user');
this.savingUser.set(false);
}
});
}
}
logout(): void { logout(): void {
this.authService.logout(); this.authService.logout();
} }
......
.page-container { .page-container {
padding: 32px 40px; padding: 32px 40px;
max-width: 1400px; max-width: 1400px;
} }
.page-header { .page-header {
margin-bottom: 32px; margin-bottom: 32px;
} }
.page-header h1 { .page-header h1 {
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
margin: 0 0 8px; margin: 0 0 8px;
} }
.page-header p { .page-header p {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 15px; font-size: 15px;
margin: 0; margin: 0;
} }
.filter-tabs { .filter-tabs {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 24px; margin-bottom: 24px;
} }
.tab { .tab {
padding: 10px 20px; padding: 10px 20px;
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
background: transparent; background: transparent;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
font-family: inherit; font-family: inherit;
} }
.tab:hover { .tab:hover {
border-color: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.2);
color: var(--text-primary); color: var(--text-primary);
} }
.tab.active { .tab.active {
background: rgba(102, 126, 234, 0.15); background: rgba(102, 126, 234, 0.15);
border-color: rgba(102, 126, 234, 0.3); border-color: rgba(102, 126, 234, 0.3);
color: #667eea; color: #667eea;
} }
.loading-state { .loading-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 80px 0; padding: 80px 0;
gap: 16px; gap: 16px;
} }
.loader { .loader {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1); border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea; border-top-color: #667eea;
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.loading-state p { .loading-state p {
color: var(--text-secondary); color: var(--text-secondary);
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 80px 0; padding: 80px 0;
} }
.empty-icon { .empty-icon {
font-size: 48px; font-size: 48px;
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
.empty-state h3 { .empty-state h3 {
color: var(--text-primary); color: var(--text-primary);
font-size: 18px; font-size: 18px;
margin: 0 0 8px; margin: 0 0 8px;
} }
.empty-state p { .empty-state p {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
} }
.users-grid { .users-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 16px; gap: 16px;
} }
.user-card { .user-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 12px;
padding: 20px; padding: 16px 20px;
background: var(--bg-card);
background: var(--bg-card); border: 1px solid var(--border-color);
border: 1px solid var(--border-color); border-radius: 16px;
text-decoration: none;
border-radius: 16px; transition: all 0.25s;
text-decoration: none; cursor: pointer;
transition: all 0.25s; overflow: hidden;
cursor: pointer; min-width: 0;
} }
.user-card:hover { .user-card:hover {
background: var(--bg-hover); background: var(--bg-hover);
border-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px); transform: translateY(-2px);
} }
.user-card-avatar { .user-card-avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 14px; border-radius: 14px;
background: linear-gradient(135deg, #667eea, #764ba2); background: linear-gradient(135deg, #667eea, #764ba2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--text-primary); color: var(--text-primary);
font-weight: 700; font-weight: 700;
font-size: 20px; font-size: 20px;
flex-shrink: 0; flex-shrink: 0;
} }
.user-card-info { .user-card-info {
flex: 1; flex: 1;
} }
.name-row { .name-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 4px; margin-bottom: 4px;
} flex-wrap: nowrap;
overflow: hidden;
.user-card-info h4 { }
color: var(--text-primary);
font-size: 15px; .user-card-info h4 {
font-weight: 600; color: var(--text-primary);
margin: 0; font-size: 15px;
} font-weight: 600;
margin: 0;
.user-card-info p { white-space: nowrap;
color: var(--text-secondary); overflow: hidden;
font-size: 13px; text-overflow: ellipsis;
margin: 0 0 4px; }
}
.user-card-info p {
.user-joined { color: var(--text-secondary);
color: var(--text-muted); font-size: 13px;
font-size: 11px; margin: 0 0 4px;
} }
.status-dot { .user-joined {
width: 10px; color: var(--text-muted);
height: 10px; font-size: 11px;
border-radius: 50%; }
background: rgba(255, 255, 255, 0.2);
flex-shrink: 0; .status-dot {
} width: 10px;
height: 10px;
.status-dot.online { border-radius: 50%;
background: #4ade80; background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 8px rgba(74, 222, 128, 0.4); flex-shrink: 0;
} }
.view-arrow { .status-dot.online {
color: rgba(255, 255, 255, 0.2); background: #4ade80;
font-size: 20px; box-shadow: 0 0 8px rgba(74, 222, 128, 0.4);
transition: all 0.2s; }
}
.view-arrow {
.user-card:hover .view-arrow { color: rgba(255, 255, 255, 0.2);
color: var(--text-secondary); font-size: 20px;
transform: translateX(4px); transition: all 0.2s;
} }
.action-btn { .user-card:hover .view-arrow {
background: transparent; color: var(--text-secondary);
border: none; transform: translateX(4px);
border-radius: 8px; }
width: 32px;
height: 32px; .action-btn {
display: flex; background: transparent;
align-items: center; border: none;
justify-content: center; border-radius: 8px;
color: var(--text-muted); width: 30px;
cursor: pointer; height: 30px;
transition: all 0.2s; min-width: 30px;
margin-left: 8px; display: flex;
} align-items: center;
justify-content: center;
.action-btn .material-symbols-rounded { color: var(--text-muted);
font-size: 20px; cursor: pointer;
} transition: all 0.2s;
flex-shrink: 0;
.text-danger:hover { }
background: rgba(239, 68, 68, 0.1);
color: #ef4444; .action-btn .material-symbols-rounded {
} font-size: 20px;
}
.level-badge {
padding: 4px 12px; .text-danger:hover {
border-radius: 20px; background: rgba(239, 68, 68, 0.1);
font-size: 11px; color: #ef4444;
font-weight: 700; }
text-transform: uppercase;
letter-spacing: 0.5px; .text-primary:hover {
flex-shrink: 0; background: rgba(102, 126, 234, 0.1);
} color: #667eea;
}
.level-badge[data-level="beginner"] {
background: rgba(148, 163, 184, 0.15); .level-badge {
color: #94a3b8; padding: 3px 10px;
} border-radius: 20px;
font-size: 10px;
.level-badge[data-level="intermediate"] { font-weight: 700;
background: rgba(59, 130, 246, 0.15); text-transform: uppercase;
color: #3b82f6; letter-spacing: 0.5px;
} flex-shrink: 0;
white-space: nowrap;
.level-badge[data-level="advanced"] { }
background: rgba(168, 85, 247, 0.15);
color: #a855f7; .level-badge[data-level="beginner"] {
} background: rgba(148, 163, 184, 0.15);
color: #94a3b8;
.level-badge[data-level="expert"] { }
background: rgba(245, 158, 11, 0.15);
color: #f59e0b; .level-badge[data-level="intermediate"] {
} background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
@media (max-width: 768px) { }
.page-container {
padding: 24px 16px; .level-badge[data-level="advanced"] {
} background: rgba(168, 85, 247, 0.15);
color: #a855f7;
.users-grid { }
grid-template-columns: 1fr;
} .level-badge[data-level="expert"] {
} background: rgba(245, 158, 11, 0.15);
\ No newline at end of file color: #f59e0b;
}
@media (max-width: 768px) {
.page-container {
padding: 24px 16px;
}
.users-grid {
grid-template-columns: 1fr;
}
}
/* ─── Student Modal Overlay ─────────────────────────────────── */
.student-modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
animation: fadeInOverlay 0.2s ease;
}
@keyframes fadeInOverlay {
from { opacity: 0; }
to { opacity: 1; }
}
.student-modal-container {
background: var(--bg-card, #fff);
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 20px;
width: 100%;
max-width: 480px;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.25);
animation: slideInModal 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
}
@keyframes slideInModal {
from { transform: scale(0.92) translateY(12px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
}
.student-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 28px 0;
}
.student-modal-header h2 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.student-modal-close {
background: transparent;
border: none;
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.2s;
}
.student-modal-close:hover {
background: var(--bg-hover, rgba(0,0,0,0.06));
color: var(--text-primary);
}
.student-modal-body {
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 16px;
}
.student-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 0 28px 24px;
}
.form-hint {
color: var(--text-secondary);
font-size: 12px;
margin-top: 4px;
display: block;
}
\ No newline at end of file
<div class="page-container animate-fade-in"> <div class="page-container animate-fade-in">
<div class="page-header"> <div class="page-header" style="display: flex; align-items: center; justify-content: space-between;">
<h1>Student Users</h1> <div>
<p>View and manage registered students</p> <h1>Student Users</h1>
<p class="page-subtitle">View and manage registered students</p>
</div>
<button class="btn btn-primary" (click)="openCreateModal()">
<span class="material-symbols-rounded">person_add</span> Create Student
</button>
</div> </div>
<div class="filter-tabs"> <div class="filter-tabs">
...@@ -38,7 +43,10 @@ ...@@ -38,7 +43,10 @@
<div class="status-dot" [class.online]="user.isLoggedIn" title="Online Status"></div> <div class="status-dot" [class.online]="user.isLoggedIn" title="Online Status"></div>
<button class="action-btn text-danger ml-auto" (click)="$event.preventDefault(); $event.stopPropagation(); deleteUser(user._id)" title="Delete Student"> <button class="action-btn text-primary ml-auto" style="margin-right: 8px;" (click)="$event.preventDefault(); $event.stopPropagation(); openEditModal(user)" title="Edit Student">
<span class="material-symbols-rounded">edit</span>
</button>
<button class="action-btn text-danger" (click)="$event.preventDefault(); $event.stopPropagation(); deleteUser(user._id)" title="Delete Student">
<span class="material-symbols-rounded">delete</span> <span class="material-symbols-rounded">delete</span>
</button> </button>
...@@ -48,3 +56,47 @@ ...@@ -48,3 +56,47 @@
</div> </div>
} }
</div> </div>
<!-- ─── Create / Edit Student Modal ───────────────────────────── -->
@if (showUserModal()) {
<div class="student-modal-overlay" (click)="closeModal()">
<div class="student-modal-container" (click)="$event.stopPropagation()">
<div class="student-modal-header">
<h2>{{ isEditMode() ? 'Edit Student' : 'Create Student' }}</h2>
<button class="student-modal-close" (click)="closeModal()">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="student-modal-body">
<div class="form-group">
<label class="form-label">Full Name *</label>
<input class="form-input" [(ngModel)]="userForm.name" placeholder="Enter student's full name">
</div>
<div class="form-group">
<label class="form-label">Email Address *</label>
<input class="form-input" type="email" [(ngModel)]="userForm.email" placeholder="student@example.com">
</div>
<div class="form-group">
<label class="form-label">Password {{ isEditMode() ? '(Optional)' : '*' }}</label>
<input class="form-input" type="password" [(ngModel)]="userForm.password" [placeholder]="isEditMode() ? 'Leave blank to keep current password' : 'Enter initial password'">
@if (isEditMode()) {
<small class="form-hint">Change this only if the student forgot their password.</small>
}
</div>
</div>
<div class="student-modal-footer">
<button class="btn btn-outline" (click)="closeModal()">Cancel</button>
<button class="btn btn-primary" (click)="saveUser()" [disabled]="savingUser()">
@if (savingUser()) {
<div class="spinner spinner-sm"></div> Saving...
} @else {
<span class="material-symbols-rounded">save</span>
{{ isEditMode() ? 'Save Changes' : 'Create Student' }}
}
</button>
</div>
</div>
</div>
}
import { Component, OnInit, signal } from '@angular/core'; import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { AuthService } from '../../../services/auth.service'; import { AuthService } from '../../../services/auth.service';
import { QuizService } from '../../../services/quiz.service'; import { QuizService } from '../../../services/quiz.service';
@Component({ @Component({
selector: 'app-users', selector: 'app-users',
imports: [CommonModule, RouterLink], imports: [CommonModule, RouterLink, FormsModule],
templateUrl: './users.html', templateUrl: './users.html',
styleUrl: './users.css', styleUrl: './users.css',
}) })
...@@ -15,6 +16,18 @@ export class Users { ...@@ -15,6 +16,18 @@ export class Users {
loading = signal<boolean>(true); loading = signal<boolean>(true);
showAll = signal<boolean>(true); showAll = signal<boolean>(true);
// Modal State
showUserModal = signal<boolean>(false);
isEditMode = signal<boolean>(false);
savingUser = signal<boolean>(false);
editingUserId = signal<string | null>(null);
userForm = {
name: '',
email: '',
password: ''
};
constructor(public authService: AuthService, private quizService: QuizService) {} constructor(public authService: AuthService, private quizService: QuizService) {}
ngOnInit(): void { ngOnInit(): void {
...@@ -63,6 +76,72 @@ export class Users { ...@@ -63,6 +76,72 @@ export class Users {
} }
} }
openCreateModal(): void {
this.isEditMode.set(false);
this.editingUserId.set(null);
this.userForm = { name: '', email: '', password: '' };
this.showUserModal.set(true);
}
openEditModal(user: any): void {
this.isEditMode.set(true);
this.editingUserId.set(user._id);
this.userForm = { name: user.name, email: user.email, password: '' };
this.showUserModal.set(true);
}
closeModal(): void {
this.showUserModal.set(false);
}
saveUser(): void {
if (!this.userForm.name || !this.userForm.email) {
alert('Name and email are required');
return;
}
if (!this.isEditMode() && !this.userForm.password) {
alert('Password is required when creating a new student');
return;
}
this.savingUser.set(true);
if (this.isEditMode() && this.editingUserId()) {
// Edit User
const payload: any = { name: this.userForm.name, email: this.userForm.email };
if (this.userForm.password) {
payload.password = this.userForm.password;
}
this.quizService.editUser(this.editingUserId()!, payload).subscribe({
next: () => {
this.savingUser.set(false);
this.closeModal();
this.loadUsers();
},
error: (err) => {
console.error('Failed to update user', err);
alert(err.error?.message || 'Failed to update user');
this.savingUser.set(false);
}
});
} else {
// Create User
this.authService.register(this.userForm.name, this.userForm.email, this.userForm.password).subscribe({
next: () => {
this.savingUser.set(false);
this.closeModal();
this.loadUsers();
},
error: (err) => {
console.error('Failed to create user', err);
alert(err.error?.message || 'Failed to create user');
this.savingUser.set(false);
}
});
}
}
logout(): void { logout(): void {
this.authService.logout(); this.authService.logout();
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment