register, login, create and manage sites and pages

This commit is contained in:
Krrish Ghimire
2026-06-30 11:13:55 +05:45
commit 65f38d68c5
110 changed files with 28049 additions and 0 deletions

View File

View File

@@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet></router-outlet>`,
})
export class AppComponent {}

View File

@@ -0,0 +1,16 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { JwtInterceptor } from './core/interceptors/jwt.interceptor';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimationsAsync(),
provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
],
};

View File

@@ -0,0 +1,24 @@
import { Routes } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{
path: 'login',
loadComponent: () => import('./auth/login/login.component').then(m => m.LoginComponent),
},
{
path: 'register',
loadComponent: () => import('./auth/register/register.component').then(m => m.RegisterComponent),
},
{
path: 'oauth2/callback',
loadComponent: () => import('./auth/oauth-callback/oauth-callback.component').then(m => m.OAuthCallbackComponent),
},
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [AuthGuard],
},
{ path: '**', redirectTo: '/dashboard' },
];

View File

@@ -0,0 +1,99 @@
import { Component } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatDividerModule } from '@angular/material/divider';
import { AuthService } from '../../core/services/auth.service';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-login',
standalone: true,
imports: [
RouterLink, ReactiveFormsModule, NgIf,
MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule,
MatProgressSpinnerModule, MatDividerModule,
],
template: `
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<mat-card class="w-full max-w-md p-8">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-indigo-600">Indie</h1>
<p class="text-gray-500 mt-1">Sign in to your account</p>
</div>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-4">
<mat-form-field appearance="outline" class="w-full">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email" placeholder="you@example.com" />
<mat-error *ngIf="loginForm.get('email')?.hasError('required')">Email is required</mat-error>
<mat-error *ngIf="loginForm.get('email')?.hasError('email')">Invalid email format</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="loginForm.get('password')?.hasError('required')">Password is required</mat-error>
</mat-form-field>
<div *ngIf="error" class="text-red-600 text-sm">{{ error }}</div>
<button mat-flat-button color="primary" type="submit" [disabled]="loading" class="w-full py-2">
<mat-spinner *ngIf="loading" diameter="20" class="inline-block mr-2"></mat-spinner>
{{ loading ? 'Signing in...' : 'Sign In' }}
</button>
</form>
<div class="my-4">
<mat-divider></mat-divider>
</div>
<div class="flex flex-col gap-3">
<a mat-stroked-button href="/oauth2/authorization/google" class="w-full">
Sign in with Google
</a>
<a mat-stroked-button href="/oauth2/authorization/github" class="w-full">
Sign in with GitHub
</a>
</div>
<p class="text-center mt-6 text-sm text-gray-500">
Don't have an account?
<a routerLink="/register" class="text-indigo-600 font-medium hover:underline">Register</a>
</p>
</mat-card>
</div>
`,
})
export class LoginComponent {
loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
});
loading = false;
error = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
) {}
onSubmit(): void {
if (this.loginForm.invalid) return;
this.loading = true;
this.error = '';
const { email, password } = this.loginForm.value;
this.authService.login(email!, password!).subscribe({
next: () => this.router.navigate(['/dashboard']),
error: (err) => {
this.error = err.error?.message || err.error?.error || 'Invalid email or password';
this.loading = false;
},
});
}
}

View File

@@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../../core/services/auth.service';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@Component({
selector: 'app-oauth-callback',
standalone: true,
imports: [MatProgressSpinnerModule],
template: `
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="text-center">
<mat-spinner diameter="40" class="mx-auto mb-4"></mat-spinner>
<p class="text-gray-500">Completing sign in...</p>
</div>
</div>
`,
})
export class OAuthCallbackComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,
) {}
ngOnInit(): void {
const accessToken = this.route.snapshot.queryParamMap.get('accessToken');
const refreshToken = this.route.snapshot.queryParamMap.get('refreshToken');
if (accessToken && refreshToken) {
this.authService.setSessionFromOAuth(accessToken, refreshToken);
this.router.navigate(['/dashboard']);
} else {
this.router.navigate(['/login']);
}
}
}

View File

@@ -0,0 +1,93 @@
import { Component } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { NgIf } from '@angular/common';
import { AuthService } from '../../core/services/auth.service';
@Component({
selector: 'app-register',
standalone: true,
imports: [
RouterLink, ReactiveFormsModule, NgIf,
MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule,
MatProgressSpinnerModule,
],
template: `
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<mat-card class="w-full max-w-md p-8">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-indigo-600">Indie</h1>
<p class="text-gray-500 mt-1">Create your account</p>
</div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-4">
<mat-form-field appearance="outline" class="w-full">
<mat-label>Name</mat-label>
<input matInput formControlName="name" placeholder="Your name" />
<mat-error *ngIf="registerForm.get('name')?.hasError('required')">Name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email" placeholder="you@example.com" />
<mat-error *ngIf="registerForm.get('email')?.hasError('required')">Email is required</mat-error>
<mat-error *ngIf="registerForm.get('email')?.hasError('email')">Invalid email format</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="registerForm.get('password')?.hasError('required')">Password is required</mat-error>
<mat-error *ngIf="registerForm.get('password')?.hasError('minlength')">At least 8 characters</mat-error>
</mat-form-field>
<div *ngIf="error" class="text-red-600 text-sm">{{ error }}</div>
<button mat-flat-button color="primary" type="submit" [disabled]="loading" class="w-full py-2">
<mat-spinner *ngIf="loading" diameter="20" class="inline-block mr-2"></mat-spinner>
{{ loading ? 'Creating account...' : 'Create Account' }}
</button>
</form>
<p class="text-center mt-6 text-sm text-gray-500">
Already have an account?
<a routerLink="/login" class="text-indigo-600 font-medium hover:underline">Sign in</a>
</p>
</mat-card>
</div>
`,
})
export class RegisterComponent {
registerForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
loading = false;
error = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
) {}
onSubmit(): void {
if (this.registerForm.invalid) return;
this.loading = true;
this.error = '';
const { name, email, password } = this.registerForm.value;
this.authService.register(name!, email!, password!).subscribe({
next: () => this.router.navigate(['/dashboard']),
error: (err) => {
this.error = err.error?.message || err.error?.error || 'Registration failed';
this.loading = false;
},
});
}
}

View File

@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { AuthService } from '../services/auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean | UrlTree {
if (this.authService.isAuthenticated()) {
return true;
}
return this.router.parseUrl('/login');
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = this.authService.getAccessToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
}
return next.handle(req);
}
}

View File

@@ -0,0 +1,14 @@
export interface AuthResponse {
accessToken: string;
refreshToken: string;
tokenType: string;
user: UserInfo;
}
export interface UserInfo {
id: string;
email: string;
name: string;
avatarUrl: string;
provider: 'LOCAL' | 'GOOGLE' | 'GITHUB' | 'OPENID';
}

View File

@@ -0,0 +1,18 @@
export interface PageResponse {
id: string;
siteId: string;
title: string;
slug: string;
seoTitle: string;
seoDescription: string;
orderIndex: number;
createdAt: string;
}
export interface PageRequest {
title: string;
slug: string;
seoTitle?: string;
seoDescription?: string;
orderIndex: number;
}

View File

@@ -0,0 +1,19 @@
export interface SiteResponse {
id: string;
userId: string;
name: string;
description: string;
subdomain: string;
themeConfig: Record<string, unknown>;
status: 'DRAFT' | 'PUBLISHED';
publishedAt: string | null;
createdAt: string;
updatedAt: string;
}
export interface SiteRequest {
name: string;
description: string;
subdomain: string;
themeConfig?: Record<string, unknown>;
}

View File

@@ -0,0 +1,101 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, map, tap } from 'rxjs';
import { AuthResponse, UserInfo } from '../models/auth-response';
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly ACCESS_TOKEN_KEY = 'indie_access_token';
private readonly REFRESH_TOKEN_KEY = 'indie_refresh_token';
private readonly USER_KEY = 'indie_user';
private userSubject = new BehaviorSubject<UserInfo | null>(this.loadUser());
user$ = this.userSubject.asObservable();
constructor(private http: HttpClient) {}
register(name: string, email: string, password: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>('/api/auth/register', { name, email, password })
.pipe(tap(res => this.setSession(res)));
}
login(email: string, password: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>('/api/auth/login', { email, password })
.pipe(tap(res => this.setSession(res)));
}
refreshToken(refreshToken: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>('/api/auth/refresh', { refreshToken })
.pipe(tap(res => this.setSession(res)));
}
logout(): Observable<void> {
return this.http.post<void>('/api/auth/logout', {})
.pipe(tap(() => this.clearSession()));
}
getAccessToken(): string | null {
return localStorage.getItem(this.ACCESS_TOKEN_KEY);
}
getRefreshToken(): string | null {
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
}
getUser(): UserInfo | null {
return this.userSubject.value;
}
isAuthenticated(): boolean {
return !!this.getAccessToken();
}
setSessionFromOAuth(accessToken: string, refreshToken: string): void {
localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
const payload = this.decodeToken(accessToken);
if (payload) {
const user: UserInfo = {
id: payload.sub,
email: payload.email,
name: payload.name,
avatarUrl: payload.avatarUrl || '',
provider: payload.provider || 'LOCAL',
};
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
this.userSubject.next(user);
}
}
clearSessionForLogout(): void {
this.clearSession();
}
private setSession(res: AuthResponse): void {
localStorage.setItem(this.ACCESS_TOKEN_KEY, res.accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, res.refreshToken);
localStorage.setItem(this.USER_KEY, JSON.stringify(res.user));
this.userSubject.next(res.user);
}
private clearSession(): void {
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
localStorage.removeItem(this.USER_KEY);
this.userSubject.next(null);
}
private loadUser(): UserInfo | null {
const data = localStorage.getItem(this.USER_KEY);
return data ? JSON.parse(data) : null;
}
private decodeToken(token: string): any {
try {
const parts = token.split('.');
return JSON.parse(atob(parts[1]));
} catch {
return null;
}
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { PageResponse, PageRequest } from '../models/page';
@Injectable({ providedIn: 'root' })
export class PageService {
constructor(private http: HttpClient) {}
list(siteId: string): Observable<PageResponse[]> {
return this.http.get<PageResponse[]>(`/api/sites/${siteId}/pages`);
}
create(siteId: string, request: PageRequest): Observable<PageResponse> {
return this.http.post<PageResponse>(`/api/sites/${siteId}/pages`, request);
}
update(siteId: string, pageId: string, request: PageRequest): Observable<PageResponse> {
return this.http.put<PageResponse>(`/api/sites/${siteId}/pages/${pageId}`, request);
}
delete(siteId: string, pageId: string): Observable<void> {
return this.http.delete<void>(`/api/sites/${siteId}/pages/${pageId}`);
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { SiteResponse, SiteRequest } from '../models/site';
@Injectable({ providedIn: 'root' })
export class SiteService {
constructor(private http: HttpClient) {}
list(): Observable<SiteResponse[]> {
return this.http.get<SiteResponse[]>('/api/sites');
}
create(request: SiteRequest): Observable<SiteResponse> {
return this.http.post<SiteResponse>('/api/sites', request);
}
get(id: string): Observable<SiteResponse> {
return this.http.get<SiteResponse>(`/api/sites/${id}`);
}
update(id: string, request: SiteRequest): Observable<SiteResponse> {
return this.http.put<SiteResponse>(`/api/sites/${id}`, request);
}
delete(id: string): Observable<void> {
return this.http.delete<void>(`/api/sites/${id}`);
}
}

View File

@@ -0,0 +1,72 @@
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { NgIf } from '@angular/common';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { SiteService } from '../../core/services/site.service';
@Component({
selector: 'app-create-site-dialog',
standalone: true,
imports: [NgIf, MatDialogModule, MatFormFieldModule, MatInputModule, MatButtonModule, ReactiveFormsModule],
template: `
<h2 mat-dialog-title>Create New Site</h2>
<mat-dialog-content>
<form [formGroup]="form" class="flex flex-col gap-4 pt-2">
<mat-form-field appearance="outline" class="w-full">
<mat-label>Site Name</mat-label>
<input matInput formControlName="name" placeholder="My Awesome Site" />
<mat-error>Name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Subdomain</mat-label>
<span matTextSuffix>.indie.com</span>
<input matInput formControlName="subdomain" placeholder="my-site" />
<mat-error *ngIf="form.get('subdomain')?.hasError('required')">Subdomain is required</mat-error>
<mat-error *ngIf="form.get('subdomain')?.hasError('pattern')">
Lowercase letters, numbers, and hyphens only
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Description (optional)</mat-label>
<textarea matInput formControlName="description" rows="3" placeholder="What's your site about?"></textarea>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-flat-button color="primary" [disabled]="form.invalid || loading" (click)="onCreate()">
{{ loading ? 'Creating...' : 'Create' }}
</button>
</mat-dialog-actions>
`,
})
export class CreateSiteDialog {
private fb = inject(FormBuilder);
private dialogRef = inject(MatDialogRef<CreateSiteDialog>);
private siteService = inject(SiteService);
form = this.fb.group({
name: ['', Validators.required],
subdomain: ['', [Validators.pattern(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/)]],
description: [''],
});
loading = false;
onCreate(): void {
if (this.form.invalid) return;
this.loading = true;
this.siteService.create(this.form.value as any).subscribe({
next: (site) => {
this.dialogRef.close(site);
},
error: () => {
this.loading = false;
},
});
}
}

View File

@@ -0,0 +1,142 @@
import { Component, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router';
import { NgFor, NgIf, DatePipe } from '@angular/common';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { AuthService } from '../core/services/auth.service';
import { SiteService } from '../core/services/site.service';
import { SiteResponse } from '../core/models/site';
import { CreateSiteDialog } from './create-site-dialog/create-site-dialog';
import { DeleteSiteDialog } from './delete-site-dialog/delete-site-dialog';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
NgFor, NgIf, DatePipe,
MatToolbarModule, MatButtonModule, MatCardModule, MatIconModule,
MatMenuModule, MatDialogModule, MatProgressSpinnerModule,
],
template: `
<mat-toolbar color="primary" class="flex justify-between">
<span class="text-xl font-bold">Indie</span>
<div class="flex items-center gap-3">
<span class="text-sm">{{ user?.name }}</span>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
<span>Logout</span>
</button>
</mat-menu>
</div>
</mat-toolbar>
<div class="max-w-6xl mx-auto p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold text-gray-800">My Sites</h2>
<button mat-flat-button color="primary" (click)="openCreateDialog()">
<mat-icon class="mr-1">add</mat-icon>
New Site
</button>
</div>
<div *ngIf="loading" class="flex justify-center py-20">
<mat-spinner diameter="40"></mat-spinner>
</div>
<div *ngIf="!loading && sites.length === 0" class="text-center py-20 text-gray-400">
<mat-icon style="font-size: 64px; width: 64px; height: 64px;" class="mb-4">web</mat-icon>
<p class="text-lg">No sites yet. Create your first site!</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<mat-card *ngFor="let site of sites" class="cursor-pointer hover:shadow-lg transition-shadow">
<mat-card-header>
<mat-card-title>{{ site.name }}</mat-card-title>
<mat-card-subtitle *ngIf="site.subdomain">{{ site.subdomain }}.indie.com</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p class="text-gray-600 text-sm mt-2">{{ site.description || 'No description' }}</p>
<div class="flex items-center gap-2 mt-3">
<span class="text-xs px-2 py-0.5 rounded-full"
[class.bg-green-100]="site.status === 'PUBLISHED'"
[class.text-green-700]="site.status === 'PUBLISHED'"
[class.bg-gray-100]="site.status === 'DRAFT'"
[class.text-gray-600]="site.status === 'DRAFT'">
{{ site.status === 'PUBLISHED' ? 'Published' : 'Draft' }}
</span>
<span class="text-xs text-gray-400">{{ site.createdAt | date:'mediumDate' }}</span>
</div>
</mat-card-content>
<mat-card-actions class="flex justify-end">
<button mat-icon-button color="warn" (click)="$event.stopPropagation(); openDeleteDialog(site)">
<mat-icon>delete</mat-icon>
</button>
</mat-card-actions>
</mat-card>
</div>
</div>
`,
})
export class DashboardComponent implements OnInit {
private authService = inject(AuthService);
private siteService = inject(SiteService);
private dialog = inject(MatDialog);
private router = inject(Router);
user = this.authService.getUser();
sites: SiteResponse[] = [];
loading = true;
ngOnInit(): void {
this.loadSites();
}
loadSites(): void {
this.loading = true;
this.siteService.list().subscribe({
next: (sites) => {
this.sites = sites;
this.loading = false;
},
error: () => this.loading = false,
});
}
openCreateDialog(): void {
const ref = this.dialog.open(CreateSiteDialog, { width: '480px' });
ref.afterClosed().subscribe((site) => {
if (site) this.loadSites();
});
}
openDeleteDialog(site: SiteResponse): void {
const ref = this.dialog.open(DeleteSiteDialog, {
width: '400px',
data: { name: site.name },
});
ref.afterClosed().subscribe((confirmed) => {
if (confirmed) {
this.siteService.delete(site.id).subscribe(() => this.loadSites());
}
});
}
logout(): void {
this.authService.logout().subscribe({
next: () => this.router.navigate(['/login']),
error: () => {
this.authService.clearSessionForLogout();
this.router.navigate(['/login']);
},
});
}
}

View File

@@ -0,0 +1,24 @@
import { Component, inject } from '@angular/core';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-delete-site-dialog',
standalone: true,
imports: [MatDialogModule, MatButtonModule],
template: `
<h2 mat-dialog-title>Delete Site</h2>
<mat-dialog-content>
<p>Are you sure you want to delete <strong>{{ data.name }}</strong>?</p>
<p class="text-red-600 text-sm mt-2">This action cannot be undone. All pages and content will be permanently removed.</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-flat-button color="warn" [mat-dialog-close]="true">Delete</button>
</mat-dialog-actions>
`,
})
export class DeleteSiteDialog {
dialogRef = inject(MatDialogRef<DeleteSiteDialog>);
data = inject<{ name: string }>(MAT_DIALOG_DATA);
}