feat: update theme structure and enhance newsletter functionality

- Refactored theme-related translations in English and Chinese to a nested structure for better organization.
- Updated the default theme in the configuration to "default" for consistency.
- Enhanced the WaitlistPage and Newsletter components with improved descriptions and error handling.
- Integrated the NewsletterCard component into the BlogPostPage for better user engagement.
- Adjusted the ThemeSelector to utilize the new translation structure for theme names.
- Improved styling and layout in the NewsletterForm for a better user experience.
This commit is contained in:
javayhu 2025-03-30 00:29:41 +08:00
parent 92ffc545de
commit f26442f611
12 changed files with 175 additions and 104 deletions

View File

@ -14,16 +14,20 @@
"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",
"theme": {
"label": "Toggle theme",
"default-theme": "Default Theme",
"default": "Default",
"blue": "Blue",
"green": "Green",
"amber": "Amber",
"neutral": "Neutral",
"scaled-theme": "Scaled Theme",
"blue-scaled": "Blue Scaled",
"default-scaled": "Default Scaled",
"mono-theme": "Mono Theme",
"mono-scaled": "Mono Scaled"
},
"copy": "Copy",
"saving": "Saving...",
"save": "Save",
@ -85,11 +89,11 @@
},
"WaitlistPage": {
"title": "Waitlist",
"description": "Join our waitlist for latest news and updates",
"subtitle": "Join our waitlist for latest news and updates",
"description": "Join our waitlist for the launch of our product",
"subtitle": "Join our waitlist for the launch of our product",
"form": {
"title": "Join Our Waitlist",
"description": "We will send you only one email every week, and no spam",
"description": "We will notify you when we launch our product",
"email": "Email",
"subscribe": "Subscribe",
"subscribing": "Subscribing...",
@ -98,6 +102,17 @@
"emailValidation": "Please enter a valid email address"
}
},
"Newsletter": {
"title": "Join the community",
"description": "Subscribe to our newsletter for the latest news and updates",
"form": {
"email": "Email",
"subscribe": "Subscribe",
"subscribing": "Subscribing...",
"success": "Subscribed successfully",
"fail": "Failed to subscribe"
}
},
"AuthPage": {
"login": {
"title": "Login",

View File

@ -14,16 +14,20 @@
"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": "等宽缩放",
"theme": {
"label": "切换主题",
"default-theme": "默认主题",
"default": "默认",
"blue": "蓝色",
"green": "绿色",
"amber": "橙色",
"neutral": "中性",
"scaled-theme": "缩放主题",
"default-scaled": "默认缩放",
"blue-scaled": "蓝色缩放",
"mono-theme": "等宽主题",
"mono-scaled": "等宽缩放"
},
"copy": "复制",
"save": "保存",
"saving": "保存中...",
@ -79,12 +83,24 @@
}
},
"WaitlistPage": {
"title": "邮件列表",
"description": "加入我们的邮件列表,获取最新消息和更新",
"subtitle": "加入我们的邮件列表,获取最新消息和更新",
"title": "等待队列",
"description": "加入等待队列,及时获取最新消息和更新",
"subtitle": "加入等待队列,及时获取最新消息和更新",
"form": {
"title": "加入等待队列",
"description": "我们将在产品发布时及时通知您",
"email": "邮箱",
"subscribe": "加入等待队列",
"subscribing": "加入等待队列中...",
"success": "加入等待队列成功",
"fail": "加入等待队列失败",
"emailValidation": "请输入有效的邮箱地址"
}
},
"Newsletter": {
"title": "加入我们的社区",
"description": "订阅邮件列表,及时获取最新消息和更新",
"form": {
"title": "加入我们的邮件列表",
"description": "我们每周只发送一封邮件,并且不会发送垃圾邮件",
"email": "邮箱",
"subscribe": "订阅",
"subscribing": "订阅中...",

View File

@ -38,7 +38,7 @@ export default async function AboutPage() {
<div className="grid gap-8 sm:grid-cols-2">
{/* avatar and name */}
<div className="flex items-center gap-8">
<Avatar className="size-32">
<Avatar className="size-32 p-0.5">
<AvatarImage
className="rounded-full border-4 border-gray-200"
src="/logo.png"

View File

@ -14,6 +14,7 @@ import Image from 'next/image';
import { notFound } from 'next/navigation';
import { constructMetadata } from '@/lib/metadata';
import { Locale } from 'next-intl';
import { NewsletterCard } from '@/components/newsletter/newsletter-card';
/**
* Gets the blog post from the params
@ -210,7 +211,7 @@ export default async function BlogPostPage(props: NextPageProps) {
</div>
{/* newsletter */}
{/* TODO: add newsletter */}
<NewsletterCard />
</div>
);
}

