Skip to content

Commit

Permalink
Add theme switcher to header (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
FelixTJDietrich authored Aug 11, 2024
1 parent 72d08f0 commit a48602d
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 8 deletions.
6 changes: 4 additions & 2 deletions webapp/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -54,8 +55,9 @@ const preview: Preview = {
light: '',
dark: 'dark bg-background',
},
defaultTheme: 'light',
defaultTheme: 'dark',
}),
applicationConfig(appConfig),
],
};

Expand Down
10 changes: 7 additions & 3 deletions webapp/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<main>
<header>
<p>Header</p>
<main class="container">
<header class="flex items-center justify-between py-4">
<a class="flex gap-2 items-center hover:text-muted-foreground" href="/">
<lucide-angular name="hammer" class="size-6" />
<span class="text-xl font-semibold">Hephaestus</span>
</a>
<app-theme-switcher />
</header>
<div class="flex gap-2 m-2 flex-col items-start">
<app-counter title="First Counter" />
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
})
Expand Down
6 changes: 4 additions & 2 deletions webapp/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 }))
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<app-button (click)="toggleTheme()" variant="outline" size="icon">
<div [@iconTrigger]="this.themeSwitcherService.currentTheme() ?? 'dark'">
<lucide-angular [name]="this.themeSwitcherService.currentTheme() === 'dark' ? 'sun' : 'moon'" class="size-5" />
</div>
</app-button>
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
71 changes: 71 additions & 0 deletions webapp/src/app/components/theme-switcher/theme-switcher.service.ts
Original file line number Diff line number Diff line change
@@ -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<AppTheme | undefined>(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);
}
}
}
26 changes: 26 additions & 0 deletions webapp/src/app/components/theme-switcher/theme-switcher.stories.ts
Original file line number Diff line number Diff line change
@@ -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<ThemeSwitcherComponent> = {
title: 'Components/ThemeSwitcher',
component: ThemeSwitcherComponent,
tags: ['autodocs'],
decorators: [
moduleMetadata({
imports: [LucideAngularModule.pick({ Sun, Moon }), BrowserAnimationsModule]
})
]
};

export default meta;
type Story = StoryObj<ThemeSwitcherComponent>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Default: Story = {
render: () => ({
template: `<app-theme-switcher />`
})
};
14 changes: 14 additions & 0 deletions webapp/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<script>
const theme = localStorage.getItem('theme');
const browserDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (theme !== null) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
} else {
if (browserDark) {
document.documentElement.classList.add('dark');
}
localStorage.setItem('theme', browserDark ? 'dark' : 'light');
}
</script>
<body>
<app-root></app-root>
</body>
Expand Down

0 comments on commit a48602d

Please sign in to comment.