AI Implementation feature(883): Admin Area General Settings and Categories 1.01 (#23)

This commit was merged in pull request #23.
This commit is contained in:
2026-07-22 12:32:00 +00:00
parent fac3179427
commit 0a2b2664b4
9 changed files with 171 additions and 89 deletions
@@ -5,7 +5,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AdminService, GeneralSettings, ThemeView } from '../../core/services/admin.service';
import { MarkdownService } from '../../core/services/markdown.service';
import { AdminCategoriesComponent } from './categories/categories.component';
import { deriveEventState, endAfterStartValidator, toDatetimeLocal, toIsoUtc } from './general.pure';
import {
deriveEventState,
endAfterStartValidator,
normalizePageTitle,
pageTitleError,
toDatetimeLocal,
toIsoUtc,
} from './general.pure';
@Component({
selector: 'app-admin-general',
@@ -42,6 +49,9 @@ import { deriveEventState, endAfterStartValidator, toDatetimeLocal, toIsoUtc } f
<label for="pageTitle">Page title</label>
<div>
<input id="pageTitle" type="text" formControlName="pageTitle" data-testid="general-pageTitle" />
@if (showPageTitleError()) {
<div class="field-error" data-testid="general-pageTitle-error">{{ pageTitleMessage() }}</div>
}
</div>
<label for="logo">Logo</label>
@@ -165,7 +175,7 @@ export class AdminGeneralComponent implements OnInit {
readonly form = this.fb.nonNullable.group(
{
pageTitle: this.fb.nonNullable.control('', [Validators.required, Validators.maxLength(120)]),
pageTitle: this.fb.nonNullable.control('', [Validators.required, Validators.maxLength(120), this.pageTitleNotBlankValidator()]),
logo: this.fb.nonNullable.control(''),
welcomeMarkdown: this.fb.nonNullable.control(''),
themeKey: this.fb.nonNullable.control('classic'),
@@ -186,6 +196,26 @@ export class AdminGeneralComponent implements OnInit {
return state.toUpperCase();
});
readonly showPageTitleError = computed(() => {
const c = this.form.controls.pageTitle;
return (c.touched || c.dirty) && c.invalid;
});
readonly pageTitleMessage = computed(() => {
const err = pageTitleError(this.form.controls.pageTitle.value);
if (err === 'required') {
return 'Page title is required and cannot contain only whitespace.';
}
if (err === 'maxlength') {
return 'Page title must be 120 characters or fewer.';
}
const controlErrors = this.form.controls.pageTitle.errors;
if (controlErrors?.['required']) return 'Page title is required and cannot contain only whitespace.';
if (controlErrors?.['maxlength']) return 'Page title must be 120 characters or fewer.';
if (controlErrors?.['whitespace']) return 'Page title is required and cannot contain only whitespace.';
return null;
});
constructor() {
this.form.controls.welcomeMarkdown.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
@@ -208,6 +238,15 @@ export class AdminGeneralComponent implements OnInit {
}
}
private pageTitleNotBlankValidator(): (ctrl: { value: string | null }) => { whitespace: true } | null {
return (ctrl) => {
const value = ctrl?.value ?? '';
if (value.length === 0) return null;
if (value.trim().length === 0) return { whitespace: true };
return null;
};
}
private applySettings(s: GeneralSettings): void {
this.form.patchValue({
pageTitle: s.pageTitle,
@@ -232,7 +271,7 @@ export class AdminGeneralComponent implements OnInit {
try {
const v = this.form.getRawValue();
const updated = await this.admin.updateGeneralSettings({
pageTitle: v.pageTitle,
pageTitle: normalizePageTitle(v.pageTitle),
logo: v.logo,
welcomeMarkdown: v.welcomeMarkdown,
themeKey: v.themeKey,
@@ -1,3 +1,16 @@
export type PageTitleError = 'required' | 'maxlength' | null;
export function pageTitleError(value: string | null | undefined, maxLength = 120): PageTitleError {
const v = value ?? '';
if (v.trim().length === 0) return 'required';
if (v.length > maxLength) return 'maxlength';
return null;
}
export function normalizePageTitle(value: string): string {
return value.trim();
}
export type EventDerivedState = 'running' | 'countdown' | 'stopped' | 'unconfigured';
export function deriveEventState(start: string, end: string): EventDerivedState {