1 Commits

13 changed files with 97 additions and 121 deletions
@@ -88,7 +88,7 @@
</div> </div>
} }
@if (project.categories.length) { @if (project.categories?.length) {
<div class="detail-section"> <div class="detail-section">
<h4>Categories</h4> <h4>Categories</h4>
<div class="tag-list"> <div class="tag-list">
@@ -4,7 +4,7 @@
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
background: var(--eerie-black-2, #1e1e1e); background: var(--eerie-black-2, #1e1e1e);
border: 1.2px solid hsla(45, 100%, 72%, 0.45); border: 1px solid var(--jet, #383838);
border-radius: 14px; border-radius: 14px;
} }
+12
View File
@@ -0,0 +1,12 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes))
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
+12
View File
@@ -0,0 +1,12 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: 'admin/login',
renderMode: RenderMode.Prerender
},
{
path: '**',
renderMode: RenderMode.Client
}
];
+7 -2
View File
@@ -16,11 +16,16 @@ const enum AdminRouteTitles {
export const routes: Routes = [ export const routes: Routes = [
{ {
path: '', path: '',
redirectTo: 'about', redirectTo: 'admin/about',
pathMatch: 'full' pathMatch: 'full'
}, },
{ {
path: '', path: 'admin',
redirectTo: 'admin/about',
pathMatch: 'full'
},
{
path: 'admin',
component: AdminLayout, component: AdminLayout,
title: 'Admin', title: 'Admin',
children: [ children: [
+1 -1
View File
@@ -111,7 +111,7 @@ export class AuthService {
this.http.post<void>(this.api('/api/v1/auth/logout'), {}).subscribe(); this.http.post<void>(this.api('/api/v1/auth/logout'), {}).subscribe();
this.accessToken = null; this.accessToken = null;
this.safeRemoveToken(); this.safeRemoveToken();
this.router.navigateByUrl('login'); this.router.navigate(['/admin/login']);
} }
refreshToken(): Observable<RefreshTokenResponse> { refreshToken(): Observable<RefreshTokenResponse> {
+12 -16
View File
@@ -2,11 +2,11 @@
<div class="verify-card"> <div class="verify-card">
@if(!isOtpSent()){ @if(!isOtpSent()){
<h2 class="title">🔐 Login</h2> <h2 class="title">🔐 Login</h2>
} }
@if(isOtpSent() && !isVerified()){ @if(isOtpSent() && !isVerified()){
<h2 class="title">🔐 OTP Verification</h2> <h2 class="title">🔐 OTP Verification</h2>
} }
<!-- Step 1: Enter Email --> <!-- Step 1: Enter Email -->
@@ -21,9 +21,19 @@
<!-- Step 2: OTP Input --> <!-- Step 2: OTP Input -->
@if (isOtpSent() && !isVerified()) { @if (isOtpSent() && !isVerified()) {
<div class="otp-section"> <div class="otp-section">
<p class="info-text">
OTP sent to <b>{{ emailForm.value.email }}</b>
</p>
<form [formGroup]="otpForm" (ngSubmit)="verifyOtp()"> <form [formGroup]="otpForm" (ngSubmit)="verifyOtp()">
<label for="otp">Enter 6-digit OTP</label> <label for="otp">Enter 6-digit OTP</label>
<input id="otp" type="text" maxlength="6" formControlName="otp" placeholder="123456" /> <input id="otp" type="text" maxlength="6" formControlName="otp" placeholder="123456" />
@if (isError()) {
<div class="error-banner">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{{ message() }}</span>
</div>
}
<button type="submit" [disabled]="otpForm.invalid"> <button type="submit" [disabled]="otpForm.invalid">
Verify OTP Verify OTP
</button> </button>
@@ -37,19 +47,5 @@
</button> </button>
</div> </div>
} }
@if (isError()) {
<div class="error-banner">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{{ message() }}</span>
</div>
}
@if (!isError() && isOtpSent() && !isVerified()) {
<div class="success-banner">
<i class="fa-solid fa-circle-check"></i>
<span>{{ message() }}</span>
</div>
}
</div> </div>
</div> </div>
-20
View File
@@ -171,23 +171,3 @@ button {
font-size: 14px; font-size: 14px;
margin-bottom: 10px; margin-bottom: 10px;
} }
/* Success banner for OTP sent */
.success-banner {
display: flex;
align-items: center;
gap: 8px;
background: rgba(120, 255, 140, 0.07);
border: 1px solid rgba(120, 255, 140, 0.25);
border-radius: 8px;
padding: 10px 14px;
margin: 10px 0 10px;
color: #78ff8c;
font-size: 13px;
i {
font-size: 15px;
flex-shrink: 0;
color: #78ff8c;
}
}
+8 -20
View File
@@ -19,7 +19,7 @@ export class OtpComponent implements OnInit {
message = signal(''); message = signal('');
countdown = signal(0); countdown = signal(0);
timer: NodeJS.Timeout | undefined; timer: NodeJS.Timeout | undefined;
returnUrl = ''; returnUrl = '/';
fb: FormBuilder = inject(FormBuilder); fb: FormBuilder = inject(FormBuilder);
authService: AuthService = inject(AuthService); authService: AuthService = inject(AuthService);
router: Router = inject(Router); router: Router = inject(Router);
@@ -35,40 +35,28 @@ export class OtpComponent implements OnInit {
}); });
if(this.authService.isLoggedIn()){ if(this.authService.isLoggedIn()){
this.router.navigate(['']); this.router.navigateByUrl('/');
} }
} }
ngOnInit() { ngOnInit() {
this.returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') || ''; this.returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') || '/';
} }
sendOtp() { sendOtp() {
if (this.emailForm.invalid) return; if (this.emailForm.invalid) return;
const email = this.emailForm.value.email; const email = this.emailForm.value.email;
this.authService.sendOtp(email).subscribe({ this.authService.sendOtp(email).subscribe(() => {
next: () => { this.isOtpSent.set(true);
this.isOtpSent.set(true); this.message.set('OTP sent successfully!');
this.isError.set(false); this.startTimer(30); // 30 seconds countdown
this.message.set(`OTP sent to ${email}.`);
this.startTimer(30); // 30 seconds countdown
},
error: (err) => {
this.isError.set(true);
if (err.status === 404) {
this.message.set('Email not found. Please check and try again.');
} else {
this.message.set('Failed to send OTP. Please try again.');
}
}
}); });
} }
resendOtp() { resendOtp() {
if (this.countdown() > 0) return; if (this.countdown() > 0) return;
this.otpForm.reset();
this.sendOtp(); this.sendOtp();
} }
+1 -1
View File
@@ -16,5 +16,5 @@ export const authGuard: CanActivateFn = (route, state) => {
return auth.currentToken return auth.currentToken
? true ? true
: router.parseUrl(`login?returnUrl=${state.url}`); : router.parseUrl(`/admin/login?returnUrl=${state.url}`);
}; };
+29 -55
View File
@@ -1,65 +1,41 @@
/* Dynamic form spacing and material tweaks to match popup theme */ /* Dynamic form spacing and material tweaks to match popup theme */
.full-width { .full-width { width: 100%; display: block; }
width: 100%;
display: block;
}
mat-form-field.full-width { mat-form-field.full-width { margin-bottom: 12px; }
margin-bottom: 12px;
}
/* Reduce label size slightly to fit popup */ /* Reduce label size slightly to fit popup */
mat-form-field .mat-form-field-label { mat-form-field .mat-form-field-label { font-size: 0.95rem; color: var(--light-gray-70); }
font-size: 0.95rem;
color: var(--light-gray-70);
}
/* Make input text contrast better */ /* Make input text contrast better */
.mat-input-element, .mat-input-element,
input.mat-mdc-input-element, input.mat-mdc-input-element,
textarea.mat-mdc-input-element, textarea.mat-mdc-input-element,
.mat-mdc-input-element { .mat-mdc-input-element {
color: var(--white-1) !important; color: var(--white-1) !important;
} }
/* Smaller helper/hint text */ /* Smaller helper/hint text */
.mat-hint { .mat-hint { color: var(--light-gray-70); font-size: 0.85rem; }
color: var(--light-gray-70);
font-size: 0.85rem;
}
/* Error styling consistent with theme */ /* Error styling consistent with theme */
mat-error { mat-error { color: #ff8a80; font-size: 0.9rem; }
color: #ff8a80;
font-size: 0.9rem;
}
/* Outline border always visible, highlight on focus */ /* Outline border always visible, highlight on focus */
::ng-deep .mdc-notched-outline__leading, ::ng-deep .mdc-notched-outline__leading,
::ng-deep .mdc-notched-outline__notch, ::ng-deep .mdc-notched-outline__notch,
::ng-deep .mdc-notched-outline__trailing { ::ng-deep .mdc-notched-outline__trailing {
border-color: var(--light-gray-70) !important; border-color: var(--light-gray-70) !important;
} }
::ng-deep .mat-mdc-form-field.mat-focused .mdc-notched-outline__leading, ::ng-deep .mat-mdc-form-field.mat-focused .mdc-notched-outline__leading,
::ng-deep .mat-mdc-form-field.mat-focused .mdc-notched-outline__notch, ::ng-deep .mat-mdc-form-field.mat-focused .mdc-notched-outline__notch,
::ng-deep .mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing { ::ng-deep .mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing {
border-color: var(--orange-yellow-crayola) !important; border-color: var(--orange-yellow-crayola) !important;
} }
/* Make array item fields inline on larger screens */ /* Make array item fields inline on larger screens */
.array-item-fields { .array-item-fields { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
display: flex; .array-item-fields mat-form-field { flex: 1 1 auto; margin-bottom: 0; min-width: 120px; }
gap: 12px;
align-items: baseline;
flex-wrap: wrap;
}
.array-item-fields mat-form-field {
flex: 1 1 auto;
margin-bottom: 0;
min-width: 120px;
}
/* Stack array item fields vertically on small screens */ /* Stack array item fields vertically on small screens */
@media (max-width: 580px) { @media (max-width: 580px) {
@@ -76,38 +52,37 @@ mat-error {
/* Hide subscript wrapper inside array items so fields align */ /* Hide subscript wrapper inside array items so fields align */
.array-item ::ng-deep .mat-mdc-form-field-subscript-wrapper { .array-item ::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none; display: none;
} }
/* Remove link at bottom-right of each array card */ /* Remove link at bottom-right of each array card */
.array-item-actions { .array-item-actions {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 4px; margin-top: 4px;
} }
.remove-link { .remove-link {
background: none; background: none;
border: none; border: none;
color: rgba(255, 255, 255, 0.35); color: rgba(255, 255, 255, 0.35);
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
transition: color 0.2s ease, background 0.2s ease; transition: color 0.2s ease, background 0.2s ease;
&:hover { &:hover {
color: hsl(0deg 75% 60%); color: hsl(0deg 75% 60%);
background: rgba(255, 70, 70, 0.1); background: rgba(255, 70, 70, 0.1);
} }
} }
/* Force material label/input colors to match dark popup theme (covers MDC + legacy classes) */ /* Force material label/input colors to match dark popup theme (covers MDC + legacy classes) */
.themed-popup { .themed-popup {
::ng-deep .mat-form-field-label, ::ng-deep .mat-form-field-label,
::ng-deep .mat-mdc-floating-label, ::ng-deep .mat-mdc-floating-label,
::ng-deep .mat-form-field .mat-form-field-label { ::ng-deep .mat-form-field .mat-form-field-label {
@@ -132,7 +107,6 @@ mat-error {
opacity: 1 !important; opacity: 1 !important;
} }
} }
.form-array { .form-array {
margin-bottom: 20px; margin-bottom: 20px;
padding: 10px 0; padding: 10px 0;
+8
View File
@@ -0,0 +1,8 @@
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { config } from './app/app.config.server';
const bootstrap = (context: BootstrapContext) =>
bootstrapApplication(App, config, context);
export default bootstrap;
+5 -4
View File
@@ -507,9 +507,9 @@
.mat-mdc-text-field.mat-mdc-focused .mat-mdc-notched-outline .mat-mdc-notched-outline-path { .mat-mdc-text-field.mat-mdc-focused .mat-mdc-notched-outline .mat-mdc-notched-outline-path {
stroke: var(--orange-yellow-crayola) !important; stroke: var(--orange-yellow-crayola) !important;
border-color: var(--orange-yellow-crayola) !important; border-color: var(--orange-yellow-crayola) !important;
}
/* Additional overrides: set MDC theme variables and force a gold focus ring */ /* Additional overrides: set MDC theme variables and force a gold focus ring */
// .themed-popup { .themed-popup {
/* Override MDC primary color used by many Material components */ /* Override MDC primary color used by many Material components */
--mdc-theme-primary: var(--orange-yellow-crayola); --mdc-theme-primary: var(--orange-yellow-crayola);
--mdc-theme-on-primary: var(--smoky-black); --mdc-theme-on-primary: var(--smoky-black);
@@ -529,9 +529,10 @@
.mat-mdc-text-field.mat-mdc-focused .mat-mdc-notched-outline-path { .mat-mdc-text-field.mat-mdc-focused .mat-mdc-notched-outline-path {
stroke: var(--orange-yellow-crayola) !important; stroke: var(--orange-yellow-crayola) !important;
} }
// } }
// box-shadow: 0 6px 18px rgba(36,26,18,0.12) !important; box-shadow: 0 6px 18px rgba(36,26,18,0.12) !important;
} }
}
.contacts-list { .contacts-list {
display: grid; display: grid;