Skip to content

Commit 49ce259

Browse files
committed
✨ add theme switcher menu
1 parent 903c14c commit 49ce259

File tree

7 files changed

+174
-23
lines changed

7 files changed

+174
-23
lines changed
Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,55 @@
1-
import { Component } from '@angular/core';
1+
import { Component, OnInit } from '@angular/core';
22

33
@Component({
44
selector: 'admin-root',
55
template: '<router-outlet></router-outlet>',
66
})
7-
export class AppComponent {
8-
title = 'Admin Cpanel';
7+
export class AppComponent implements OnInit {
8+
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
9+
10+
updateTheme(savedTheme: string | null = null): string {
11+
let theme = 'system'
12+
try {
13+
if (!savedTheme) {
14+
savedTheme = window.localStorage.getItem('theme')
15+
}
16+
if (savedTheme === 'dark') {
17+
theme = 'dark'
18+
document.documentElement.classList.add('dark')
19+
} else if (savedTheme === 'light') {
20+
theme = 'light'
21+
document.documentElement.classList.remove('dark')
22+
} else if (this.mediaQuery.matches) {
23+
document.documentElement.classList.add('dark')
24+
} else {
25+
document.documentElement.classList.remove('dark')
26+
}
27+
} catch {
28+
theme = 'light'
29+
document.documentElement.classList.remove('dark')
30+
}
31+
return theme
32+
}
33+
34+
updateThemeWithoutTransitions(savedTheme: string | null = null): void {
35+
this.updateTheme(savedTheme)
36+
document.documentElement.classList.add('[&_*]:!transition-none')
37+
window.setTimeout(() => {
38+
document.documentElement.classList.remove('[&_*]:!transition-none')
39+
}, 0)
40+
}
41+
42+
ngOnInit(): void {
43+
document.documentElement.setAttribute('data-theme', this.updateTheme())
44+
45+
new MutationObserver(([{ oldValue }]) => {
46+
let newValue = document.documentElement.getAttribute('data-theme')!
47+
if (newValue !== oldValue) {
48+
try {
49+
window.localStorage.setItem('theme', newValue)
50+
} catch {}
51+
this.updateThemeWithoutTransitions(newValue)
52+
}
53+
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
54+
}
955
}

src/app/shared/themes/components/header/header.component.html

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="flex">
33
<button
44
type="button"
5-
class="px-4 border-r text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 md:hidden"
5+
class="px-4 border-r text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 md:hidden dark:border-slate-700"
66
(click)="openSidebar()"
77
>
88
<span class="sr-only">Open sidebar</span>
@@ -15,7 +15,7 @@
1515
<logo-svg class="w-24 h-auto text-slate-900 sm:w-28 dark:text-white"></logo-svg>
1616

1717
<!-- SearchBar Component -->
18-
<div class="w-full max-w-xs ml-12">
18+
<div class="hidden w-full max-w-xs ml-12 md:block">
1919
<label for="search" class="sr-only">Recherche rapide</label>
2020
<div class="relative flex items-center">
2121
<input type="text" name="search" id="search" placeholder="Rechercher" class="block w-full pr-12 bg-white border-gray-300 rounded-md shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:bg-gray-900 dark:border-gray-700 dark:text-white">
@@ -26,7 +26,7 @@
2626
</div>
2727
</div>
2828

29-
<div class="flex items-center lg:divide-x lg:divide-slate-200 dark:lg:divide-slate-700">
29+
<div class="flex items-center lg:divide-x lg:divide-slate-200 lg:dark:divide-slate-700">
3030
<a
3131
href="https://github.com/laravelcm/angular-admin-panel"
3232
target="_blank"
@@ -47,6 +47,7 @@
4747
Vous rencontrez un problème ?
4848
</a>
4949
<div class="flex items-center pl-4">
50+
<!-- SearchBar Button -->
5051
<button
5152
type="button"
5253
class="inline-flex items-center p-1 text-sm leading-5 rounded-full hover:bg-gray-50 text-slate-500 hover:text-slate-900 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-white focus:outline-none md:hidden">
@@ -63,6 +64,7 @@
6364
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
6465
</svg>
6566
</button>
67+
<!-- Notification Icon -->
6668
<button
6769
type="button"
6870
class="inline-flex items-center p-1 ml-3 text-sm leading-5 rounded-full hover:bg-gray-50 text-slate-500 hover:text-slate-900 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-white focus:outline-none md:ml-0">
@@ -79,6 +81,77 @@
7981
d="M9.354 21c.705.622 1.632 1 2.646 1s1.94-.378 2.646-1M18 8A6 6 0 1 0 6 8c0 3.09-.78 5.206-1.65 6.605-.735 1.18-1.102 1.771-1.089 1.936.015.182.054.252.2.36.133.099.732.099 1.928.099H18.61c1.196 0 1.795 0 1.927-.098.147-.11.186-.179.2-.361.014-.165-.353-.755-1.088-1.936C18.78 13.206 18 11.09 18 8Z" />
8082
</svg>
8183
</button>
84+
<!-- Theme Switcher -->
85+
<div class="relative ml-3">
86+
<label class="sr-only" id="label-system">Theme</label>
87+
<button (click)="showDialog =! showDialog" type="button" class="flex items-center justify-center w-8 h-8 rounded-full shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
88+
<span class="sr-only">{{ currentTheme }}</span>
89+
<svg class="hidden h-4 w-4 fill-green-400 [[data-theme=light]_&]:block" aria-hidden="true" viewBox="0 0 16 16">
90+
<path
91+
fill-rule="evenodd"
92+
clip-rule="evenodd"
93+
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
94+
/>
95+
</svg>
96+
<svg class="hidden h-4 w-4 fill-slate-400 [:not(.dark)[data-theme=system]_&]:block" aria-hidden="true" viewBox="0 0 16 16">
97+
<path
98+
fill-rule="evenodd"
99+
clip-rule="evenodd"
100+
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
101+
/>
102+
</svg>
103+
<svg class="hidden h-4 w-4 fill-green-400 [[data-theme=dark]_&]:block" aria-hidden="true" viewBox="0 0 16 16">
104+
<path
105+
fill-rule="evenodd"
106+
clip-rule="evenodd"
107+
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
108+
/>
109+
</svg>
110+
<svg class="hidden h-4 w-4 fill-slate-400 [.dark[data-theme=system]_&]:block" aria-hidden="true" viewBox="0 0 16 16">
111+
<path
112+
fill-rule="evenodd"
113+
clip-rule="evenodd"
114+
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
115+
/>
116+
</svg>
117+
</button>
118+
<ul
119+
*ngIf="showDialog"
120+
class="absolute z-50 p-3 mt-3 space-y-1 text-sm font-medium -translate-x-1/2 bg-white shadow-md top-full left-1/2 w-36 rounded-xl shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5"
121+
aria-labelledby="tailwind-list-label"
122+
aria-orientation="vertical"
123+
id="tailwind-listbox-options"
124+
role="listbox"
125+
>
126+
<li *ngFor="let theme of themes" class="flex cursor-pointer select-none items-center rounded-[0.625rem] p-1 hover:bg-slate-100 dark:hover:bg-slate-900/40">
127+
<button (click)="updateTheme(theme.value)" class="p-1 bg-white rounded-md shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
128+
<svg class="w-4 h-4" [ngClass]="theme.value === currentTheme ? 'fill-green-400 dark:fill-green-400': 'fill-slate-400'" aria-hidden="true" viewBox="0 0 16 16">
129+
<path
130+
*ngIf="theme.value === 'light'"
131+
fill-rule="evenodd"
132+
clip-rule="evenodd"
133+
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
134+
/>
135+
<path
136+
*ngIf="theme.value === 'dark'"
137+
fill-rule="evenodd"
138+
clip-rule="evenodd"
139+
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
140+
/>
141+
<path
142+
*ngIf="theme.value === 'system'"
143+
fill-rule="evenodd"
144+
clip-rule="evenodd"
145+
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
146+
/>
147+
</svg>
148+
</button>
149+
<div [ngClass]="theme.value === currentTheme ? 'text-green-500': 'text-slate-700 dark:text-slate-400'" class="ml-3">{{ theme.name }}</div>
150+
</li>
151+
</ul>
152+
</div>
153+
154+
<!-- Profile Dropdown Menu -->
82155
<div class="relative ml-3" #menuDropdown>
83156
<button
84157
type="button"
@@ -102,7 +175,7 @@
102175
</button>
103176
<div
104177
[@openClose]="openCloseTrigger"
105-
class="absolute right-0 z-10 w-56 mt-2 origin-top-right bg-white divide-y rounded-md shadow-lg divide-slate-100 ring-1 ring-black dark:bg-gray-800 dark:divide-slate-700 dark:ring-gray-800 ring-opacity-5 focus:outline-none"
178+
class="absolute right-0 z-10 w-56 mt-2 origin-top-right bg-white divide-y rounded-md shadow-lg divide-slate-100 ring-1 ring-black dark:bg-gray-800 dark:divide-slate-700 dark:ring-gray-800 ring-opacity-5 dark:ring-opacity-70 focus:outline-none"
106179
role="menu"
107180
aria-orientation="vertical"
108181
aria-labelledby="menu-button"

src/app/shared/themes/components/header/header.component.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ElementRef,
66
EventEmitter,
77
HostListener,
8+
OnInit,
89
Output,
910
ViewChild,
1011
} from '@angular/core';
@@ -36,9 +37,19 @@ import { logoutAction } from '@app/modules/authentication/store/auth.actions';
3637
]),
3738
],
3839
})
39-
export class HeaderComponent {
40+
export class HeaderComponent implements OnInit {
4041
mobileMenuOpen!: boolean;
4142

43+
currentTheme!: string;
44+
45+
showDialog: boolean = false;
46+
47+
themes = [
48+
{ name: 'Light', value: 'light' },
49+
{ name: 'Dark', value: 'dark' },
50+
{ name: 'System', value: 'system' },
51+
];
52+
4253
@ViewChild('menuDropdown') menuDropdown!: ElementRef;
4354

4455
@Output() private openMobileSidebar: EventEmitter<boolean> =
@@ -57,6 +68,7 @@ export class HeaderComponent {
5768
}
5869

5970
toggleMobileMenu(): void {
71+
this.showDialog = false;
6072
this.mobileMenuOpen = !this.mobileMenuOpen;
6173
}
6274

@@ -75,4 +87,26 @@ export class HeaderComponent {
7587
}
7688

7789
constructor(private store: Store) {}
90+
91+
ngOnInit(): void {
92+
const selectedTheme = window.localStorage.getItem('theme');
93+
94+
if (selectedTheme) {
95+
document.documentElement.setAttribute('data-theme', selectedTheme)
96+
} else {
97+
const theme = this.themes.find(
98+
(theme) =>
99+
theme.value === document.documentElement.getAttribute('data-theme')
100+
);
101+
window.localStorage.setItem('theme', theme!.value);
102+
}
103+
104+
this.currentTheme = window.localStorage.getItem('theme')!;
105+
}
106+
107+
updateTheme(theme: string) {
108+
document.documentElement.setAttribute('data-theme', theme);
109+
window.localStorage.setItem('theme', theme);
110+
this.currentTheme = theme;
111+
}
78112
}

src/app/shared/themes/components/sidebar/sidebar.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
<aside id="sidebar" class="relative flex flex-col h-full w-[260px] bg-gray-100 lg:bg-transparent dark:bg-gray-800">
2-
<div class="flex flex-col flex-grow py-6 overflow-y-auto">
3-
<div class="relative flex items-center px-6 py-8 shrink-0 sm:hidden">
4-
<logo-svg class="h-auto w-36"></logo-svg>
1+
<aside id="sidebar" class="relative flex flex-col h-full w-[260px] bg-gray-100 lg:bg-transparent dark:bg-gray-800 md:dark:bg-transparent">
2+
<div class="flex flex-col flex-grow pb-6 overflow-y-auto">
3+
<div class="relative flex items-center px-6 py-8 shrink-0 md:hidden">
4+
<logo-svg class="h-auto w-36 text-slate-900 dark:text-white"></logo-svg>
55
</div>
66
<nav class="flex flex-col h-full pt-4 pb-8 space-y-8" aria-label="Sidebar">
77
<div *ngFor="let menu of menus">
88
<h5 class="px-6 font-mono text-xs font-semibold leading-5 tracking-widest uppercase text-slate-400 sm:px-8 dark:text-slate-300">{{ menu.group }}</h5>
99
<div class="mt-3 space-y-1">
1010
<ng-template *ngFor="let item of menu.items" [ngxPermissionsOnly]="item.roles">
11-
<a [routerLink]="item.link" routerLinkActive="menu-current" class="menu group">
12-
<svg class="w-6 h-6 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
11+
<a [routerLink]="item.link" routerLinkActive="menu-current" class="text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white menu group">
12+
<svg class="w-6 h-6 mr-3 shrink-0 text-slate-500 group-hover:text-slate-900 dark:text-slate-500 dark:group-hover:text-slate-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
1313
<path *ngFor="let path of item.svgPath" [attr.d]="path" stroke-linecap="round" stroke-linejoin="round"/>
1414
</svg>
1515
<span class="truncate">{{ item.title }}</span>
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11

22
.menu {
3-
@apply flex items-center px-6 py-2 text-sm font-medium border-l-4 border-transparent text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white;
4-
5-
svg {
6-
@apply text-slate-600 group-hover:text-slate-900 dark:text-slate-500 dark:group-hover:text-white;
7-
}
3+
@apply flex items-center px-6 py-2 text-sm font-medium border-l-4 border-transparent;
84

95
&-current {
10-
@apply text-primary-600 border-primary-600 hover:text-primary-600;
6+
@apply border-primary-600 hover:text-primary-600;
7+
color: theme('colors.primary.600') !important;
118

129
svg {
13-
@apply text-primary-600 group-hover:text-primary-600;
10+
@apply group-hover:text-primary-600;
11+
stroke: theme('colors.primary.600') !important;
1412
}
1513
}
1614
}

src/app/shared/themes/layouts/cpanel/cpanel.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<div class="flex h-full min-h-screen">
88
<!-- Static sidebar for mobile -->
99
<div class="relative z-40 md:hidden" [ngClass]="mobileSidebarOpen ? 'block' : 'hidden'">
10-
<div [@openBackdrop]="openCloseTrigger" class="fixed inset-0 bg-gray-900 bg-opacity-75 dark:bg-gray-700"></div>
10+
<div [@openBackdrop]="openCloseTrigger" class="fixed inset-0 bg-gray-900 bg-opacity-75 dark:bg-gray-700 dark:bg-opacity-80"></div>
1111

1212
<admin-sidebar [@openClose]="openCloseTrigger" ngClass="fixed inset-0 z-40 flex"></admin-sidebar>
1313

src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html>
2-
<html lang="fr" class="smooth-scroll">
2+
<html lang="fr" class="smooth-scroll [font-feature-settings:'ss01']">
33
<head>
44
<meta charset="utf-8">
55
<title>Admin Cpanel</title>

0 commit comments

Comments
 (0)