mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-05 00:36:23 +00:00
feat: create random labels page feat: implement login page with OpenID Connect support feat: add Gravatar component for user avatars feat: create Navbar component with navigation menu chore: create Sidebar component placeholder feat: implement login form with OIDC and email/password options feat: add reusable button component feat: create card component for structured content display feat: implement input component for forms feat: create label component for form labels feat: add navigation menu component for site navigation chore: add configuration file for site metadata feat: implement device context for responsive design feat: add utility functions for class name management feat: define OIDC configuration model feat: define base response model for API responses feat: define user model for user data feat: implement i18n for internationalization support feat: add English and Chinese translations for login chore: create index for locale resources chore: add blog home view placeholder
143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
|
|
import i18n, { getDefaultLang } from "@/utils/i18n";
|
|
|
|
type Mode = "light" | "dark";
|
|
type Lang = string;
|
|
|
|
interface DeviceContextProps {
|
|
isMobile: boolean;
|
|
mode: Mode;
|
|
setMode: (mode: Mode) => void;
|
|
toggleMode: () => void;
|
|
lang: Lang;
|
|
setLang: (lang: Lang) => void;
|
|
viewport: {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
}
|
|
|
|
const DeviceContext = createContext<DeviceContextProps>({
|
|
isMobile: false,
|
|
mode: "light",
|
|
setMode: () => {},
|
|
toggleMode: () => {},
|
|
lang: "zh-cn",
|
|
setLang: () => {},
|
|
viewport: {
|
|
width: 0,
|
|
height: 0,
|
|
},
|
|
});
|
|
|
|
export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
const [mode, setModeState] = useState<Mode>("light");
|
|
const [lang, setLangState] = useState<Lang>(getDefaultLang());
|
|
const [viewport, setViewport] = useState({
|
|
width: typeof window !== "undefined" ? window.innerWidth : 0,
|
|
height: typeof window !== "undefined" ? window.innerHeight : 0,
|
|
});
|
|
|
|
// 检查系统主题
|
|
const getSystemTheme = () =>
|
|
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
? "dark"
|
|
: "light";
|
|
|
|
useEffect(() => {
|
|
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
|
checkMobile();
|
|
window.addEventListener("resize", checkMobile);
|
|
return () => window.removeEventListener("resize", checkMobile);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// 更新检测函数以同时更新视窗尺寸
|
|
const handleResize = () => {
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight;
|
|
setIsMobile(width <= 768);
|
|
setViewport({ width, height });
|
|
};
|
|
|
|
handleResize(); // 初始化
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
// 初始化主题和系统主题变化监听
|
|
useEffect(() => {
|
|
if (typeof window !== "undefined") {
|
|
const savedTheme = localStorage.getItem("theme") as Mode | null;
|
|
const systemTheme = getSystemTheme();
|
|
const theme = savedTheme || systemTheme;
|
|
setModeState(theme);
|
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
|
|
// 监听系统主题变动
|
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const handleChange = (e: MediaQueryListEvent) => {
|
|
if (!localStorage.getItem("theme")) {
|
|
const newTheme = e.matches ? "dark" : "light";
|
|
setModeState(newTheme);
|
|
document.documentElement.classList.toggle("dark", newTheme === "dark");
|
|
}
|
|
};
|
|
media.addEventListener("change", handleChange);
|
|
return () => media.removeEventListener("change", handleChange);
|
|
}
|
|
}, []);
|
|
|
|
// 初始化语言
|
|
useEffect(() => {
|
|
if (typeof window !== "undefined") {
|
|
const savedLang = localStorage.getItem("language") || getDefaultLang();
|
|
setLangState(savedLang);
|
|
i18n.changeLanguage(savedLang);
|
|
}
|
|
}, []);
|
|
|
|
const setMode = useCallback((newMode: Mode) => {
|
|
setModeState(newMode);
|
|
document.documentElement.classList.toggle("dark", newMode === "dark");
|
|
if (newMode === getSystemTheme()) {
|
|
localStorage.removeItem("theme");
|
|
} else {
|
|
localStorage.setItem("theme", newMode);
|
|
}
|
|
}, []);
|
|
|
|
const toggleMode = useCallback(() => {
|
|
setModeState((prev) => {
|
|
const newMode = prev === "dark" ? "light" : "dark";
|
|
document.documentElement.classList.toggle("dark", newMode === "dark");
|
|
if (newMode === getSystemTheme()) {
|
|
localStorage.removeItem("theme");
|
|
} else {
|
|
localStorage.setItem("theme", newMode);
|
|
}
|
|
return newMode;
|
|
});
|
|
}, []);
|
|
|
|
const setLang = useCallback((newLang: Lang) => {
|
|
setLangState(newLang);
|
|
i18n.changeLanguage(newLang);
|
|
localStorage.setItem("language", newLang);
|
|
}, []);
|
|
|
|
return (
|
|
<DeviceContext.Provider
|
|
value={{ isMobile, mode, setMode, toggleMode, lang, setLang, viewport }}
|
|
>
|
|
{children}
|
|
</DeviceContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useDevice = () => useContext(DeviceContext);
|