diff --git a/webapp/.storybook/preview.ts b/webapp/.storybook/preview.ts index bd976444..f7573730 100644 --- a/webapp/.storybook/preview.ts +++ b/webapp/.storybook/preview.ts @@ -1,8 +1,9 @@ -import { Preview } from '@storybook/angular'; +import { applicationConfig, Preview } from '@storybook/angular'; import { withThemeByClassName } from '@storybook/addon-themes'; import { DocsContainer } from '@storybook/blocks'; import { createElement } from 'react'; import { themes } from '@storybook/core/theming'; +import { appConfig } from 'app/app.config'; const preview: Preview = { parameters: { @@ -54,8 +55,9 @@ const preview: Preview = { light: '', dark: 'dark bg-background', }, - defaultTheme: 'light', + defaultTheme: 'dark', }), + applicationConfig(appConfig), ], }; diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html index 460bba46..9b87933d 100644 --- a/webapp/src/app/app.component.html +++ b/webapp/src/app/app.component.html @@ -1,6 +1,10 @@ -
-
-

Header

+
+
+ + + Hephaestus + +
diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index 513811ab..b75100bd 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -2,11 +2,13 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { CounterComponent } from './example/counter/counter.component'; import { HelloComponent } from './example/hello/hello.component'; +import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switcher.component'; +import { LucideAngularModule } from 'lucide-angular'; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, CounterComponent, HelloComponent], + imports: [RouterOutlet, CounterComponent, HelloComponent, LucideAngularModule, ThemeSwitcherComponent], templateUrl: './app.component.html', styles: [] }) diff --git a/webapp/src/app/app.config.ts b/webapp/src/app/app.config.ts index c3b646d2..515151a9 100644 --- a/webapp/src/app/app.config.ts +++ b/webapp/src/app/app.config.ts @@ -1,8 +1,9 @@ import { ApplicationConfig, importProvidersFrom, provideExperimentalZonelessChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideAngularQuery, QueryClient } from '@tanstack/angular-query-experimental'; -import { LucideAngularModule, Home } from 'lucide-angular'; +import { LucideAngularModule, Home, Sun, Moon, Hammer } from 'lucide-angular'; import { routes } from './app.routes'; import { BASE_PATH } from './core/modules/openapi'; @@ -13,6 +14,7 @@ export const appConfig: ApplicationConfig = { provideAngularQuery(new QueryClient()), { provide: BASE_PATH, useValue: 'http://localhost:8080' }, provideHttpClient(withInterceptorsFromDi()), - importProvidersFrom(LucideAngularModule.pick({ Home })) + provideAnimationsAsync(), + importProvidersFrom(LucideAngularModule.pick({ Home, Sun, Moon, Hammer })) ] }; diff --git a/webapp/src/app/components/theme-switcher/theme-switcher.component.html b/webapp/src/app/components/theme-switcher/theme-switcher.component.html new file mode 100644 index 00000000..f2ca7226 --- /dev/null +++ b/webapp/src/app/components/theme-switcher/theme-switcher.component.html @@ -0,0 +1,5 @@ + +
+ +
+
diff --git a/webapp/src/app/components/theme-switcher/theme-switcher.component.ts b/webapp/src/app/components/theme-switcher/theme-switcher.component.ts new file mode 100644 index 00000000..86e285b3 --- /dev/null +++ b/webapp/src/app/components/theme-switcher/theme-switcher.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { LucideAngularModule } from 'lucide-angular'; +import { ButtonComponent } from 'app/ui/button/button.component'; +import { AppTheme, ThemeSwitcherService } from './theme-switcher.service'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +@Component({ + selector: 'app-theme-switcher', + standalone: true, + imports: [ButtonComponent, LucideAngularModule], + templateUrl: './theme-switcher.component.html', + animations: [ + trigger('iconTrigger', [ + state('*', style({ transform: 'rotate(0deg)' })), + transition('light => dark', animate('0.25s ease-out', style({ transform: 'rotate(90deg)' }))), + transition('dark => light', animate('0.25s ease-out', style({ transform: 'rotate(360deg)' }))) + ]) + ] +}) +export class ThemeSwitcherComponent { + themeSwitcherService = inject(ThemeSwitcherService); + + toggleTheme() { + if (this.themeSwitcherService.currentTheme() === AppTheme.DARK) { + this.themeSwitcherService.setLightTheme(); + } else { + this.themeSwitcherService.setDarkTheme(); + } + } +} diff --git a/webapp/src/app/components/theme-switcher/theme-switcher.service.ts b/webapp/src/app/components/theme-switcher/theme-switcher.service.ts new file mode 100644 index 00000000..40d58eb2 --- /dev/null +++ b/webapp/src/app/components/theme-switcher/theme-switcher.service.ts @@ -0,0 +1,71 @@ +import { Injectable, signal } from '@angular/core'; + +export enum AppTheme { + LIGHT = 'light', + DARK = 'dark' +} + +const IS_CLIENT_RENDER = typeof localStorage !== 'undefined'; +const LOCAL_STORAGE_THEME_KEY = 'theme'; + +let selectedTheme: AppTheme | undefined = undefined; + +if (IS_CLIENT_RENDER) { + selectedTheme = (localStorage.getItem(LOCAL_STORAGE_THEME_KEY) as AppTheme) || undefined; +} + +@Injectable({ + providedIn: 'root' +}) +export class ThemeSwitcherService { + currentTheme = signal(selectedTheme); + + setLightTheme() { + this.currentTheme.set(AppTheme.LIGHT); + this.setToLocalStorage(AppTheme.LIGHT); + this.removeClassFromHtml('dark'); + } + + setDarkTheme() { + this.currentTheme.set(AppTheme.DARK); + this.setToLocalStorage(AppTheme.DARK); + this.addClassToHtml('dark'); + } + + setSystemTheme() { + this.currentTheme.set(undefined); + this.removeFromLocalStorage(); + + const isSystemDark = window?.matchMedia('(prefers-color-scheme: dark)').matches ?? false; + if (isSystemDark) { + this.addClassToHtml('dark'); + } else { + this.removeClassFromHtml('dark'); + } + } + + private addClassToHtml(className: string) { + if (IS_CLIENT_RENDER) { + this.removeClassFromHtml(className); + document.documentElement.classList.add(className); + } + } + + private removeClassFromHtml(className: string) { + if (IS_CLIENT_RENDER) { + document.documentElement.classList.remove(className); + } + } + + private setToLocalStorage(theme: AppTheme) { + if (IS_CLIENT_RENDER) { + localStorage.setItem(LOCAL_STORAGE_THEME_KEY, theme); + } + } + + private removeFromLocalStorage() { + if (IS_CLIENT_RENDER) { + localStorage.removeItem(LOCAL_STORAGE_THEME_KEY); + } + } +} diff --git a/webapp/src/app/components/theme-switcher/theme-switcher.stories.ts b/webapp/src/app/components/theme-switcher/theme-switcher.stories.ts new file mode 100644 index 00000000..527dde1c --- /dev/null +++ b/webapp/src/app/components/theme-switcher/theme-switcher.stories.ts @@ -0,0 +1,26 @@ +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { LucideAngularModule, Sun, Moon } from 'lucide-angular'; +import { ThemeSwitcherComponent } from './theme-switcher.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories +const meta: Meta = { + title: 'Components/ThemeSwitcher', + component: ThemeSwitcherComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [LucideAngularModule.pick({ Sun, Moon }), BrowserAnimationsModule] + }) + ] +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Default: Story = { + render: () => ({ + template: `` + }) +}; diff --git a/webapp/src/index.html b/webapp/src/index.html index 755019f9..efa75a11 100644 --- a/webapp/src/index.html +++ b/webapp/src/index.html @@ -7,6 +7,20 @@ +