From 3f036223f81280d259ce2c4c707a7bcd033156aa Mon Sep 17 00:00:00 2001 From: KamyCoding Date: Sun, 9 Aug 2026 21:03:48 +0200 Subject: [PATCH] feat(not-found): implement production 404 page --- src/app/pages/not-found/not-found.css | 138 ++++++++++++++++++ src/app/pages/not-found/not-found.html | 21 ++- src/app/pages/not-found/not-found.spec.ts | 42 ++++++ src/app/pages/not-found/not-found.ts | 5 +- .../project-detail/project-detail.spec.ts | 2 +- 5 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 src/app/pages/not-found/not-found.spec.ts diff --git a/src/app/pages/not-found/not-found.css b/src/app/pages/not-found/not-found.css index e69de29..da0e604 100644 --- a/src/app/pages/not-found/not-found.css +++ b/src/app/pages/not-found/not-found.css @@ -0,0 +1,138 @@ +:host { + display: flex; + min-height: 100svh; + background-color: var(--color-canvas); + color: var(--color-foreground); + flex-direction: column; +} + +.not-found { + display: grid; + min-height: min(720px, 78svh); + padding: 128px 20px 72px; + place-items: center; + text-align: center; +} + +.not-found__content { + display: flex; + width: min(680px, 100%); + flex-direction: column; + align-items: center; +} + +.not-found__code { + margin: 0 0 16px; + color: var(--color-secondary); + font-family: var(--font-hand); + font-size: clamp(28px, 8vw, 40px); + font-weight: 700; + line-height: 1; + transform: rotate(-3deg); +} + +.not-found h1 { + margin: 0; + font-size: clamp(48px, 13vw, 88px); + font-weight: 700; + letter-spacing: -0.04em; + line-height: 0.95; +} + +.not-found__message { + max-width: 500px; + margin: 24px 0 0; + color: color-mix(in srgb, var(--color-foreground) 78%, transparent); + font-size: clamp(18px, 5vw, 22px); + line-height: 1.45; +} + +.not-found__actions { + display: flex; + width: min(360px, 100%); + margin-top: 40px; + flex-direction: column; + gap: 20px; +} + +.not-found__action { + display: inline-flex; + min-height: 44px; + padding: 10px 24px; + align-items: center; + justify-content: center; + border: 1px solid currentColor; + border-radius: 100px; + color: var(--color-foreground); + font-size: 18px; + font-weight: 500; + line-height: 1.2; + text-decoration: none; + transition: + color 180ms ease-out, + background-color 180ms ease-out, + box-shadow 180ms ease-out, + transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.not-found__action--primary { + background-color: var(--color-primary); + box-shadow: 2px 4px 0 var(--color-foreground); +} + +.not-found__action--secondary { + border-color: color-mix(in srgb, var(--color-foreground) 55%, transparent); +} + +.not-found__action:focus-visible { + outline: 2px solid var(--color-secondary); + outline-offset: 4px; +} + +@media (hover: hover) and (pointer: fine) { + .not-found__action:hover { + border-color: var(--color-foreground); + background-color: var(--color-tertiary); + color: var(--color-foreground); + } + + .not-found__action--primary:hover { + box-shadow: none; + transform: translate(2px, 4px); + } +} + +.not-found__action:active { + border-color: var(--color-foreground); + background-color: var(--color-tertiary); + box-shadow: none; + transform: translate(2px, 4px); +} + +@media (min-width: 40rem) { + .not-found { + padding-inline: 32px; + } + + .not-found__actions { + width: auto; + flex-direction: row; + } + + .not-found__action { + min-width: 176px; + } +} + +@media (min-width: 64rem) { + .not-found { + min-height: 760px; + padding: 160px 64px 96px; + } +} + +@media (prefers-reduced-motion: reduce) { + .not-found__action { + transition: none; + } +} diff --git a/src/app/pages/not-found/not-found.html b/src/app/pages/not-found/not-found.html index 8071020..2db451e 100644 --- a/src/app/pages/not-found/not-found.html +++ b/src/app/pages/not-found/not-found.html @@ -1 +1,20 @@ -

not-found works!

+
+
+

Error 404

+ +

Page not found

+ +

+ The page you’re looking for doesn’t exist or may have moved. +

+ + +
+
+ + diff --git a/src/app/pages/not-found/not-found.spec.ts b/src/app/pages/not-found/not-found.spec.ts new file mode 100644 index 0000000..83d8b2f --- /dev/null +++ b/src/app/pages/not-found/not-found.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; + +import { NotFound } from './not-found'; + +describe('NotFound', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NotFound], + providers: [provideRouter([])], + }).compileComponents(); + + fixture = TestBed.createComponent(NotFound); + fixture.detectChanges(); + }); + + it('creates the page', () => { + expect(fixture.componentInstance).toBeTruthy(); + }); + + it('renders the 404 message with one primary heading', () => { + const content = fixture.nativeElement.textContent as string; + const headings = fixture.nativeElement.querySelectorAll('h1') as NodeListOf; + + expect(content).toContain('Error 404'); + expect(content).toContain('Page not found'); + expect(content).toContain('doesn\u2019t exist'); + expect(headings).toHaveLength(1); + expect(headings[0]?.textContent).toBe('Page not found'); + }); + + it('provides an Angular route back to the homepage', () => { + const homeLink = fixture.nativeElement.querySelector( + '.not-found__action--primary', + ) as HTMLAnchorElement | null; + + expect(homeLink?.getAttribute('href')).toBe('/'); + expect(homeLink?.textContent?.trim()).toBe('Back to homepage'); + }); +}); diff --git a/src/app/pages/not-found/not-found.ts b/src/app/pages/not-found/not-found.ts index f689271..cbf9f67 100644 --- a/src/app/pages/not-found/not-found.ts +++ b/src/app/pages/not-found/not-found.ts @@ -1,8 +1,11 @@ import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { Footer } from '../../layout/footer/footer'; @Component({ selector: 'app-not-found', - imports: [], + imports: [RouterLink, Footer], templateUrl: './not-found.html', styleUrl: './not-found.css', }) diff --git a/src/app/pages/project-detail/project-detail.spec.ts b/src/app/pages/project-detail/project-detail.spec.ts index 1f827e7..f646c06 100644 --- a/src/app/pages/project-detail/project-detail.spec.ts +++ b/src/app/pages/project-detail/project-detail.spec.ts @@ -59,7 +59,7 @@ describe('ProjectDetail', () => { await harness.navigateByUrl('/projects/unsupported'); expect(TestBed.inject(Router).url).toBe('/not-found'); - expect(harness.routeNativeElement?.textContent).toContain('not-found works!'); + expect(harness.routeNativeElement?.textContent).toContain('Page not found'); }); function getRouteElement(selector: string): T {