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:
parent
92ffc545de
commit
f26442f611
@ -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",
|
||||
|
@ -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": "订阅中...",
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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);
|
||||
|
||||
|
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user