Add theme switcher component with dark and light mode support

Extracted theme toggle functionality into a new `app-theme` component to improve modularity and reusability. Integrated theme settings with a service using cookies for persistence and introduced the `THEME_MODE` enum. Updated header component to utilize the new theme switcher and removed inline implementation.
This commit is contained in:
Teriuihi 2025-04-03 22:21:21 +02:00
parent e3c9fa2165
commit dbfe5b38ab
11 changed files with 214 additions and 92 deletions

View File

@ -17,6 +17,7 @@
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
"ngx-cookie-service": "^19.1.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"

View File

@ -5,23 +5,26 @@ import {RouterModule, Routes} from '@angular/router';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import {NgOptimizedImage} from '@angular/common';
import {ThemeComponent} from "./theme/theme.component";
import {CookieService} from 'ngx-cookie-service';
const routes: Routes = [
{ path: '', component: HomeComponent }
];
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
HomeComponent
],
declarations: [
AppComponent,
HeaderComponent,
HomeComponent,
ThemeComponent
],
imports: [
BrowserModule,
RouterModule.forRoot(routes),
NgOptimizedImage,
],
providers: [],
providers: [CookieService],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -1 +1,5 @@
export const ALTITUDE_VERSION = "1.21"
export const enum THEME_MODE {
LIGHT= 'theme-light',
DARK = 'theme-dark'
}

View File

@ -126,12 +126,7 @@
</ul>
</li>
</ul>
<div class="switch-div" style="z-index: 1;">
<label class="switch">
<input type="checkbox" onchange="toggleTheme()" id="slider">
<span class="slider round"></span>
</label>
</div>
<app-theme></app-theme>
</div>
</nav>

View File

@ -251,86 +251,6 @@ nav img {
display: none;
}
/* darkmode switch start */
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 25px;
}
.switch-div{
position: absolute;
right: 40px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 37px;
width: 37px;
left: 0;
top: 0;
bottom: 0;
margin: auto 0;
-webkit-transition: 0.4s;
transition: 0.4s;
box-shadow: 0 0 15px #2020203d;
background: var(--switch) url('https://i.ibb.co/7JfqXxB/sunny.png');
background-repeat: no-repeat;
background-position: center;
}
input:checked + .slider {
background-color: var(--link-color);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--link-color);
}
input:checked + .slider:before {
-webkit-transform: translateX(24px);
-ms-transform: translateX(24px);
transform: translateX(24px);
background: var(--switch) url('https://i.ibb.co/FxzBYR9/night.png');
background-repeat: no-repeat;
background-position: center;
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/* darkmode switch end */
@-moz-document url-prefix() {
.dropdown2{
top: 12px;

View File

@ -0,0 +1,6 @@
<div class="switch-div" style="z-index: 1;">
<label class="switch">
<input type="checkbox" [checked]="isDarkMode" (change)="toggleTheme()" id="slider">
<span class="slider round"></span>
</label>
</div>

View File

@ -0,0 +1,75 @@
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 25px;
}
.switch-div{
position: absolute;
right: 40px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 37px;
width: 37px;
left: 0;
top: 0;
bottom: 0;
margin: auto 0;
-webkit-transition: 0.4s;
transition: 0.4s;
box-shadow: 0 0 15px #2020203d;
background: var(--switch) url('https://i.ibb.co/7JfqXxB/sunny.png');
background-repeat: no-repeat;
background-position: center;
}
input:checked + .slider {
background-color: var(--link-color);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--link-color);
}
input:checked + .slider:before {
-webkit-transform: translateX(24px);
-ms-transform: translateX(24px);
transform: translateX(24px);
background: var(--switch) url('https://i.ibb.co/FxzBYR9/night.png');
background-repeat: no-repeat;
background-position: center;
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}

View File

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

View File

@ -0,0 +1,35 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from 'rxjs';
import {ThemeService} from './theme.service';
import {THEME_MODE} from '../constant';
@Component({
standalone: false,
selector: 'app-theme',
templateUrl: './theme.component.html',
styleUrl: './theme.component.scss'
})
export class ThemeComponent implements OnInit, OnDestroy {
isDarkMode: boolean = false;
private themeSubscription: Subscription | null = null;
constructor(private themeService: ThemeService) {}
ngOnInit(): void {
this.themeSubscription = this.themeService.theme$.subscribe(theme => {
this.isDarkMode = theme === THEME_MODE.DARK;
});
}
toggleTheme(): void {
this.isDarkMode = this.themeService.toggleTheme() === THEME_MODE.DARK;
}
ngOnDestroy(): void {
if (this.themeSubscription) {
this.themeSubscription.unsubscribe();
}
}
}

View File

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

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { CookieService } from 'ngx-cookie-service';
import { BehaviorSubject } from 'rxjs';
import {THEME_MODE} from '../constant';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private readonly THEME_KEY = 'theme';
private themeSubject: BehaviorSubject<THEME_MODE> = new BehaviorSubject<THEME_MODE>(THEME_MODE.DARK);
public theme$ = this.themeSubject.asObservable();
constructor(private cookieService: CookieService) {
this.initializeTheme();
}
public initializeTheme(): void {
const savedTheme = this.cookieService.check(this.THEME_KEY)
? this.cookieService.get(this.THEME_KEY)
: THEME_MODE.DARK;
let currentTheme: THEME_MODE;
switch (savedTheme) {
case THEME_MODE.LIGHT: currentTheme = THEME_MODE.LIGHT; break;
case THEME_MODE.DARK:
default: currentTheme = THEME_MODE.DARK;
}
this.setTheme(currentTheme);
}
public setTheme(themeName: THEME_MODE): void {
this.cookieService.set(this.THEME_KEY, themeName, 365);
document.body.className = themeName;
this.themeSubject.next(themeName);
}
public toggleTheme(): THEME_MODE {
const currentTheme = this.themeSubject.getValue();
const newTheme = currentTheme === THEME_MODE.LIGHT ? THEME_MODE.DARK : THEME_MODE.LIGHT;
this.setTheme(newTheme);
return newTheme
}
}