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:
parent
42c2460718
commit
34ffcc989e
@ -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",
|
||||
|
@ -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": "保存中...",
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 />
|
||||
},
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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');
|
@ -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">
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -49,6 +49,7 @@ import { useTranslations } from 'next-intl';
|
||||
* website config, without translations
|
||||
*/
|
||||
export const websiteConfig: WebsiteConfig = {
|
||||
theme: "amber",
|
||||
metadata: {
|
||||
image: '/og.png',
|
||||
},
|
||||
|
@ -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;
|
||||
|
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user