Skip to content

Commit 03d84f0

Browse files
committed
feat: add theme switcher
Signed-off-by: rajput-hemant <rajput.hemant2001@gmail.com>
1 parent 448d8b8 commit 03d84f0

12 files changed

+160
-15
lines changed

.prettierrc.mjs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ const config = {
1717
"<THIRD_PARTY_MODULES>",
1818
"",
1919
"^types$",
20-
"^@/types/(.*)$",
21-
"^@/store/?(.*)$",
22-
"^@/config/(.*)$",
23-
"^@/lib/(.*)$",
24-
"^@/hooks/?(.*)$",
25-
"^@/components/(.*)$",
26-
"^@/styles/(.*)$",
20+
"^~/types/(.*)$",
21+
"^~/store/?(.*)$",
22+
"^~/config/(.*)$",
23+
"^~/lib/(.*)$",
24+
"^~/hooks/?(.*)$",
25+
"^~/components/(.*)$",
26+
"^~/styles/(.*)$",
27+
"",
28+
"^~/public/(.*)$",
2729
"^[./]",
2830
],
2931
importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ git commit --no-verify -m "init"
5555

5656
Check out the [package.json](package.json) for all available scripts.
5757

58-
## Folder Structure
59-
6058
## After Installation Checklist
6159

6260
- [ ] Update `package.json` with your project details.

src/components/theme/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./theme-provider";
2+
export * from "./theme-script";
3+
export * from "./theme-switcher";
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Signal } from "@builder.io/qwik";
2+
import {
3+
component$,
4+
createContextId,
5+
Slot,
6+
useContextProvider,
7+
useSignal,
8+
} from "@builder.io/qwik";
9+
10+
export type Theme = "light" | "dark" | "system" | null;
11+
12+
export const ThemeContext = createContextId<Signal<Theme>>("theme");
13+
14+
/**
15+
* @see https://github.com/BuilderIO/qwik/issues/1480
16+
*/
17+
export const ThemeProvider = component$(() => {
18+
const theme = useSignal<Theme>(null);
19+
20+
useContextProvider(ThemeContext, theme);
21+
22+
return <Slot />;
23+
});

src/components/theme/theme-script.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @see https://github.com/BuilderIO/qwik/issues/1480
3+
*/
4+
export const ThemeScript = () => {
5+
const script = `
6+
!function() {
7+
try {
8+
var html = document.documentElement;
9+
var system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
10+
var theme = localStorage.getItem('theme') ?? system;
11+
if (theme === 'system') theme = system;
12+
html.classList.add(theme);
13+
html.dataset.theme = theme;
14+
} catch (e) {}
15+
}();
16+
`;
17+
18+
return <script type="text/javascript" dangerouslySetInnerHTML={script} />;
19+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { HtmlHTMLAttributes } from "@builder.io/qwik";
2+
import { $, component$ } from "@builder.io/qwik";
3+
import { LuMoon, LuSun } from "@qwikest/icons/lucide";
4+
5+
import { useTheme } from "~/hooks/use-theme";
6+
7+
type ThemeSwitcherProps = HtmlHTMLAttributes<HTMLButtonElement>;
8+
9+
/**
10+
* @see https://github.com/BuilderIO/qwik/issues/1480
11+
*/
12+
export const ThemeSwitcher = component$<ThemeSwitcherProps>((props) => {
13+
const theme = useTheme();
14+
15+
const toggleTheme = $(() => {
16+
theme.value = theme.value === "dark" ? "light" : "dark";
17+
});
18+
19+
return (
20+
<button onClick$={toggleTheme} {...props}>
21+
<LuSun
22+
aria-hidden="true"
23+
class="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
24+
/>
25+
<LuMoon
26+
aria-hidden="true"
27+
class="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
28+
/>
29+
</button>
30+
);
31+
});

src/hooks/use-theme.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useContext, useSignal, useVisibleTask$ } from "@builder.io/qwik";
2+
3+
import type { Theme } from "~/components/theme";
4+
import { ThemeContext } from "~/components/theme";
5+
6+
export function useTheme() {
7+
const theme = useContext(ThemeContext);
8+
const prev = useSignal<Theme>(null);
9+
10+
useVisibleTask$(
11+
({ track }) => {
12+
track(theme);
13+
const html = document.documentElement;
14+
15+
if (!theme.value) return;
16+
17+
const media = window.matchMedia("(prefers-color-scheme: dark)");
18+
19+
if (prev.value) html.classList.remove(prev.value);
20+
if (prev.value === "system" && media.matches)
21+
html.classList.remove("dark");
22+
23+
html.classList.add(theme.value);
24+
html.dataset.theme = theme.value;
25+
26+
if (theme.value === "system") {
27+
media.matches
28+
? html.classList.add("dark")
29+
: html.classList.remove("dark");
30+
}
31+
32+
const colorSchemeListener = () => {
33+
if (theme.value !== "system") return;
34+
media.matches
35+
? html.classList.add("dark")
36+
: html.classList.remove("dark");
37+
};
38+
media.addEventListener("change", colorSchemeListener);
39+
40+
prev.value = theme.value;
41+
42+
return () => media.removeEventListener("change", colorSchemeListener);
43+
},
44+
{ strategy: "document-ready" }
45+
);
46+
47+
useVisibleTask$(({ track }) => {
48+
track(theme);
49+
if (theme.value) localStorage.setItem("theme", theme.value);
50+
});
51+
52+
useVisibleTask$(() => {
53+
const storedTheme = localStorage.getItem("theme") as Theme;
54+
theme.value = storedTheme ?? "system";
55+
});
56+
57+
return theme;
58+
}

