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:
parent
e3c9fa2165
commit
dbfe5b38ab
|
|
@ -17,6 +17,7 @@
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^19.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^19.2.0",
|
||||||
|
"ngx-cookie-service": "^19.1.2",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,26 @@ import {RouterModule, Routes} from '@angular/router';
|
||||||
import { HeaderComponent } from './header/header.component';
|
import { HeaderComponent } from './header/header.component';
|
||||||
import { HomeComponent } from './home/home.component';
|
import { HomeComponent } from './home/home.component';
|
||||||
import {NgOptimizedImage} from '@angular/common';
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
import {ThemeComponent} from "./theme/theme.component";
|
||||||
|
import {CookieService} from 'ngx-cookie-service';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: HomeComponent }
|
{ path: '', component: HomeComponent }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
HomeComponent
|
HomeComponent,
|
||||||
],
|
ThemeComponent
|
||||||
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
RouterModule.forRoot(routes),
|
RouterModule.forRoot(routes),
|
||||||
NgOptimizedImage,
|
NgOptimizedImage,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [CookieService],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
export const ALTITUDE_VERSION = "1.21"
|
export const ALTITUDE_VERSION = "1.21"
|
||||||
|
export const enum THEME_MODE {
|
||||||
|
LIGHT= 'theme-light',
|
||||||
|
DARK = 'theme-dark'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,12 +126,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="switch-div" style="z-index: 1;">
|
<app-theme></app-theme>
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" onchange="toggleTheme()" id="slider">
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,86 +251,6 @@ nav img {
|
||||||
display: none;
|
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() {
|
@-moz-document url-prefix() {
|
||||||
.dropdown2{
|
.dropdown2{
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
|
|
||||||
6
frontend/src/app/theme/theme.component.html
Normal file
6
frontend/src/app/theme/theme.component.html
Normal 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>
|
||||||
75
frontend/src/app/theme/theme.component.scss
Normal file
75
frontend/src/app/theme/theme.component.scss
Normal 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%;
|
||||||
|
}
|
||||||
23
frontend/src/app/theme/theme.component.spec.ts
Normal file
23
frontend/src/app/theme/theme.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
35
frontend/src/app/theme/theme.component.ts
Normal file
35
frontend/src/app/theme/theme.component.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
16
frontend/src/app/theme/theme.service.spec.ts
Normal file
16
frontend/src/app/theme/theme.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
44
frontend/src/app/theme/theme.service.ts
Normal file
44
frontend/src/app/theme/theme.service.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user