diff --git a/angular.json b/angular.json index 317e969..eab6e67 100644 --- a/angular.json +++ b/angular.json @@ -37,6 +37,9 @@ "outputMode": "server", "ssr": { "entry": "src/server.ts" + }, + "stylePreprocessorOptions": { + "includePaths": ["src/styles"] } }, "configurations": { diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index ffd37b1..4c5dd72 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -1,6 +1,10 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ + { + path: '', + renderMode: RenderMode.Prerender + }, { path: '**', renderMode: RenderMode.Prerender diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index a18e20a..35b6109 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -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) } ]; diff --git a/src/app/app.ts b/src/app/app.ts index 9fea51d..4674f1e 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -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', diff --git a/src/app/core/models/navigation.model.ts b/src/app/core/models/navigation.model.ts new file mode 100644 index 0000000..fbcad24 --- /dev/null +++ b/src/app/core/models/navigation.model.ts @@ -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"; \ No newline at end of file diff --git a/src/app/core/models/project.model.ts b/src/app/core/models/project.model.ts new file mode 100644 index 0000000..4222e94 --- /dev/null +++ b/src/app/core/models/project.model.ts @@ -0,0 +1,8 @@ +export interface Project { + id: string; + name: string; + client: string; + description: string; + socialDescription?: string; + image: string; +} \ No newline at end of file diff --git a/src/app/core/models/seo.model.ts b/src/app/core/models/seo.model.ts new file mode 100644 index 0000000..4a6ddb2 --- /dev/null +++ b/src/app/core/models/seo.model.ts @@ -0,0 +1,7 @@ +export interface SeoData { + title: string; + description: string; + socialsDescription?: string; + image?: string; + type?: "website" | "article"; +} \ No newline at end of file diff --git a/src/app/core/services/navigation.service.spec.ts b/src/app/core/services/navigation.service.spec.ts new file mode 100644 index 0000000..3faeea3 --- /dev/null +++ b/src/app/core/services/navigation.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/core/services/navigation.service.ts b/src/app/core/services/navigation.service.ts new file mode 100644 index 0000000..5aa7c68 --- /dev/null +++ b/src/app/core/services/navigation.service.ts @@ -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([ + // 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) + ); + } +} \ No newline at end of file diff --git a/src/app/core/services/seo.service.spec.ts b/src/app/core/services/seo.service.spec.ts new file mode 100644 index 0000000..9f1cff7 --- /dev/null +++ b/src/app/core/services/seo.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/core/services/seo.service.ts b/src/app/core/services/seo.service.ts new file mode 100644 index 0000000..89c4c2a --- /dev/null +++ b/src/app/core/services/seo.service.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/app/core/services/theme.service.spec.ts b/src/app/core/services/theme.service.spec.ts new file mode 100644 index 0000000..6c27591 --- /dev/null +++ b/src/app/core/services/theme.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/core/services/theme.service.ts b/src/app/core/services/theme.service.ts new file mode 100644 index 0000000..d758ed0 --- /dev/null +++ b/src/app/core/services/theme.service.ts @@ -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('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); + } + } + } +} \ No newline at end of file diff --git a/src/app/features/landing/components/hero/hero.component.html b/src/app/features/landing/components/hero/hero.component.html index a2d6cef..5627005 100644 --- a/src/app/features/landing/components/hero/hero.component.html +++ b/src/app/features/landing/components/hero/hero.component.html @@ -1 +1,16 @@ -
hero works!
+
+

+ Digitales Handwerk + statt Standard-Baukasten +

+

+ Wir programmieren Blitzschnelle, sichere und maßgeschneiderte Webseiten für kleine, + mittelständische Unternehmen und Vereine. Ohne CMS-Ballast, dafür mit maximaler Performance. +