src/root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77

88
import { RouterHead } from "./components/router-head/router-head";
99
import { TailwindIndicator } from "./components/tailwind-indicator";
10+
import { ThemeScript } from "./components/theme";
1011

1112
import "./global.css";
1213

@@ -24,6 +25,7 @@ export default component$(() => {
2425
<meta charSet="utf-8" />
2526
<link rel="manifest" href="/manifest.json" />
2627
<RouterHead />
28+
<ThemeScript />
2729
</head>
2830
<body lang="en">
2931
<RouterOutlet />

src/routes/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
LuFlameKindling,
1313
LuGithub,
1414
} from "@qwikest/icons/lucide";
15-
import QwikLogo from "~public/favicon.svg?jsx";
15+
16+
import QwikLogo from "~/public/favicon.svg?jsx";
1617

1718
const packageManagers = {
1819
bun: "bunx",
@@ -86,8 +87,8 @@ export default component$(() => {
8687
});
8788

8889
return (
89-
<main class="layout min-h-screen w-full bg-[#141414] bg-fixed text-white selection:bg-zinc-300 selection:text-black">
90-
<section class="container px-4 py-12 md:px-6 md:pt-24 lg:pt-32 xl:pt-48">
90+
<main class="layout grid min-h-screen w-full place-items-center bg-[#141414] bg-fixed text-white selection:bg-zinc-300 selection:text-black">
91+
<section class="px-4">
9192
<QwikLogo
9293
alt="Next.js logo"
9394
class="w-2h-28 mx-auto mb-6 h-28 md:max-w-full"
@@ -175,12 +176,13 @@ export default component$(() => {
175176
</section>
176177

177178
<footer class="absolute inset-x-0 bottom-4 flex justify-center">
178-
<em class="bg-gradient-to-r from-sky-500 to-purple-400 bg-clip-text text-transparent">
179+
<em class="text-zinc-500">
179180
&copy;{new Date().getFullYear()}{" "}
180181
<a
181182
href="https://github.com/rajput-hemant"
182183
target="_blank"
183184
rel="noopener noreferrer"
185+
class="underline-offset-4 duration-150 hover:text-zinc-400 hover:underline"
184186
>
185187
rajput-hemant@github
186188
</a>

src/routes/layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { component$, Slot } from "@builder.io/qwik";
22
import type { RequestHandler } from "@builder.io/qwik-city";
33

4+
import { ThemeProvider } from "~/components/theme";
5+
46
export const onGet: RequestHandler = async ({ cacheControl }) => {
57
// Control caching for this request for best performance and to reduce hosting costs:
68
// https://qwik.builder.io/docs/caching/
@@ -13,5 +15,9 @@ export const onGet: RequestHandler = async ({ cacheControl }) => {
1315
};
1416

1517
export default component$(() => {
16-
return <Slot />;
18+
return (
19+
<ThemeProvider>
20+
<Slot />
21+
</ThemeProvider>
22+
);
1723
});

tailwind.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Config } from "tailwindcss";
22

33
const config = {
4+
darkMode: ["class"],
45
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
56
theme: {
67
container: {

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"types": ["node", "vite/client"],
2020
"paths": {
2121
"~/*": ["./src/*"],
22-
"~public/*": ["./public/*"]
22+
"~/public/*": ["./public/*"]
2323
}
2424
},
2525
"files": [".eslintrc.cjs", "postcss.config.cjs"],

0 commit comments

Comments
 (0)