mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 09:24:54 +10:00
use vite+
This commit is contained in:
@@ -9,22 +9,22 @@ import { useTheme } from "./provider";
|
||||
type Props = Omit<SingleComboboxProps, "options" | "value" | "onValueChange">;
|
||||
|
||||
export function ThemeCombobox(props: Props) {
|
||||
const router = useRouter();
|
||||
const { i18n } = useLingui();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const router = useRouter();
|
||||
const { i18n } = useLingui();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const options = Object.entries(themeMap).map(([value, label]) => ({
|
||||
value,
|
||||
label: i18n.t(label),
|
||||
keywords: [i18n.t(label)],
|
||||
}));
|
||||
const options = Object.entries(themeMap).map(([value, label]) => ({
|
||||
value,
|
||||
label: i18n.t(label),
|
||||
keywords: [i18n.t(label)],
|
||||
}));
|
||||
|
||||
const onThemeChange = async (value: string | null) => {
|
||||
if (!value || !isTheme(value)) return;
|
||||
await setThemeServerFn({ data: value });
|
||||
setTheme(value);
|
||||
void router.invalidate();
|
||||
};
|
||||
const onThemeChange = async (value: string | null) => {
|
||||
if (!value || !isTheme(value)) return;
|
||||
await setThemeServerFn({ data: value });
|
||||
setTheme(value);
|
||||
void router.invalidate();
|
||||
};
|
||||
|
||||
return <Combobox {...props} showClear={false} options={options} defaultValue={theme} onValueChange={onThemeChange} />;
|
||||
return <Combobox {...props} showClear={false} options={options} defaultValue={theme} onValueChange={onThemeChange} />;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { createContext, type PropsWithChildren, use } from "react";
|
||||
import { setThemeServerFn, type Theme } from "@/utils/theme";
|
||||
|
||||
type ThemeContextValue = {
|
||||
theme: Theme;
|
||||
setTheme: (value: Theme, options?: { playSound?: boolean }) => void;
|
||||
toggleTheme: (options?: { playSound?: boolean }) => void;
|
||||
theme: Theme;
|
||||
setTheme: (value: Theme, options?: { playSound?: boolean }) => void;
|
||||
toggleTheme: (options?: { playSound?: boolean }) => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
@@ -14,37 +14,37 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
type Props = PropsWithChildren<{ theme: Theme }>;
|
||||
|
||||
export function ThemeProvider({ children, theme }: Props) {
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
async function setTheme(value: Theme, options: { playSound?: boolean } = {}) {
|
||||
const { playSound = true } = options;
|
||||
async function setTheme(value: Theme, options: { playSound?: boolean } = {}) {
|
||||
const { playSound = true } = options;
|
||||
|
||||
document.documentElement.classList.toggle("dark", value === "dark");
|
||||
await setThemeServerFn({ data: value });
|
||||
void router.invalidate();
|
||||
document.documentElement.classList.toggle("dark", value === "dark");
|
||||
await setThemeServerFn({ data: value });
|
||||
void router.invalidate();
|
||||
|
||||
if (!playSound) return;
|
||||
if (!playSound) return;
|
||||
|
||||
try {
|
||||
const soundClip = value === "dark" ? "/sounds/switch-off.mp3" : "/sounds/switch-on.mp3";
|
||||
const audio = new Audio(soundClip);
|
||||
await audio.play();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
try {
|
||||
const soundClip = value === "dark" ? "/sounds/switch-off.mp3" : "/sounds/switch-on.mp3";
|
||||
const audio = new Audio(soundClip);
|
||||
await audio.play();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme(options: { playSound?: boolean } = {}) {
|
||||
void setTheme(theme === "dark" ? "light" : "dark", options);
|
||||
}
|
||||
function toggleTheme(options: { playSound?: boolean } = {}) {
|
||||
void setTheme(theme === "dark" ? "light" : "dark", options);
|
||||
}
|
||||
|
||||
return <ThemeContext value={{ theme, setTheme, toggleTheme }}>{children}</ThemeContext>;
|
||||
return <ThemeContext value={{ theme, setTheme, toggleTheme }}>{children}</ThemeContext>;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const value = use(ThemeContext);
|
||||
const value = use(ThemeContext);
|
||||
|
||||
if (!value) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
if (!value) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return value;
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -8,60 +8,60 @@ import { Button } from "@/components/ui/button";
|
||||
import { useTheme } from "./provider";
|
||||
|
||||
export function ThemeToggleButton(props: React.ComponentProps<typeof Button>) {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const onToggleTheme = useCallback(async () => {
|
||||
if (
|
||||
!buttonRef.current ||
|
||||
!document.startViewTransition ||
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
) {
|
||||
toggleTheme();
|
||||
return;
|
||||
}
|
||||
const onToggleTheme = useCallback(async () => {
|
||||
if (
|
||||
!buttonRef.current ||
|
||||
!document.startViewTransition ||
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
) {
|
||||
toggleTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
const style = document.createElement("style");
|
||||
let timeout: NodeJS.Timeout;
|
||||
const style = document.createElement("style");
|
||||
|
||||
style.textContent = `
|
||||
style.textContent = `
|
||||
::view-transition-old(root), ::view-transition-new(root) {
|
||||
mix-blend-mode: normal !important;
|
||||
animation: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
function transitionCallback() {
|
||||
flushSync(() => {
|
||||
toggleTheme();
|
||||
timeout = setTimeout(() => {
|
||||
clearTimeout(timeout);
|
||||
document.head.removeChild(style);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
function transitionCallback() {
|
||||
flushSync(() => {
|
||||
toggleTheme();
|
||||
timeout = setTimeout(() => {
|
||||
clearTimeout(timeout);
|
||||
document.head.removeChild(style);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
document.head.appendChild(style);
|
||||
await document.startViewTransition(transitionCallback).ready;
|
||||
document.head.appendChild(style);
|
||||
await document.startViewTransition(transitionCallback).ready;
|
||||
|
||||
const { top, left, width, height } = buttonRef.current.getBoundingClientRect();
|
||||
const x = left + width / 2;
|
||||
const y = top + height / 2;
|
||||
const right = window.innerWidth - left;
|
||||
const bottom = window.innerHeight - top;
|
||||
const maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom));
|
||||
const { top, left, width, height } = buttonRef.current.getBoundingClientRect();
|
||||
const x = left + width / 2;
|
||||
const y = top + height / 2;
|
||||
const right = window.innerWidth - left;
|
||||
const bottom = window.innerHeight - top;
|
||||
const maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom));
|
||||
|
||||
document.documentElement.animate(
|
||||
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`] },
|
||||
{ duration: 500, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)" },
|
||||
);
|
||||
}, [toggleTheme]);
|
||||
document.documentElement.animate(
|
||||
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`] },
|
||||
{ duration: 500, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)" },
|
||||
);
|
||||
}, [toggleTheme]);
|
||||
|
||||
const ariaLabel = theme === "dark" ? t`Switch to light theme` : t`Switch to dark theme`;
|
||||
const ariaLabel = theme === "dark" ? t`Switch to light theme` : t`Switch to dark theme`;
|
||||
|
||||
return (
|
||||
<Button size="icon" variant="ghost" ref={buttonRef} onClick={onToggleTheme} aria-label={ariaLabel} {...props}>
|
||||
{theme === "dark" ? <MoonIcon aria-hidden="true" /> : <SunIcon aria-hidden="true" />}
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Button size="icon" variant="ghost" ref={buttonRef} onClick={onToggleTheme} aria-label={ariaLabel} {...props}>
|
||||
{theme === "dark" ? <MoonIcon aria-hidden="true" /> : <SunIcon aria-hidden="true" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user