Landingpage Grid-layout erstellt, Navbar implementiert, SEO vorbereitet

This commit is contained in:
2026-02-23 20:52:45 +01:00
parent b6ca9be225
commit 2fdeb41c25
40 changed files with 646 additions and 79 deletions

View File

@@ -37,6 +37,9 @@
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
},
"stylePreprocessorOptions": {
"includePaths": ["src/styles"]
}
},
"configurations": {

View File

@@ -1,6 +1,10 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '',
renderMode: RenderMode.Prerender
},
{
path: '**',
renderMode: RenderMode.Prerender

View File

@@ -1,9 +1,8 @@
import { Routes } from '@angular/router';
import { LandingpageComponent } from './features/landing/pages/landingpage.component';
export const routes: Routes = [
{
path: "",
component: LandingpageComponent
loadComponent: () => import('@features/landing/').then(m => m.LandingpageComponent)
}
];

View File

@@ -1,7 +1,7 @@
import { Component, signal } from '@angular/core';
import { Component, signal, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {provideIcons} from "@ng-icons/core";
import {cssMenu} from "@ng-icons/css.gg"
import {cssMenu} from "@ng-icons/css.gg";
@Component({
selector: 'app-root',

View File

@@ -0,0 +1,12 @@
export type NavigationType = "anchor" | "route" | "external";
export interface NavigationItem {
readonly label: string;
readonly type: NavigationType;
readonly target: string;
readonly icon?: string;
readonly children?: NavigationItem[];
}
export const isAnchor = (item: NavigationItem): boolean => item.type === "anchor";
export const isRoute = (item: NavigationItem): boolean => item.type === "route";

View File

@@ -0,0 +1,8 @@
export interface Project {
id: string;
name: string;
client: string;
description: string;
socialDescription?: string;
image: string;
}

View File

@@ -0,0 +1,7 @@
export interface SeoData {
title: string;
description: string;
socialsDescription?: string;
image?: string;
type?: "website" | "article";
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { NavigationService } from './navigation.service';
describe('NavigationService', () => {
let service: NavigationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NavigationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,74 @@
// core/services/navigation.service.ts
import { Injectable, signal, computed, inject } from '@angular/core';
import { Router } from '@angular/router';
import { NavigationItem, isAnchor, isRoute } from '../models/navigation.model';
@Injectable({ providedIn: 'root' })
export class NavigationService {
private readonly router = inject(Router);
// === STATE ===
private readonly _navigationItems = signal<NavigationItem[]>([
// Anchor-Links (Landingpage intern)
{ label: 'Home', type: 'anchor', target: 'hero', icon: 'home' },
{ label: 'Features', type: 'anchor', target: 'features-section', icon: 'star' },
{ label: 'Pricing', type: 'anchor', target: 'pricing', icon: 'tag' },
// Route-Links ( andere Pages)
{ label: 'Login', type: 'route', target: '/login', icon: 'log-in' },
{
label: 'Dashboard',
type: 'route',
target: '/dashboard',
icon: 'layout',
// Geschützte Route - wird später gefiltert
}
]);
// === PUBLIC SIGNALS ===
readonly navigationItems = this._navigationItems.asReadonly();
// Gefiltert nach Kontext (Landingpage vs. App)
readonly landingNavigation = computed(() =>
this._navigationItems().filter(item =>
isAnchor(item) || item.target === '/login'
)
);
readonly appNavigation = computed(() =>
this._navigationItems().filter(item => isRoute(item))
);
// === METHODS ===
navigate(item: NavigationItem): void {
switch (item.type) {
case 'anchor':
this.scrollToAnchor(item.target);
break;
case 'route':
this.router.navigate([item.target]);
break;
case 'external':
window.open(item.target, '_blank');
break;
}
}
private scrollToAnchor(anchorId: string): void {
const element = document.getElementById(anchorId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
// Dynamische Updates (z.B. nach Login)
addItem(item: NavigationItem): void {
this._navigationItems.update(items => [...items, item]);
}
removeItem(target: string): void {
this._navigationItems.update(items =>
items.filter(i => i.target !== target)
);
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SeoService } from './seo.service';
describe('SeoService', () => {
let service: SeoService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SeoService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,91 @@
import {
Component,
Injectable,
inject,
RendererFactory2,
ViewEncapsulation,
ChangeDetectionStrategy,
signal,
computed } from "@angular/core";
import { Title, Meta } from "@angular/platform-browser";
import { SeoData } from "@core/models/seo.model";
import { DOCUMENT } from "@angular/common";
@Injectable({providedIn: "root"})
export class SeoService {
private titleService = inject(Title);
private metaService = inject(Meta);
private document = inject(DOCUMENT);
private rendererFactory = inject(RendererFactory2);
private renderer = this.rendererFactory.createRenderer(null, null)
private readonly BRAND = "Hurler Webdesign";
updateMetadata(data: SeoData, canonicalPath: string = "") {
const fullTitle = `${data.title} | ${this.BRAND}`;
const url = `https://hurler-webdesign.de${canonicalPath}`;
const fallbackImage = 'https://hurler-webdesign.de/assets/og-default.jpg'; // Falls mal kein Bild da ist
this.titleService.setTitle(fullTitle);
this.metaService.updateTag({ name: 'description', content: data.description });
// Open Graph
this.metaService.updateTag({ property: 'og:title', content: fullTitle }); // Mit Branding
this.metaService.updateTag({ property: 'og:description', content: data.description });
this.metaService.updateTag({ property: 'og:type', content: data.type || 'website' });
this.metaService.updateTag({ property: 'og:image', content: data.image || fallbackImage });
// Twitter
this.metaService.updateTag({ name: 'twitter:card', content: 'summary_large_image' }); // Wichtig für große Bilder!
this.metaService.updateTag({ name: 'twitter:title', content: fullTitle });
this.metaService.updateTag({ name: 'twitter:description', content: data.description });
this.metaService.updateTag({ name: 'twitter:url', content: url });
this.metaService.updateTag({ name: 'twitter:image', content: data.image || fallbackImage });
this.updateCanonicalUrl(url);
this.setLocalBusinessSchema();
}
private updateCanonicalUrl(url: string) {
let link: HTMLLinkElement = this.document.querySelector("link[rel='canonical']") || this.renderer.createElement('link');
this.renderer.setAttribute(link, 'rel', 'canonical');
this.renderer.setAttribute(link, 'href', url);
if (!this.document.head.contains(link)) {
this.renderer.appendChild(this.document.head, link);
}
}
private setLocalBusinessSchema() {
const oldScript = this.document.getElementById('schema-org-data');
if (oldScript) this.renderer.removeChild(this.document.head, oldScript);
const schema = {
"@context": "https://schema.org",
"@type": "WebDesignService",
"name": this.BRAND,
"url": "https://hurler-webdesign.de",
"logo": "https://hurler-webdesign.de/assets/logo.png",
"image": "https://hurler-webdesign.de/assets/office.jpg",
"description": "Spezialist für performante Webseiten ohne CMS für kleine und mittelständische Unternehmen.",
"address": {
"@type": "PostalAddress",
"streetAddress": "Untermagerbein 30",
"addressLocality": "Mönchsdeggingen",
"postalCode": "86751",
"addressCountry": "DE"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 48.7506,
"longitude": 10.5773
},
"telephone": "+49 171 8084830",
"priceRange": "€€"
};
const script = this.renderer.createElement('script');
script.type = 'application/ld+json';
script.id = 'schema-org-data';
script.text = JSON.stringify(schema);
this.renderer.appendChild(this.document.head, script);
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ThemeService } from './theme.service';
describe('Theme', () => {
let service: ThemeService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ThemeService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,55 @@
// core/services/theme.service.ts
import { Injectable, signal, effect, computed, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
export type Theme = 'light' | 'dark' | 'system';
@Injectable({ providedIn: 'root' })
export class ThemeService {
// Privater Signal-State
private readonly _theme = signal<Theme>('system');
// Öffentlicher Readonly-Signal
readonly theme = this._theme.asReadonly();
// Abgeleiteter Wert: Tatsächlich aktives Theme (aufgelöst)
readonly effectiveTheme = computed(() => {
const current = this._theme();
if (current !== 'system') return current;
if (isPlatformBrowser(this.platformId)) {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
return 'light';
});
constructor(@Inject(PLATFORM_ID) private platformId: object) {
// Effekt: Reagiert auf Änderungen (nur im Browser)
effect(() => {
if (isPlatformBrowser(this.platformId)) {
const theme = this.effectiveTheme();
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', this._theme());
}
});
// Initialisierung
this.initializeTheme();
}
setTheme(theme: Theme): void {
this._theme.set(theme);
}
private initializeTheme(): void {
if (isPlatformBrowser(this.platformId)) {
const saved = localStorage.getItem('theme') as Theme | null;
if (saved) {
this._theme.set(saved);
}
}
}
}

View File

@@ -1 +1,16 @@
<section>hero works!</section>
<section class="hero-section">
<h1 class="hero-section__header">
<span>Digitales Handwerk</span>
<span>statt Standard-Baukasten</span>
</h1>
<p class="hero-section__claim">
Wir programmieren Blitzschnelle, sichere und maßgeschneiderte Webseiten für kleine,
mittelständische Unternehmen und Vereine. Ohne CMS-Ballast, dafür mit maximaler Performance.
</p>
<div class="hero-section__links">
<button href="#" variant="primary" label="Leistungen ansehen">Leistungen ansehen</button>
<button href="#" variant="secondary" label="Warum kein WordPress?">
Warum kein WordPress?
</button>
</div>
</section>

View File

@@ -0,0 +1,4 @@
.hero-section {
width: 100%;
max-width: 1200px;
}

View File

@@ -1,15 +1,15 @@
<div class="logo-container">
<span class="logo-container__logo centered">H</span>
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
</div>
<nav class="navigation">
<ul class="navigation__list">
<li class="navigation__list-item"><a href="#">Home</a></li>
<li class="navigation__list-item"><a href="#">Features</a></li>
<li class="navigation__list-item"><a href="#">Contact</a></li>
</ul>
</nav>
<div class="burger-menu centered">
<ng-icon name="cssMenu" class="burger-menu__icon"></ng-icon>
</div>
<section class="header">
<div class="logo-container">
<span class="logo-container__logo centered">H</span>
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
</div>
<div class="header__nav-section centered">
<div class="theme-toggle-container">
<app-toogle-theme></app-toogle-theme>
</div>
<app-nav-menu [items]="navigationService.landingNavigation()"></app-nav-menu>
<div class="burger-menu centered">
<ng-icon name="cssMenu" class="burger-menu__icon"></ng-icon>
</div>
</div>
</section>

View File

@@ -1,32 +1,19 @@
@use "../../../../../styles/abstracts";
@use "abstracts";
.navigation {
display: none;
.header {
display: flex;
justify-content: space-between;
min-height: abstracts.rem(60);
border-radius: 0 0 10px 10px;
background-color: var(--nav-bg);
backdrop-filter: var(--nav-backdrop);
box-shadow: var(--nav-shadow);
position: sticky;
top: 0;
@include abstracts.breakpoint("md") {
display: block;
padding-right: var(--space-4);
}
&__list {
&__nav-section {
display: flex;
gap: var(--space-4);
}
&__list-item {
list-style: none;
font-weight: 500;
font-size: var(--font-size-base);
color: var(--text-main);
transition: color 0.3s ease;
a {
color: var(--accent);
&:hover {
color: var(--accent-hover);
}
}
flex-direction: row;
}
}
@@ -39,14 +26,13 @@
&__logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--text-main);
color: var(--text-on-accent);
border: 1px solid var(--accent);
width: abstracts.rem(30);
height: abstracts.rem(30);
display: flex;
background-color: var(--accent);
border-radius: 5px;
color: white;
}
&__company {
@@ -60,6 +46,18 @@
}
}
.theme-toggle-container {
width: abstracts.rem(24);
height: abstracts.rem(24);
margin: auto;
margin-left: var(--space-4);
@include abstracts.breakpoint("md") {
margin: auto;
margin-left: var(--space-4);
}
}
.burger-menu {
padding-right: var(--space-4);
cursor: pointer;

View File

@@ -1,12 +1,15 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import { ToogleThemeComponent } from '@shared/utils/toogle-theme/toogle-theme.component';
import { NavMenuComponent } from '@shared/ui/nav-menu/nav-menu.component';
import { NavigationService } from '@core/services/navigation.service';
@Component({
selector: 'app-navigation',
imports: [NgIcon],
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent],
templateUrl: './navigation.component.html',
styleUrl: './navigation.component.scss',
})
export class NavigationComponent {
protected readonly navigationService = inject(NavigationService);
}

View File

@@ -0,0 +1,3 @@
// features/landing/public-api.ts (oder index.ts)
// Nur was von außen importiert werden soll
export { LandingpageComponent } from './pages/landingpage.component';

View File

@@ -1,6 +1,14 @@
<div class="landing-grid">
<app-navigation class="navigation__component"></app-navigation>
<app-hero class="hero__component"></app-hero>
<app-features-section class="features-section__component"></app-features-section>
<app-footer class="footer__component"></app-footer>
<div class="grid-area-navigation">
<app-navigation></app-navigation>
</div>
<div class="grid-area-hero" id="hero">
<app-hero></app-hero>
</div>
<div class="grid-area-features" id="features-section">
<app-features-section></app-features-section>
</div>
<div class="grid-area-footer">
<app-footer></app-footer>
</div>
</div>

View File

@@ -1,8 +1,9 @@
import { Component } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { NavigationComponent } from '../components/navigation/navigation.component';
import { HeroComponent } from '../components/hero/hero.component';
import { FeaturesSectionComponent } from '../components/features-section/features-section.component';
import { FooterComponent } from '../components/footer/footer.component';
import { SeoService } from '@core/services/seo.service';
@Component({
selector: 'app-landingpage',
@@ -15,6 +16,15 @@ import { FooterComponent } from '../components/footer/footer.component';
templateUrl: './landingpage.component.html',
styleUrl: './landingpage.component.scss',
})
export class LandingpageComponent {
export class LandingpageComponent implements OnInit {
private seo = inject(SeoService);
ngOnInit(): void {
this.seo.updateMetadata({
title: 'Performante Webseiten & Webdesign für KMU',
description: 'Spezialist für performante Webseiten ohne CMS für KMU. Wir bieten maßgeschneidertes Webdesign für eine schnelle und sichere Online-Präsenz.',
socialsDescription: 'Webseiten ohne CMS für KMU der Performance-Vorteil für Ihr Unternehmen.',
type: 'website'
});
}
}

View File

@@ -0,0 +1,27 @@
<nav class="navigation">
<ul class="navigation__list">
@for (item of items(); track item.target) {
<li class="navigation__list-item">
<!-- Anchor-Link: Smooth Scroll -->
@if (isAnchor(item)) {
<a [href]="'#' + item.target" (click)="onAnchorClick($event, item)">
@if (item.icon) {
<ng-icon [name]="item.icon"></ng-icon>
}
{{ item.label }}
</a>
}
<!-- Route-Link: Angular Router -->
@else if (isRoute(item)) {
<a [routerLink]="item.target">
@if (item.icon) {
<ng-icon [name]="item.icon"></ng-icon>
}
{{ item.label }}
</a>
}
</li>
}
</ul>
</nav>

View File

@@ -0,0 +1,36 @@
@use "abstracts";
.navigation {
display: none;
@include abstracts.breakpoint("md") {
display: block;
padding-right: var(--space-4);
}
&__list {
display: flex;
gap: var(--space-4);
}
&__list-item {
list-style: none;
font-weight: 500;
font-size: var(--font-size-base);
color: var(--text-main);
transition: color 0.3s ease;
display: flex;
align-items: center;
a {
color: var(--accent);
display: inline-flex;
align-items: center;
gap: var(--space-1);
&:hover {
color: var(--accent-hover);
}
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NavMenuComponent } from './nav-menu.component';
describe('NavMenuComponent', () => {
let component: NavMenuComponent;
let fixture: ComponentFixture<NavMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NavMenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NavMenuComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Component, inject, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { NgIcon } from '@ng-icons/core';
import { NavigationService } from '@core/services/navigation.service';
import {NavigationItem, isAnchor, isRoute} from '@core/models/navigation.model';
@Component({
selector: 'app-nav-menu',
imports: [RouterLink, NgIcon],
templateUrl: './nav-menu.component.html',
styleUrl: './nav-menu.component.scss',
})
export class NavMenuComponent {
protected readonly navigationService = inject(NavigationService);
items = input<NavigationItem[]>([]);
protected readonly isAnchor = isAnchor;
protected readonly isRoute = isRoute;
onAnchorClick(event: Event, item: NavigationItem): void {
event.preventDefault();
this.navigationService.navigate(item);
}
}

View File

@@ -0,0 +1,3 @@
<ng-icon name="cssSun" (click)="toggleTheme()" class="theme-icon-sun"></ng-icon>
<ng-icon name="cssMoon" (click)="toggleTheme()" class="theme-icon-moon"></ng-icon>

View File

@@ -0,0 +1,16 @@
.theme-icon-moon,
.theme-icon-sun {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
color: var(--accent);
}
:host-context([data-theme='dark']) .theme-icon-sun {
display: none;
}
:host-context([data-theme='light']) .theme-icon-moon {
display: none;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToogleThemeComponent } from './toogle-theme.component';
describe('ToogleThemeComponent', () => {
let component: ToogleThemeComponent;
let fixture: ComponentFixture<ToogleThemeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ToogleThemeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ToogleThemeComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
import { Component, inject } from '@angular/core';
import { ThemeService } from '@core/services/theme.service';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { cssMoon, cssSun } from '@ng-icons/css.gg';
@Component({
selector: 'app-toogle-theme',
imports: [NgIcon],
templateUrl: './toogle-theme.component.html',
styleUrl: './toogle-theme.component.scss',
viewProviders: [provideIcons({cssMoon, cssSun})]
})
export class ToogleThemeComponent {
themeService = inject(ThemeService);
toggleTheme() {
const current = this.themeService.theme();
const next = current === 'light' ? 'dark' : 'light';
this.themeService.setTheme(next);
}
}

View File

@@ -0,0 +1,11 @@
import { Project } from "@core/models/project.model";
export const MOCK_PROJECTS: Project[] = [
{
id: "schreiner-mueller",
name: "Schreiner Müller",
client: "Schreiner Müller GmbH",
description: "Ein modernes Webdesign für die Schreiner Müller GmbH, das die Handwerkskunst und Qualität ihrer Dienstleistungen hervorhebt. Die Website bietet eine benutzerfreundliche Navigation, ansprechende Bildergalerien und detaillierte Informationen zu ihren maßgeschneiderten Schreinerarbeiten.",
image: "assets/projects/schreiner-mueller.jpg"
}
]

View File

@@ -1,3 +1,3 @@
@use "styles/abstracts";
@use "styles/base";
@use "styles/layout";
@use "abstracts";
@use "base";
@use "layout";

View File

@@ -1,12 +1,18 @@
@use "tokens";
html {
font-size: 100%;
box-sizing: border-box;
scroll-padding-top: 60px;
scroll-behavior: smooth;
}
*,
*::before,
*::after {
box-sizing: inherit;
padding: 0;
margin: 0;
}
body {

View File

@@ -1,3 +1,3 @@
@forward "tokens";
@forward "boilerplate";
@forward "typography";
@forward "tokens";
@forward "typography";

View File

@@ -1,3 +1,17 @@
@use "abstracts";
// ==============================
// Tier 1: Primitives
// ==============================
:root {
// Sie haben diese bereits als OKLCH
--color-white: oklch(100% 0 0);
--color-black: oklch(0% 0 0);
}
// ==============================
// Tier 2: Semantic
// ==============================
:root {
// Farben
--brand-vue: 250;
@@ -13,6 +27,9 @@
--border: oklch(90% 0.02 250);
--shadow-color: oklch(0% 0 250);
// Stapelbare Werte
--z-index-sticky: 100;
// Skalierung (modulare Skala)
--space-unit: 0.25rem;
--space-1: calc(var(--space-unit) * 1); // 4px
@@ -37,4 +54,24 @@
--border: oklch(25% 0.02 250);
--shadow-color: 0 0% 100%;
}
// ==============================
// Tier 3: Component-Specific
// ==============================
:root {
--text-on-accent: var(--color-white);
// Navigation
--nav-shadow: 1px 2px 10px oklch(80% 0 250 / 0.1);
--nav-backdrop: blur(10px);
--nav-bg: oklch(100% 0.00011 271.152 / 0.05);
--nav-height: abstracts.rem(60);
// Dark Mode Overrides
[data-theme="dark"] {
// Navigation
--nav-shadow: 1px 2px 10px oklch(0% 0 250 / 0.5);
--nav-bg: oklch(20% 0.02 250 / 0.8);
}
}

View File

@@ -1,41 +1,35 @@
@use "../abstracts";
@use "abstracts";
.landing-grid {
display: grid;
grid-template-columns: 1fr minmax(auto, 1200px) 1fr;
grid-template-areas:
"navigation navigation navigation"
". hero ."
"hero hero hero"
". features-section ."
"footer footer footer";
}
.navigation__component {
.grid-area-navigation {
grid-area: navigation;
display: flex;
justify-content: space-between;
border-radius: 0 0 10px 10px;
backdrop-filter: blur(10px);
box-shadow: 1px 2px 10px rgba(197, 197, 197, 0.824);
position: sticky;
top: 0;
z-index: var(--z-index-sticky);
}
.hero__component {
.grid-area-hero {
grid-area: hero;
height: 80vh;
padding: var(--space-4);
height: 100vh;
@include abstracts.breakpoint("md") {
padding: 0;
}
display: flex;
justify-content: center;
}
.features-section__component {
.grid-area-features {
grid-area: features-section;
height: 80vh;
}
.footer__component {
.grid-area-footer {
grid-area: footer;
}

View File

@@ -13,7 +13,12 @@
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
"module": "preserve",
"paths": {
"@core/*": ["./src/app/core/*"],
"@features/*": ["./src/app/features/*"],
"@shared/*": ["./src/app/shared/*"]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,