feat: implement theme switcher & intl and support theme config

- Added new mode switcher components for both horizontal and dropdown layouts to facilitate theme toggling.
- Updated translation files to include new theme-related entries in English and Chinese.
- Refactored existing components to replace ThemeSwitcher with ModeSwitcher for consistency.
- Introduced new themes in the configuration and updated the theme selector to utilize translations for theme names.
- Enhanced global styles to support new themes and ensure proper application across the project.
This commit is contained in:
javayhu 2025-03-29 22:15:00 +08:00
parent 42c2460718
commit 34ffcc989e
16 changed files with 112 additions and 71 deletions

View File

@ -9,11 +9,21 @@
"login": "Log in",
"logout": "Log out",
"signUp": "Sign up",
"theme": "Toggle theme",
"language": "Switch language",
"mode": "Toggle mode",
"light": "Light",
"dark": "Dark",
"system": "System",
"theme": "Toggle theme",
"theme-default": "Default",
"theme-blue": "Blue",
"theme-green": "Green",
"theme-amber": "Amber",
"theme-scaled": "Scaled",
"theme-blue-scaled": "Blue Scaled",
"theme-default-scaled": "Default Scaled",
"theme-mono": "Mono",
"theme-mono-scaled": "Mono Scaled",
"copy": "Copy",
"saving": "Saving...",
"save": "Save",

View File

@ -9,11 +9,21 @@
"login": "登录",
"logout": "退出",
"signUp": "注册",
"theme": "切换主题",
"language": "切换语言",
"mode": "切换模式",
"light": "浅色模式",
"dark": "深色模式",
"system": "跟随系统",
"theme": "切换主题",
"theme-default": "默认",
"theme-blue": "蓝色",
"theme-green": "绿色",
"theme-amber": "橙色",
"theme-scaled": "缩放",
"theme-blue-scaled": "蓝色缩放",
"theme-default-scaled": "默认缩放",
"theme-mono": "等宽",
"theme-mono-scaled": "等宽缩放",
"copy": "复制",
"save": "保存",
"saving": "保存中...",

View File

