Landingpage Grid-layout erstellt, Navbar implementiert, SEO vorbereitet
This commit is contained in:
@@ -37,6 +37,9 @@
|
||||
"outputMode": "server",
|
||||
"ssr": {
|
||||
"entry": "src/server.ts"
|
||||
},
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["src/styles"]
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{
|
||||
path: '',
|
||||
renderMode: RenderMode.Prerender
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Prerender
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
];
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
@@ -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";
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
client: string;
|
||||
description: string;
|
||||
socialDescription?: string;
|
||||
image: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface SeoData {
|
||||
title: string;
|
||||
description: string;
|
||||
socialsDescription?: string;
|
||||
image?: string;
|
||||
type?: "website" | "article";
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.hero-section {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<div class="logo-container">
|
||||
<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>
|
||||
|
||||
<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">
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
@use "../../../../../styles/abstracts";
|
||||
@use "abstracts";
|
||||
|
||||
.navigation {
|
||||
display: none;
|
||||
|
||||
@include abstracts.breakpoint("md") {
|
||||
display: block;
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
&__list {
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
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;
|
||||
|
||||
&__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);
|
||||
}
|
||||
}
|
||||
&__nav-section {
|
||||
display: flex;
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
@use "styles/abstracts";
|
||||
@use "styles/base";
|
||||
@use "styles/layout";
|
||||
@use "abstracts";
|
||||
@use "base";
|
||||
@use "layout";
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@forward "tokens";
|
||||
@forward "boilerplate";
|
||||
@forward "typography";
|
||||
@forward "tokens";
|
||||
@@ -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
|
||||
@@ -38,3 +55,23 @@
|
||||
--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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+6
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user