View File

@ -141,7 +141,7 @@ export function NavUser({ user, className }: NavUserProps) {
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer">
<LaptopIcon className="mr-2 size-4" />
<span>{t('Common.theme')}</span>
<span>{t('Common.mode')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem

View File

@ -26,6 +26,15 @@ type ThemeContextType = {
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
/**
* This component is used to provide the active theme to the application
* It also sets the theme cookie and updates the body class when the theme changes.
*
* NOTICE: Since custom theme is set in useEffect,
* it will not be applied until the component is mounted,
* for better user experience, we recommend to replace the
* default theme with the custom theme in global.css.
*/
export function ActiveThemeProvider({
children,
initialTheme,

View File

@ -27,37 +27,41 @@ export function ThemeSelector() {
const DEFAULT_THEMES = [
{
name: t('theme-default'),
name: t('theme.default'),
value: "default",
},
{
name: t('theme-blue'),
name: t('theme.neutral'),
value: "neutral",
},
{
name: t('theme.blue'),
value: "blue",
},
{
name: t('theme-green'),
name: t('theme.green'),
value: "green",
},
{
name: t('theme-amber'),
name: t('theme.amber'),
value: "amber",
},
];
const SCALED_THEMES = [
{
name: t('theme-default-scaled'),
name: t('theme.default-scaled'),
value: "default-scaled",
},
{
name: t('theme-blue-scaled'),
name: t('theme.blue-scaled'),
value: "blue-scaled",
},
];
const MONO_THEMES = [
{
name: t('theme-mono-scaled'),
name: t('theme.mono-scaled'),
value: "mono-scaled",
},
];
@ -65,7 +69,7 @@ export function ThemeSelector() {
return (
<div className="flex items-center gap-2">
<Label htmlFor="theme-selector" className="sr-only">
{t('theme')}
{t('theme.label')}
</Label>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
@ -74,13 +78,13 @@ export function ThemeSelector() {
className="cursor-pointer justify-start *:data-[slot=select-value]:w-12"
>
<span className="text-muted-foreground block sm:hidden">
{t('theme')}
{t('theme.label')}
</span>
<SelectValue placeholder={t('theme')} />
<SelectValue placeholder={t('theme.label')} />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
<SelectLabel>{t('theme-default')}</SelectLabel>
<SelectLabel>{t('theme.default-theme')}</SelectLabel>
{DEFAULT_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}
className="cursor-pointer"
@ -91,7 +95,7 @@ export function ThemeSelector() {
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>{t('theme-scaled')}</SelectLabel>
<SelectLabel>{t('theme.scaled-theme')}</SelectLabel>
{SCALED_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}
className="cursor-pointer"
@ -101,7 +105,7 @@ export function ThemeSelector() {
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>{t('theme-mono')}</SelectLabel>
<SelectLabel>{t('theme.mono-theme')}</SelectLabel>
{MONO_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}
className="cursor-pointer"

View File

@ -2,17 +2,18 @@
import { HeaderSection } from '@/components/shared/header-section';
import { NewsletterForm } from '@/components/newsletter/newsletter-form';
import { useTranslations } from 'next-intl';
export function NewsletterCard() {
const t = useTranslations('Newsletter');
return (
<div className="w-full px-4 py-8 md:p-12 bg-muted rounded-lg">
<div className="flex flex-col items-center justify-center gap-8">
<HeaderSection
labelAs="h2"
label="Newsletter"
title="Join the Community"
titleAs="h3"
subtitle="Subscribe to our newsletter for the latest news and updates"
title={t('title')}
subtitle={t('description')}
/>
<NewsletterForm />

View File

@ -1,6 +1,7 @@
'use client';
// import { subscribeToNewsletter } from "@/actions/subscribe-to-newsletter";
import { subscribeNewsletterAction } from '@/actions/subscribe-newsletter';
import { FormError } from '@/components/shared/form-error';
import { Icons } from '@/components/icons/icons';
import { Button } from '@/components/ui/button';
import {
@ -19,9 +20,13 @@ import { PaperPlaneIcon } from '@radix-ui/react-icons';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
export function NewsletterForm() {
const [isPending, startTransition] = useTransition();
const t = useTranslations('Newsletter.form');
const [error, setError] = useState<string | undefined>();
const form = useForm<NewsletterFormData>({
resolver: zodResolver(NewsletterFormSchema),
@ -31,64 +36,80 @@ export function NewsletterForm() {
});
function onSubmit(data: NewsletterFormData) {
// startTransition(async () => {
// subscribeToNewsletter({ email: data.email })
// .then((data) => {
// switch (data.status) {
// case "success":
// toast.success("Thank you for subscribing to our newsletter");
// form.reset();
// break;
// default:
// toast.error("Something went wrong, please try again");
// }
// })
// .catch((error) => {
// console.error("NewsletterForm, onSubmit, error:", error);
// toast.error("Something went wrong");
// });
// });
startTransition(async () => {
try {
setError(undefined);
const result = await subscribeNewsletterAction({
email: data.email,
});
if (result?.data?.success) {
toast.success(t('success'));
form.reset();
} else {
const errorMessage = result?.data?.error || t('fail');
setError(errorMessage);
toast.error(errorMessage);
}
} catch (err) {
console.error('Newsletter subscription error:', err);
const errorMessage = t('fail');
setError(errorMessage);
toast.error(errorMessage);
}
});
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex items-center justify-center"
className="flex flex-col items-center justify-center w-full max-w-md mx-auto space-y-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="relative space-y-0">
<FormLabel className="sr-only">Email</FormLabel>
<FormControl className="rounded-r-none">
<Input
type="email"
className={cn(
'w-[280px] sm:w-[320px] md:w-[400px] h-12 rounded-r-none',
'focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-primary focus:border-2 focus:border-r-0'
)}
placeholder="Enter your email"
{...field}
/>
</FormControl>
<FormMessage className="pt-2 text-sm" />
</FormItem>
)}
/>
<Button
type="submit"
className="rounded-l-none size-12"
disabled={isPending}
>
{isPending ? (
<Icons.spinner className="size-6 animate-spin" aria-hidden="true" />
) : (
<PaperPlaneIcon className="size-6" aria-hidden="true" />
)}
<span className="sr-only">Subscribe</span>
</Button>
<div className="flex items-center w-full">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="relative w-full space-y-0">
<FormLabel className="sr-only">{t('email')}</FormLabel>
<FormControl className="rounded-r-none">
<Input
type="email"
className={cn(
'w-full h-12 rounded-r-none',
'focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-primary focus:border-2 focus:border-r-0'
)}
placeholder={t('email')}
{...field}
/>
</FormControl>
<div className="absolute -bottom-6 left-0">
<FormMessage className="text-sm text-destructive" />
</div>
</FormItem>
)}
/>
<Button
type="submit"
className="rounded-l-none size-12"
disabled={isPending}
>
{isPending ? (
<Icons.spinner className="size-6 animate-spin" aria-hidden="true" />
) : (
<PaperPlaneIcon className="size-6" aria-hidden="true" />
)}
<span className="sr-only">{t('subscribe')}</span>
</Button>
</div>
{error && (
<div className="w-full">
<FormError message={error} />
</div>
)}
</form>
</Form>
);

View File

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

View File

@ -221,8 +221,12 @@ body {
}
}
.theme-default,
.theme-default-scaled {
.theme-default{
/* default theme */
}
.theme-neutral,
.theme-neutral-scaled {
--primary: var(--color-neutral-600);
--primary-foreground: var(--color-neutral-50);

View File

@ -4,7 +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";
theme: "default" | "blue" | "green" | "amber" | "neutral" | "default-scaled" | "blue-scaled" | "mono-scaled";
metadata: {
image?: string;
};