@ -67,8 +67,8 @@ export async function generateMetadata({
}: {
params: Promise<{ slug: string; locale: Locale }>;
}): Promise<Metadata | undefined> {
const {slug, locale} = await params;
const { slug, locale } = await params;
const post = await getBlogPostFromParams({
params: Promise.resolve({ slug, locale }),
searchParams: Promise.resolve({})
@ -80,7 +80,7 @@ export async function generateMetadata({
return {};
}
const t = await getTranslations({locale, namespace: 'Metadata'});
const t = await getTranslations({ locale, namespace: 'Metadata' });
return constructMetadata({
title: `${post.title} | ${t('title')}`,
@ -126,13 +126,15 @@ export default async function BlogPostPage(props: NextPageProps) {
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<CalendarIcon className="size-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">{date}</p>
<span className="text-sm text-muted-foreground leading-none my-auto">
{date}
</span>
</div>
<div className="flex items-center gap-2">
<ClockIcon className="size-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground leading-none my-auto">
{estimateReadingTime(post.body.raw)}
</p>
</span>
</div>
</div>
@ -168,7 +170,9 @@ export default async function BlogPostPage(props: NextPageProps) {
/>
)}
</div>
<span className="line-clamp-1">{post.author?.name}</span>
<span className="line-clamp-1">
{post.author?.name}
</span>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { ThemeSwitcher } from '@/components/layout/theme-switcher';
import { ModeSwitcher } from '@/components/layout/mode-switcher';
import { Logo } from '@/components/logo';
import { websiteConfig } from '@/config';
import { defaultMessages } from '@/i18n/messages';
@ -31,6 +31,6 @@ export const baseOptions: BaseLayoutProps = {
themeSwitch: {
enabled: true,
mode: 'light-dark-system',
component: <ThemeSwitcher />
component: <ModeSwitcher />
},
};

View File

@ -5,6 +5,7 @@ import { ThemeProvider } from 'next-themes';
import { TooltipProvider } from '@/components/ui/tooltip';
import { PropsWithChildren } from 'react';
import { ActiveThemeProvider } from '@/components/layout/active-theme';
export function Providers({ children }: PropsWithChildren) {
return (
<ThemeProvider

View File

@ -9,7 +9,7 @@ import { Separator } from '@/components/ui/separator';
import { SidebarTrigger } from '@/components/ui/sidebar';
import React, { ReactNode } from 'react';
import LocaleSwitcher from '../layout/locale-switcher';
import { ThemeSwitcher } from '../layout/theme-switcher';
import { ModeSwitcher } from '../layout/mode-switcher';
import { ThemeSelector } from '../layout/theme-selector';
interface BreadcrumbItem {
@ -57,7 +57,7 @@ export function DashboardHeader({ breadcrumbs, actions }: DashboardHeaderProps)
{actions}
<ThemeSelector />
<ThemeSwitcher />
<ModeSwitcher />
<LocaleSwitcher />
</div>
</div>

View File

@ -1,5 +1,6 @@
"use client";
import { websiteConfig } from "@/config";
import {
ReactNode,
createContext,
@ -9,7 +10,7 @@ import {
} from "react";
const COOKIE_NAME = "active_theme";
const DEFAULT_THEME = "blue";
const DEFAULT_THEME = websiteConfig.theme ?? "default";
function setThemeCookie(theme: string) {
if (typeof window === "undefined") return;

View File

@ -1,7 +1,7 @@
'use client';
import Container from '@/components/container';
import { ThemeSwitcherHorizontal } from '@/components/layout/theme-switcher-horizontal';
import { ModeSwitcherHorizontal } from '@/components/layout/mode-switcher-horizontal';
import { Logo } from '@/components/logo';
import BuiltWithButton from '@/components/shared/built-with-button';
import { getFooterLinks, getSocialLinks } from '@/config';
@ -102,7 +102,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
<div className="flex items-center gap-x-4">
<ThemeSelector />
<ThemeSwitcherHorizontal />
<ModeSwitcherHorizontal />
</div>
</Container>
</div>

View File

@ -8,9 +8,9 @@ import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
/**
* Theme switcher component, used in the footer, switch theme by theme variable
* Mode switcher component, used in the footer
*/
export function ThemeSwitcherHorizontal() {
export function ModeSwitcherHorizontal() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const t = useTranslations('Common');

View File

@ -12,9 +12,9 @@ import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
/**
* Theme switcher component, used in the navbar, switch theme by CSS transitions
* Mode switcher component, used in the navbar
*/
export function ThemeSwitcher() {
export function ModeSwitcher() {
const { setTheme } = useTheme();
const t = useTranslations('Common');
@ -28,7 +28,7 @@ export function ThemeSwitcher() {
>
<SunIcon className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">{t('theme')}</span>
<span className="sr-only">{t('mode')}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">

View File

@ -1,7 +1,7 @@
'use client';
import LocaleSelector from '@/components/layout/locale-selector';
import { ThemeSwitcherHorizontal } from '@/components/layout/theme-switcher-horizontal';
import { ModeSwitcherHorizontal } from '@/components/layout/mode-switcher-horizontal';
import { Logo } from '@/components/logo';
import { Button, buttonVariants } from '@/components/ui/button';
import {
@ -327,7 +327,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
{/* bottom buttons */}
<div className="flex w-full items-center justify-between gap-4 border-t border-border/50 p-4">
<LocaleSelector />
<ThemeSwitcherHorizontal />
<ModeSwitcherHorizontal />
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
import { LoginWrapper } from '@/components/auth/login-button';
import Container from '@/components/container';
import { NavbarMobile } from '@/components/layout/navbar-mobile';
import { ThemeSwitcher } from '@/components/layout/theme-switcher';
import { ModeSwitcher } from '@/components/layout/mode-switcher';
import { UserButton } from '@/components/layout/user-button';
import { Logo } from '@/components/logo';
import { Button } from '@/components/ui/button';
@ -231,7 +231,7 @@ export function Navbar({ scroll }: NavBarProps) {
</div>
)}
<ThemeSwitcher />
<ModeSwitcher />
<LocaleSwitcher />
</div>
</nav>

View File

@ -12,51 +12,60 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useThemeConfig } from "./active-theme";
import { useTranslations } from "next-intl";
const DEFAULT_THEMES = [
{
name: "Default",
value: "default",
},
{
name: "Blue",
value: "blue",
},
{
name: "Green",
value: "green",
},
{
name: "Amber",
value: "amber",
},
];
const SCALED_THEMES = [
{
name: "Default",
value: "default-scaled",
},
{
name: "Blue",
value: "blue-scaled",
},
];
const MONO_THEMES = [
{
name: "Mono",
value: "mono-scaled",
},
];
/**
* 1. The component allows the user to select the theme of the website
* 2. All the themes are copied from the shadcn-ui dashboard example
* https://github.com/shadcn-ui/ui/blob/main/apps/v4/app/(examples)/dashboard/theme.css
* https://github.com/shadcn-ui/ui/blob/main/apps/v4/app/(examples)/dashboard/components/theme-selector.tsx
* https://github.com/TheOrcDev/orcish-dashboard/blob/main/components/theme-selector.tsx
*/
export function ThemeSelector() {
const { activeTheme, setActiveTheme } = useThemeConfig();
const t = useTranslations('Common');
const DEFAULT_THEMES = [
{
name: t('theme-default'),
value: "default",
},
{
name: t('theme-blue'),
value: "blue",
},
{
name: t('theme-green'),
value: "green",
},
{
name: t('theme-amber'),
value: "amber",
},
];
const SCALED_THEMES = [
{
name: t('theme-default-scaled'),
value: "default-scaled",
},
{
name: t('theme-blue-scaled'),
value: "blue-scaled",
},
];
const MONO_THEMES = [
{
name: t('theme-mono-scaled'),
value: "mono-scaled",
},
];
return (
<div className="flex items-center gap-2">
<Label htmlFor="theme-selector" className="sr-only">
Theme
{t('theme')}
</Label>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
@ -64,15 +73,14 @@ export function ThemeSelector() {
size="sm"
className="cursor-pointer justify-start *:data-[slot=select-value]:w-12"
>
{/* <span className="text-muted-foreground hidden sm:block">
Select a theme:
</span> */}
<span className="text-muted-foreground block sm:hidden">Theme</span>
<SelectValue placeholder="Select a theme" />
<span className="text-muted-foreground block sm:hidden">
{t('theme')}
</span>
<SelectValue placeholder={t('theme')} />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
<SelectLabel>Default</SelectLabel>
<SelectLabel>{t('theme-default')}</SelectLabel>
{DEFAULT_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}
className="cursor-pointer"
@ -83,7 +91,7 @@ export function ThemeSelector() {
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Scaled</SelectLabel>
<SelectLabel>{t('theme-scaled')}</SelectLabel>
{SCALED_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}
className="cursor-pointer"
@ -93,7 +101,7 @@ export function ThemeSelector() {
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Monospaced</SelectLabel>
<SelectLabel>{t('theme-mono')}</SelectLabel>
{MONO_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}
className="cursor-pointer"

View File

@ -49,6 +49,7 @@ import { useTranslations } from 'next-intl';
* website config, without translations
*/
export const websiteConfig: WebsiteConfig = {
theme: "amber",
metadata: {
image: '/og.png',
},

View File

@ -197,6 +197,11 @@ body {
--header-height: calc(var(--spacing) * 12 + 1px);
}
/*
* All the themes are copied from the shadcn-ui dashboard example
* https://github.com/shadcn-ui/ui/blob/main/apps/v4/app/(examples)/dashboard/theme.css
* https://github.com/TheOrcDev/orcish-dashboard/blob/main/app/globals.css
*/
.theme-scaled {
@media (min-width: 1024px) {
--radius: 0.6rem;

View File

@ -4,6 +4,7 @@ import type { ReactNode } from 'react';
* website config, without translations
*/
export type WebsiteConfig = {
theme: "default" | "blue" | "green" | "amber" | "default-scaled" | "blue-scaled" | "mono-scaled";
metadata: {
image?: string;
};