+ +
diff --git a/src/app/features/landing/components/hero/hero.component.scss b/src/app/features/landing/components/hero/hero.component.scss index e69de29..9bac757 100644 --- a/src/app/features/landing/components/hero/hero.component.scss +++ b/src/app/features/landing/components/hero/hero.component.scss @@ -0,0 +1,4 @@ +.hero-section { + width: 100%; + max-width: 1200px; +} \ No newline at end of file diff --git a/src/app/features/landing/components/navigation/navigation.component.html b/src/app/features/landing/components/navigation/navigation.component.html index 31382d5..a37c45c 100644 --- a/src/app/features/landing/components/navigation/navigation.component.html +++ b/src/app/features/landing/components/navigation/navigation.component.html @@ -1,15 +1,15 @@ -
- -

Hurler Webdesign

-
- - -
- -
+
+
+ +

Hurler Webdesign

+
+
+
+ +
+ +
+ +
+
+
diff --git a/src/app/features/landing/components/navigation/navigation.component.scss b/src/app/features/landing/components/navigation/navigation.component.scss index 28103c0..587fd24 100644 --- a/src/app/features/landing/components/navigation/navigation.component.scss +++ b/src/app/features/landing/components/navigation/navigation.component.scss @@ -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; diff --git a/src/app/features/landing/components/navigation/navigation.component.ts b/src/app/features/landing/components/navigation/navigation.component.ts index a7279f3..9f7d4ca 100644 --- a/src/app/features/landing/components/navigation/navigation.component.ts +++ b/src/app/features/landing/components/navigation/navigation.component.ts @@ -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); } diff --git a/src/app/features/landing/index.ts b/src/app/features/landing/index.ts new file mode 100644 index 0000000..7c3ce43 --- /dev/null +++ b/src/app/features/landing/index.ts @@ -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'; diff --git a/src/app/features/landing/pages/landingpage.component.html b/src/app/features/landing/pages/landingpage.component.html index bd6d444..fb9a4bd 100644 --- a/src/app/features/landing/pages/landingpage.component.html +++ b/src/app/features/landing/pages/landingpage.component.html @@ -1,6 +1,14 @@
- - - - +
+ +
+
+ +
+
+ +
+
diff --git a/src/app/features/landing/pages/landingpage.component.ts b/src/app/features/landing/pages/landingpage.component.ts index bb1ae1e..3070ee4 100644 --- a/src/app/features/landing/pages/landingpage.component.ts +++ b/src/app/features/landing/pages/landingpage.component.ts @@ -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' + }); + } } diff --git a/src/app/features/shared/ui/button/button.component.html b/src/app/shared/ui/button/button.component.html similarity index 100% rename from src/app/features/shared/ui/button/button.component.html rename to src/app/shared/ui/button/button.component.html diff --git a/src/app/features/shared/ui/button/button.component.scss b/src/app/shared/ui/button/button.component.scss similarity index 100% rename from src/app/features/shared/ui/button/button.component.scss rename to src/app/shared/ui/button/button.component.scss diff --git a/src/app/features/shared/ui/button/button.component.spec.ts b/src/app/shared/ui/button/button.component.spec.ts similarity index 100% rename from src/app/features/shared/ui/button/button.component.spec.ts rename to src/app/shared/ui/button/button.component.spec.ts diff --git a/src/app/features/shared/ui/button/button.component.ts b/src/app/shared/ui/button/button.component.ts similarity index 100% rename from src/app/features/shared/ui/button/button.component.ts rename to src/app/shared/ui/button/button.component.ts diff --git a/src/app/shared/ui/nav-menu/nav-menu.component.html b/src/app/shared/ui/nav-menu/nav-menu.component.html new file mode 100644 index 0000000..00bf826 --- /dev/null +++ b/src/app/shared/ui/nav-menu/nav-menu.component.html @@ -0,0 +1,27 @@ + diff --git a/src/app/shared/ui/nav-menu/nav-menu.component.scss b/src/app/shared/ui/nav-menu/nav-menu.component.scss new file mode 100644 index 0000000..5abc6ab --- /dev/null +++ b/src/app/shared/ui/nav-menu/nav-menu.component.scss @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/app/shared/ui/nav-menu/nav-menu.component.spec.ts b/src/app/shared/ui/nav-menu/nav-menu.component.spec.ts new file mode 100644 index 0000000..2d9db8e --- /dev/null +++ b/src/app/shared/ui/nav-menu/nav-menu.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NavMenuComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NavMenuComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/ui/nav-menu/nav-menu.component.ts b/src/app/shared/ui/nav-menu/nav-menu.component.ts new file mode 100644 index 0000000..f258d67 --- /dev/null +++ b/src/app/shared/ui/nav-menu/nav-menu.component.ts @@ -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([]); + + protected readonly isAnchor = isAnchor; + protected readonly isRoute = isRoute; + + onAnchorClick(event: Event, item: NavigationItem): void { + event.preventDefault(); + this.navigationService.navigate(item); + } +} \ No newline at end of file diff --git a/src/app/shared/utils/toogle-theme/toogle-theme.component.html b/src/app/shared/utils/toogle-theme/toogle-theme.component.html new file mode 100644 index 0000000..c8af6cb --- /dev/null +++ b/src/app/shared/utils/toogle-theme/toogle-theme.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/shared/utils/toogle-theme/toogle-theme.component.scss b/src/app/shared/utils/toogle-theme/toogle-theme.component.scss new file mode 100644 index 0000000..4757da1 --- /dev/null +++ b/src/app/shared/utils/toogle-theme/toogle-theme.component.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/shared/utils/toogle-theme/toogle-theme.component.spec.ts b/src/app/shared/utils/toogle-theme/toogle-theme.component.spec.ts new file mode 100644 index 0000000..b956480 --- /dev/null +++ b/src/app/shared/utils/toogle-theme/toogle-theme.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToogleThemeComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToogleThemeComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/utils/toogle-theme/toogle-theme.component.ts b/src/app/shared/utils/toogle-theme/toogle-theme.component.ts new file mode 100644 index 0000000..f050013 --- /dev/null +++ b/src/app/shared/utils/toogle-theme/toogle-theme.component.ts @@ -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); + } +} diff --git a/src/environments/mock-projects.ts b/src/environments/mock-projects.ts new file mode 100644 index 0000000..bc77a0e --- /dev/null +++ b/src/environments/mock-projects.ts @@ -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" + } +] \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index 52513f2..67b7c38 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,3 +1,3 @@ -@use "styles/abstracts"; -@use "styles/base"; -@use "styles/layout"; \ No newline at end of file +@use "abstracts"; +@use "base"; +@use "layout"; \ No newline at end of file diff --git a/src/styles/base/_boilerplate.scss b/src/styles/base/_boilerplate.scss index 733fa3f..ee5e659 100644 --- a/src/styles/base/_boilerplate.scss +++ b/src/styles/base/_boilerplate.scss @@ -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 { diff --git a/src/styles/base/_index.scss b/src/styles/base/_index.scss index 5e12de5..786a050 100644 --- a/src/styles/base/_index.scss +++ b/src/styles/base/_index.scss @@ -1,3 +1,3 @@ +@forward "tokens"; @forward "boilerplate"; -@forward "typography"; -@forward "tokens"; \ No newline at end of file +@forward "typography"; \ No newline at end of file diff --git a/src/styles/base/_tokens.scss b/src/styles/base/_tokens.scss index 5f7b03b..f4d8a22 100644 --- a/src/styles/base/_tokens.scss +++ b/src/styles/base/_tokens.scss @@ -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); + } } \ No newline at end of file diff --git a/src/styles/layout/_grid.scss b/src/styles/layout/_grid.scss index 511ebb3..cebf4af 100644 --- a/src/styles/layout/_grid.scss +++ b/src/styles/layout/_grid.scss @@ -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; } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2ab7442..56b97e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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,