feat: add new content pages

This commit is contained in:
beilunyang
2025-12-10 02:29:17 +08:00
committed by ty
parent cef0ee0f0c
commit 07ad672e7d
54 changed files with 2314 additions and 57 deletions

View File

@@ -0,0 +1,24 @@
import { Header } from "@/components/layout/header"
import { AboutContent } from "@/components/about/about-content"
import { getTranslations } from "next-intl/server"
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "common" })
return {
title: `${t("nav.about")} - ${t("app.name")}`
}
}
export default function AboutPage() {
return (
<div className="min-h-screen bg-background flex flex-col">
<Header />
<main className="container mx-auto px-4 py-24 flex-1">
<div className="max-w-4xl mx-auto">
<AboutContent />
</div>
</main>
</div>
)
}

View File

@@ -0,0 +1,68 @@
import { Header } from "@/components/layout/header"
import { getBlogPost, blogPosts } from "@/lib/blog-data"
import { notFound } from "next/navigation"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { getTranslations } from "next-intl/server"
export function generateStaticParams() {
return blogPosts.map((post) => ({
slug: post.slug,
}))
}
export async function generateMetadata({
params
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
const post = getBlogPost(slug, locale)
if (!post) return {}
return {
title: `${post.title} - MoeMail`
}
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
const post = getBlogPost(slug, locale)
const t = await getTranslations({ locale, namespace: "blog" })
if (!post) {
notFound()
}
return (
<div className="min-h-screen bg-background flex flex-col">
<Header />
<main className="container mx-auto px-4 py-24 flex-1">
<article className="max-w-3xl mx-auto space-y-8">
<Link
href={`/${locale}/blog`}
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="mr-2 h-4 w-4" />
{t("backToBlog")}
</Link>
<header className="space-y-4">
<h1 className="text-4xl font-bold">{post.title}</h1>
<time className="text-muted-foreground block" dateTime={post.date}>
{post.date}
</time>
</header>
<div
className="prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
</main>
</div>
)
}

View File

@@ -0,0 +1,62 @@
import { Header } from "@/components/layout/header"
import { getTranslations } from "next-intl/server"
import Link from "next/link"
import { getBlogPosts } from "@/lib/blog-data"
import { ArrowRight } from "lucide-react"
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "common" })
return {
title: `${t("nav.blog")} - ${t("app.name")}`
}
}
export default async function BlogIndexPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "blog" })
const posts = getBlogPosts(locale)
return (
<div className="min-h-screen bg-background flex flex-col">
<Header />
<main className="container mx-auto px-4 py-24 flex-1">
<div className="max-w-4xl mx-auto space-y-12">
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold">{t("title")}</h1>
<p className="text-muted-foreground text-lg">
{t("subtitle")}
</p>
</div>
<div className="grid gap-8 md:grid-cols-2">
{posts.map((post) => (
<article key={post.slug} className="group relative flex flex-col space-y-4 rounded-lg border p-6 hover:bg-accent/50 transition-colors">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<time dateTime={post.date}>{post.date}</time>
</div>
<h2 className="text-2xl font-bold group-hover:text-primary transition-colors">
{post.title}
</h2>
<p className="text-muted-foreground flex-1">
{post.excerpt}
</p>
<Link
href={`/${locale}/blog/${post.slug}`}
className="inline-flex items-center font-medium text-primary hover:underline"
>
{t("readMore")} <ArrowRight className="ml-1 h-4 w-4" />
<span className="absolute inset-0" />
</Link>
</article>
))}
</div>
</div>
</main>
</div>
)
}

View File

@@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server"
import { i18n, type Locale } from "@/i18n/config"
import type { Metadata, Viewport } from "next"
import { FloatMenu } from "@/components/float-menu"
import { ConditionalFooter } from "@/components/layout/conditional-footer"
import { ThemeProvider } from "@/components/theme/theme-provider"
import { Toaster } from "@/components/ui/toaster"
import { cn } from "@/lib/utils"
@@ -29,10 +30,14 @@ async function getMessages(locale: Locale) {
const metadata = (await import(`@/i18n/messages/${locale}/metadata.json`)).default
const emails = (await import(`@/i18n/messages/${locale}/emails.json`)).default
const profile = (await import(`@/i18n/messages/${locale}/profile.json`)).default
return { common, home, auth, metadata, emails, profile }
const privacy = (await import(`@/i18n/messages/${locale}/privacy.json`)).default
const terms = (await import(`@/i18n/messages/${locale}/terms.json`)).default
const about = (await import(`@/i18n/messages/${locale}/about.json`)).default
const blog = (await import(`@/i18n/messages/${locale}/blog.json`)).default
return { common, home, auth, metadata, emails, profile, privacy, terms, about, blog }
} catch (error) {
console.error(`Failed to load messages for locale ${locale}:`, error)
return { common: {}, home: {}, auth: {}, metadata: {}, emails: {}, profile: {} }
return { common: {}, home: {}, auth: {}, metadata: {}, emails: {}, profile: {}, privacy: {}, terms: {}, about: {}, blog: {} }
}
}
@@ -46,7 +51,7 @@ export async function generateMetadata({
const t = await getTranslations({ locale, namespace: "metadata" })
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://moemail.app"
// Generate hreflang links for all supported locales
const languages: Record<string, string> = {}
i18n.locales.forEach((loc) => {
@@ -117,7 +122,7 @@ export default async function LocaleLayout({
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
</head>
<body
<body
className={cn(
zpix.variable,
"font-zpix min-h-screen antialiased",
@@ -135,6 +140,7 @@ export default async function LocaleLayout({
<Providers>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
<ConditionalFooter />
<FloatMenu />
</NextIntlClientProvider>
</Providers>

View File

@@ -3,6 +3,8 @@ import { auth } from "@/lib/auth"
import { Shield, Share2, Clock, Code2 } from "lucide-react"
import { ActionButton } from "@/components/home/action-button"
import { FeatureCard } from "@/components/home/feature-card"
import { FaqSection } from "@/components/home/faq-section"
import { HowItWorks } from "@/components/home/how-it-works"
import { getTranslations } from "next-intl/server"
import type { Locale } from "@/i18n/config"
@@ -23,39 +25,41 @@ export default async function Home({
<div className="container mx-auto px-4 lg:px-8 max-w-[1600px]">
<Header />
<main className="pt-16">
<div className="h-[calc(100vh-4rem)] flex flex-col items-center justify-center text-center px-2 relative overflow-hidden">
<div className="absolute inset-0 -z-10 bg-grid-primary/5" />
<div className="min-h-[calc(100vh-4rem)] flex flex-col items-center justify-center text-center px-2 relative overflow-hidden py-20">
{/* Background Effects */}
<div className="absolute inset-0 -z-10 bg-grid-primary/10" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-primary/20 blur-[100px] rounded-full -z-10 animate-pulse" />
<div className="w-full max-w-3xl mx-auto space-y-6 sm:space-y-8 py-4">
<div className="space-y-2 sm:space-y-3">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-wider">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-purple-600">
<div className="w-full max-w-4xl mx-auto space-y-8 sm:space-y-12 py-4 relative z-10">
<div className="space-y-4 sm:space-y-6">
<h1 className="text-4xl sm:text-5xl md:text-7xl font-bold tracking-tight">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary via-purple-500 to-pink-500 animate-gradient">
{t("title")}
</span>
</h1>
<p className="text-lg sm:text-xl text-gray-600 dark:text-gray-300 tracking-wide">
<p className="text-lg sm:text-2xl text-muted-foreground tracking-wide max-w-2xl mx-auto leading-relaxed">
{t("subtitle")}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 px-2 sm:px-0">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 px-4 sm:px-0">
<FeatureCard
icon={<Shield className="w-5 h-5" />}
icon={<Shield className="w-6 h-6" />}
title={t("features.privacy.title")}
description={t("features.privacy.description")}
/>
<FeatureCard
icon={<Share2 className="w-5 h-5" />}
icon={<Share2 className="w-6 h-6" />}
title={t("features.instant.title")}
description={t("features.instant.description")}
/>
<FeatureCard
icon={<Clock className="w-5 h-5" />}
icon={<Clock className="w-6 h-6" />}
title={t("features.expiry.title")}
description={t("features.expiry.description")}
/>
<FeatureCard
icon={<Code2 className="w-5 h-5" />}
icon={<Code2 className="w-6 h-6" />}
title={t("features.openapi.title")}
description={t("features.openapi.description")}
/>
@@ -66,6 +70,9 @@ export default async function Home({
</div>
</div>
</div>
<HowItWorks />
<FaqSection />
</main>
</div>
</div>

View File

@@ -0,0 +1,24 @@
import { Header } from "@/components/layout/header"
import { PrivacyContent } from "@/components/legal/privacy-content"
import { getTranslations } from "next-intl/server"
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "common" })
return {
title: `${t("nav.privacy")} - ${t("app.name")} `
}
}
export default function PrivacyPage() {
return (
<div className="min-h-screen bg-background">
<Header />
<main className="container mx-auto px-4 py-24">
<div className="max-w-3xl mx-auto">
<PrivacyContent />
</div>
</main>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { Header } from "@/components/layout/header"
import { TermsContent } from "@/components/legal/terms-content"
import { getTranslations } from "next-intl/server"
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "common" })
return {
title: `${t("nav.terms")} - ${t("app.name")} `
}
}
export default function TermsPage() {
return (
<div className="min-h-screen bg-background">
<Header />
<main className="container mx-auto px-4 py-24">
<div className="max-w-3xl mx-auto">
<TermsContent />
</div>
</main>
</div>
)
}

View File

@@ -0,0 +1,80 @@
"use client"
import { Shield, Zap, Globe } from "lucide-react"
import { useTranslations } from "next-intl"
export function AboutContent() {
const t = useTranslations("about")
return (
<div className="space-y-12">
<section className="text-center space-y-4">
<h1 className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-purple-600">
{t("title")}
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
{t("subtitle")}
</p>
</section>
<section className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="p-6 rounded-lg border bg-card text-card-foreground shadow-sm">
<Shield className="w-12 h-12 text-primary mb-4" />
<h3 className="text-xl font-semibold mb-2">{t("features.privacy.title")}</h3>
<p className="text-muted-foreground">
{t("features.privacy.description")}
</p>
</div>
<div className="p-6 rounded-lg border bg-card text-card-foreground shadow-sm">
<Zap className="w-12 h-12 text-primary mb-4" />
<h3 className="text-xl font-semibold mb-2">{t("features.speed.title")}</h3>
<p className="text-muted-foreground">
{t("features.speed.description")}
</p>
</div>
<div className="p-6 rounded-lg border bg-card text-card-foreground shadow-sm">
<Globe className="w-12 h-12 text-primary mb-4" />
<h3 className="text-xl font-semibold mb-2">{t("features.openSource.title")}</h3>
<p className="text-muted-foreground">
{t("features.openSource.description")}
</p>
</div>
</section>
<section className="prose dark:prose-invert max-w-none">
<h2>{t("mission.title")}</h2>
<p>
{t("mission.content")}
</p>
<h2>{t("techStack.title")}</h2>
<p>
{t("techStack.content")}
</p>
<h2>{t("contact.title")}</h2>
<p>
{t("contact.intro")}{" "}
<a
href="https://github.com/beilunyang/moemail"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t("contact.github")}
</a>
{t("contact.joinText")}{" "}
<a
href="https://t.me/moecloudflare"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t("contact.telegram")}
</a>
{" "}{t("contact.outro")}
</p>
</section>
</div>
)
}

View File

@@ -0,0 +1,38 @@
"use client"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { useTranslations } from "next-intl"
export function FaqSection() {
const t = useTranslations("home.faq")
// We'll hardcode the keys for now to map them
const questions = ["q1", "q2", "q3", "q4", "q5"]
return (
<section className="py-24 bg-muted/30">
<div className="container mx-auto px-4 max-w-3xl">
<h2 className="text-4xl font-bold text-center mb-12">{t("title")}</h2>
<div className="bg-card w-full rounded-2xl border shadow-sm p-6 sm:p-10">
<Accordion type="single" collapsible className="w-full">
{questions.map((key) => (
<AccordionItem key={key} value={key} className="border-b-border/50 last:border-0 px-2">
<AccordionTrigger className="text-left py-6 text-lg hover:text-primary transition-colors hover:no-underline">
{t(`${key}.question`)}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground text-base pb-6 leading-relaxed">
{t(`${key}.answer`)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
</section>
)
}

View File

@@ -1,12 +1,12 @@
interface FeatureCardProps {
interface FeatureCardProps {
icon: React.ReactNode
title: string
description: string
description: string
}
export function FeatureCard({ icon, title, description }: FeatureCardProps) {
return (
<div className="p-4 rounded border-2 border-primary/20 hover:border-primary/40 transition-colors bg-white/5 backdrop-blur">
<div className="p-4 rounded-xl border border-primary/20 bg-background/50 backdrop-blur-sm hover:border-primary/50 transition-colors group">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-primary/10 text-primary p-2">
{icon}

View File

@@ -0,0 +1,68 @@
"use client"
import { useTranslations } from "next-intl"
import { MousePointerClick, RefreshCw, ArchiveX, Network, FileCode, Share2 } from "lucide-react"
export function HowItWorks() {
const t = useTranslations("home.howItWorks")
return (
<section className="py-16">
<div className="container mx-auto px-4 max-w-5xl">
<h2 className="text-3xl font-bold text-center mb-12">{t("title")}</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="flex flex-col items-center text-center space-y-4 p-6 rounded-xl border border-border bg-card/50 hover:border-primary/50 transition-colors">
<div className="p-4 bg-primary/10 rounded-full text-primary">
<MousePointerClick className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold">{t("step1.title")}</h3>
<p className="text-muted-foreground">{t("step1.description")}</p>
</div>
<div className="flex flex-col items-center text-center space-y-4 p-6 rounded-xl border border-border bg-card/50 hover:border-primary/50 transition-colors">
<div className="p-4 bg-primary/10 rounded-full text-primary">
<RefreshCw className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold">{t("step2.title")}</h3>
<p className="text-muted-foreground">{t("step2.description")}</p>
</div>
<div className="flex flex-col items-center text-center space-y-4 p-6 rounded-xl border border-border bg-card/50 hover:border-primary/50 transition-colors">
<div className="p-4 bg-primary/10 rounded-full text-primary">
<ArchiveX className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold">{t("step3.title")}</h3>
<p className="text-muted-foreground">{t("step3.description")}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-8">
<div className="flex flex-col items-center text-center space-y-4 p-6 rounded-xl border border-border bg-card/50 hover:border-primary/50 transition-colors">
<div className="p-4 bg-primary/10 rounded-full text-primary">
<Network className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold">{t("advStep1.title")}</h3>
<p className="text-muted-foreground">{t("advStep1.description")}</p>
</div>
<div className="flex flex-col items-center text-center space-y-4 p-6 rounded-xl border border-border bg-card/50 hover:border-primary/50 transition-colors">
<div className="p-4 bg-primary/10 rounded-full text-primary">
<FileCode className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold">{t("advStep2.title")}</h3>
<p className="text-muted-foreground">{t("advStep2.description")}</p>
</div>
<div className="flex flex-col items-center text-center space-y-4 p-6 rounded-xl border border-border bg-card/50 hover:border-primary/50 transition-colors">
<div className="p-4 bg-primary/10 rounded-full text-primary">
<Share2 className="w-8 h-8" />
</div>
<h3 className="text-xl font-semibold">{t("advStep3.title")}</h3>
<p className="text-muted-foreground">{t("advStep3.description")}</p>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,20 @@
"use client"
import { usePathname } from "next/navigation"
import { Footer } from "./footer"
export function ConditionalFooter() {
const pathname = usePathname()
// Hide footer on /moe and /profile paths
const hideFooterPaths = ["/moe", "/profile"]
const shouldHideFooter = hideFooterPaths.some(path =>
pathname.includes(path)
)
if (shouldHideFooter) {
return null
}
return <Footer />
}

View File

@@ -0,0 +1,83 @@
"use client"
import { useTranslations } from "next-intl"
import Link from "next/link"
import { useLocale } from "next-intl"
export function Footer() {
const t = useTranslations("common")
const locale = useLocale()
const year = new Date().getFullYear()
// Helper to generate localized path
// If we had a centralized navigation config with createSharedPathnamesNavigation, we would use that Link.
// Since we are using next/link manually:
const getPath = (path: string) => {
// Ideally we should check if default locale needs prefix or not based on middleware config
// But safely: if locale is strictly part of the URL structure in this app...
// The Logo uses "/" which might reset locale.
// Let's try to preserve it.
return `/${locale}${path}`
}
return (
<footer className="border-t bg-background/50 backdrop-blur-sm mt-auto">
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="space-y-4">
<h3 className="font-bold text-lg">{t("app.name")}</h3>
<p className="text-sm text-muted-foreground">
{t("footer.copyright", { year })}
</p>
</div>
<div className="space-y-4">
<h4 className="font-medium">{t("nav.home")}</h4>
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
<Link href={getPath("/about")} className="hover:text-foreground transition-colors">
{t("nav.about")}
</Link>
<Link href={getPath("/blog")} className="hover:text-foreground transition-colors">
{t("nav.blog")}
</Link>
</div>
</div>
<div className="space-y-4">
<h4 className="font-medium text-foreground">{t("nav.privacy")} & {t("nav.terms")}</h4>
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
<Link href={getPath("/privacy")} className="hover:text-foreground transition-colors">
{t("nav.privacy")}
</Link>
<Link href={getPath("/terms")} className="hover:text-foreground transition-colors">
{t("nav.terms")}
</Link>
</div>
</div>
<div className="space-y-4">
<h4 className="font-medium">{t("footer.contact")}</h4>
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
<a
href="https://github.com/beilunyang/moemail"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
>
{t("footer.github")}
</a>
<a
href="https://t.me/moecloudflare"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
>
{t("footer.telegram")}
</a>
</div>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,29 @@
"use client"
import { useTranslations } from "next-intl"
export function PrivacyContent() {
const t = useTranslations("privacy")
return (
<div className="prose dark:prose-invert max-w-none">
<h1>{t("title")}</h1>
<p>{t("lastUpdated")}</p>
<h2>{t("introduction.title")}</h2>
<p>{t("introduction.content")}</p>
<h2>{t("informationCollection.title")}</h2>
<p>{t("informationCollection.content")}</p>
<h2>{t("emailContent.title")}</h2>
<p>{t("emailContent.content")}</p>
<h2>{t("thirdParty.title")}</h2>
<p>{t("thirdParty.content")}</p>
<h2>{t("contact.title")}</h2>
<p>{t("contact.content")}</p>
</div>
)
}

View File

@@ -0,0 +1,35 @@
"use client"
import { useTranslations } from "next-intl"
export function TermsContent() {
const t = useTranslations("terms")
return (
<div className="prose dark:prose-invert max-w-none">
<h1>{t("title")}</h1>
<p>{t("lastUpdated")}</p>
<h2>{t("acceptance.title")}</h2>
<p>{t("acceptance.content")}</p>
<h2>{t("description.title")}</h2>
<p>{t("description.content")}</p>
<h2>{t("prohibited.title")}</h2>
<p>{t("prohibited.content")}
<ul>
<li>{t("prohibited.items.0")}</li>
<li>{t("prohibited.items.1")}</li>
<li>{t("prohibited.items.2")}</li>
</ul>
</p>
<h2>{t("disclaimer.title")}</h2>
<p>{t("disclaimer.content")}</p>
<h2>{t("changes.title")}</h2>
<p>{t("changes.content")}</p>
</div>
)
}

View File

@@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -59,6 +59,17 @@
}
@layer utilities {
.animate-gradient {
background-size: 200% auto;
animation: gradient 4s linear infinite;
}
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.bg-grid-primary {
background-image: linear-gradient(var(--primary) 1px, transparent 1px),
linear-gradient(90deg, var(--primary) 1px, transparent 1px);

View File

@@ -0,0 +1,34 @@
{
"title": "About MoeMail",
"subtitle": "A cute temporary email service built with Next.js + Cloudflare technology stack, protecting your privacy while keeping things simple and adorable 🎉",
"features": {
"privacy": {
"title": "Privacy First",
"description": "We don't track you. Secure account access ensures your mailbox remains private. Protect your real email address from spam and unnecessary subscriptions."
},
"speed": {
"title": "Lightning Fast",
"description": "Get your temporary email address instantly with automatic polling for real-time email notifications. Quick setup - just sign up and start using immediately."
},
"openSource": {
"title": "Open Source & Free",
"description": "Our code is open source on GitHub. Built on Cloudflare, capable of free self-hosting without any cost. You can verify how we handle your data and even host it yourself."
}
},
"mission": {
"title": "Our Mission",
"content": "In an age where digital privacy is constantly under threat and spam is a daily nuisance, MoeMail aims to provide a simple, effective solution. We believe that everyone deserves a clean inbox and the right to keep their personal email address private when signing up for one-time services or testing untrusted websites. Built with modern web technologies and powered by Cloudflare's edge network, MoeMail offers a fast, secure, and completely free temporary email service with a cute and friendly interface."
},
"techStack": {
"title": "Technology Stack",
"content": "MoeMail is built with cutting-edge technologies: Next.js for the framework, Cloudflare Pages for hosting, D1 for database, NextAuth for authentication, Tailwind CSS for styling, and Drizzle ORM for type-safe database operations. We support multiple languages through next-intl and provide a modern, responsive UI based on Radix UI components."
},
"contact": {
"title": "Get Involved",
"intro": "MoeMail is an open-source project. If you have suggestions, require support, or want to contribute, please visit our",
"github": "GitHub repository",
"joinText": ". Join our community on",
"telegram": "Telegram @moecloudflare",
"outro": "to stay updated with the latest developments."
}
}

View File

@@ -0,0 +1,6 @@
{
"title": "Blog",
"subtitle": "Latest updates and insights from MoeMail",
"readMore": "Read More",
"backToBlog": "Back to Blog"
}

View File

@@ -13,7 +13,17 @@
"login": "Login",
"profile": "Profile",
"logout": "Logout",
"backToMailbox": "Back to Mailbox"
"backToMailbox": "Back to Mailbox",
"about": "About Us",
"privacy": "Privacy Policy",
"terms": "Terms of Service",
"blog": "Blog"
},
"footer": {
"copyright": "© {year} MoeMail. All rights reserved.",
"contact": "Contact",
"github": "GitHub",
"telegram": "Telegram"
},
"github": "Get Source Code"
}
}

View File

@@ -8,7 +8,7 @@
},
"instant": {
"title": "Email Sharing",
"description": "Share your mailbox with others"
"description": "REST API available"
},
"expiry": {
"title": "Auto Expiry",
@@ -22,6 +22,55 @@
"actions": {
"enterMailbox": "Enter Mailbox",
"getStarted": "Get Started"
},
"howItWorks": {
"title": "How It Works",
"step1": {
"title": "Sign Up",
"description": "Create an account to securely access your personal dashboard."
},
"step2": {
"title": "Create Mailbox",
"description": "Generate custom or random temporary email addresses instantly."
},
"step3": {
"title": "Manage Emails",
"description": "Receive and view emails in real-time from your dashboard."
},
"advStep1": {
"title": "Webhooks",
"description": "Receive real-time notifications to your own server when emails arrive."
},
"advStep2": {
"title": "OpenAPI",
"description": "Integrate MoeMail into your applications using our standard REST API."
},
"advStep3": {
"title": "Sharing",
"description": "Share individual emails or entire mailboxes with others easily."
}
},
"faq": {
"title": "Frequently Asked Questions",
"q1": {
"question": "Do I need to register?",
"answer": "Yes, registration is required to manage your temporary mailboxes securely."
},
"q2": {
"question": "How long do emails last?",
"answer": "Emails are stored temporarily and automatically deleted after they expire."
},
"q3": {
"question": "Can I send emails?",
"answer": "Sending functionality is available for users with appropriate permissions."
},
"q4": {
"question": "Can I have multiple mailboxes?",
"answer": "Yes, you can create and manage multiple temporary email addresses from a single account."
},
"q5": {
"question": "How to access OpenAPI?",
"answer": "You can access the OpenAPI definition at https://docs.moemail.app/api.html#openapi. Authentication requires an API Key from your profile."
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"title": "Privacy Policy",
"lastUpdated": "Last updated: December 9, 2024",
"introduction": {
"title": "1. Introduction",
"content": "Welcome to MoeMail. We respect your privacy and represent that we do not collect any personal data from you when you use our temporary email service."
},
"informationCollection": {
"title": "2. Information Collection",
"content": "We require registration to provide you with a secure personal dashboard. We collect minimal information (such as username) for account management. We do not track your IP address for tracking purposes."
},
"emailContent": {
"title": "3. Email Content",
"content": "Emails received to your temporary address are stored for a limited time (e.g., 24 hours) and then permanently deleted. We do not read or analyze your email content."
},
"thirdParty": {
"title": "4. Third-Party Services",
"content": "We may use third-party services for hosting or analytics (like Google Analytics) which may collect anonymous usage data."
},
"contact": {
"title": "5. Contact Us",
"content": "If you have any questions about this Privacy Policy, please contact us via our GitHub repository."
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "Terms of Service",
"lastUpdated": "Last updated: December 9, 2024",
"acceptance": {
"title": "1. Acceptance of Terms",
"content": "By accessing and using MoeMail, you accept and agree to be bound by the terms and provision of this agreement."
},
"description": {
"title": "2. Description of Service",
"content": "MoeMail provides temporary disposable email addresses. This service is provided \"as is\" and intended for lawful purposes only."
},
"prohibited": {
"title": "3. Prohibited Use",
"content": "You agree not to use the service for:",
"items": [
"Sending spam or unsolicited emails.",
"Illegal activities.",
"Harassment or abuse."
]
},
"disclaimer": {
"title": "4. Disclaimer",
"content": "We make no warranties about the reliability or availability of the service. Emails are temporary and may be deleted at any time."
},
"changes": {
"title": "5. Changes to Terms",
"content": "We reserve the right to modify these terms at any time."
}
}

View File

@@ -0,0 +1,34 @@
{
"title": "MoeMail について",
"subtitle": "Next.js + Cloudflare テクノロジースタックで構築されたかわいい一時メールサービス、プライバシーを保護しながらシンプルでかわいい 🎉",
"features": {
"privacy": {
"title": "プライバシー第一",
"description": "私たちはあなたを追跡しません。安全なアカウントアクセスにより、メールボックスのプライバシーが保たれます。実際のメールアドレスをスパムや不要な購読から保護します。"
},
"speed": {
"title": "超高速",
"description": "一時メールアドレスを即座に取得でき、自動ポーリングでリアルタイムのメール通知を受信します。クイックセットアップ - サインアップするだけですぐに使用開始できます。"
},
"openSource": {
"title": "オープンソース&無料",
"description": "私たちのコードはGitHubでオープンソースです。Cloudflare上に構築されており、完全無料でセルフホスティングが可能です。データの取り扱い方法を確認でき、自分でホストすることもできます。"
}
},
"mission": {
"title": "私たちのミッション",
"content": "デジタルプライバシーが常に脅威にさらされ、スパムが日常的な迷惑となっている時代において、MoeMail はシンプルで効果的なソリューションを提供することを目指しています。私たちは、誰もがクリーンな受信トレイを持つ権利があり、一時的なサービスに登録したり信頼できないウェブサイトをテストしたりする際に個人のメールアドレスを非公開に保つ権利があると信じています。MoeMail は最新のウェブ技術で構築され、Cloudflare のエッジネットワークによって支えられており、高速で安全、完全無料の一時メールサービスをかわいくフレンドリーなインターフェースで提供します。"
},
"techStack": {
"title": "技術スタック",
"content": "MoeMail は最先端技術で構築されています:フレームワークに Next.js、ホスティングに Cloudflare Pages、データベースに D1、認証に NextAuth、スタイリングに Tailwind CSS、型安全なデータベース操作に Drizzle ORM を使用しています。next-intl を通じて多言語をサポートし、Radix UI コンポーネントに基づいた最新のレスポンシブ UI を提供しています。"
},
"contact": {
"title": "参加する",
"intro": "MoeMail はオープンソースプロジェクトです。提案、サポート、または貢献をご希望の場合は、",
"github": "GitHub リポジトリ",
"joinText": "をご覧ください。",
"telegram": "Telegram @moecloudflare",
"outro": "のコミュニティに参加して最新情報を入手してください。"
}
}

View File

@@ -0,0 +1,6 @@
{
"title": "ブログ",
"subtitle": "MoeMail からの最新アップデートと洞察",
"readMore": "続きを読む",
"backToBlog": "ブログに戻る"
}

View File

@@ -13,8 +13,17 @@
"login": "ログイン",
"profile": "プロフィール",
"logout": "ログアウト",
"backToMailbox": "メールボックスに戻る"
"backToMailbox": "メールボックスに戻る",
"about": "私たちについて",
"privacy": "プライバシーポリシー",
"terms": "利用規約",
"blog": "ブログ"
},
"footer": {
"copyright": "© {year} MoeMail. All rights reserved.",
"contact": "お問い合わせ",
"github": "GitHub",
"telegram": "Telegram"
},
"github": "ソースコードを入手"
}
}

View File

@@ -1,27 +1,76 @@
{
"title": "MoeMail",
"subtitle": "かわいい使い捨てメールサービス",
"subtitle": "かわいい一時メールサービス",
"features": {
"privacy": {
"title": "プライバシー保護",
"description": "本物のメールアドレスを保護します"
"description": "実際のメールアドレスを保護"
},
"instant": {
"title": "メールボックス共有",
"description": "メールボックスを他の人と共有できます"
"title": "メール共有",
"description": "メールボックスを他の人と共有"
},
"expiry": {
"title": "自動期限切れ",
"description": "有効期限が切れると自動的に無効になります"
"description": "期限がると自動的に無効"
},
"openapi": {
"title": "オープンAPI",
"title": "オープン API",
"description": "完全な OpenAPI インターフェースを提供"
}
},
"actions": {
"enterMailbox": "メールボックスに入る",
"getStarted": "今すぐ始める"
"getStarted": "始める"
},
"howItWorks": {
"title": "使い方",
"step1": {
"title": "サインアップ",
"description": "個人ダッシュボードに安全にアクセスするためのアカウントを作成します。"
},
"step2": {
"title": "メールボックス作成",
"description": "カスタムまたはランダムな一時メールアドレスを即座に生成します。"
},
"step3": {
"title": "メール管理",
"description": "ダッシュボードからリアルタイムでメールを受信して表示します。"
},
"advStep1": {
"title": "Webhooks",
"description": "メールが届いたときに、自分のサーバーにリアルタイムで通知を受け取ります。"
},
"advStep2": {
"title": "OpenAPI",
"description": "標準 REST API を使用して MoeMail をアプリケーションに統合します。"
},
"advStep3": {
"title": "共有機能",
"description": "個別のメールまたはメールボックス全体を他の人と簡単に共有できます。"
}
},
"faq": {
"title": "よくある質問",
"q1": {
"question": "登録は必要ですか?",
"answer": "はい、一時メールボックスを安全に管理するには登録が必要です。"
},
"q2": {
"question": "メールはどのくらい保存されますか?",
"answer": "メールは一時的に保存され、期限切れ後に自動的に削除されます。"
},
"q3": {
"question": "メールを送信できますか?",
"answer": "適切な権限を持つユーザーは、メール送信機能を使用できます。"
},
"q4": {
"question": "複数のメールボックスを持てますか?",
"answer": "はい、1つのアカウントで複数の一時メールアドレスを作成および管理できます。"
},
"q5": {
"question": "OpenAPI にアクセスするには?",
"answer": "https://docs.moemail.app/api.html#openapi で OpenAPI 定義にアクセスできます。認証にはプロフィールから API キーが必要です。"
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"title": "プライバシーポリシー",
"lastUpdated": "最終更新日2024年12月9日",
"introduction": {
"title": "1. はじめに",
"content": "MoeMail へようこそ。私たちはあなたのプライバシーを尊重し、一時メールサービスを使用する際に個人データを収集しないことを表明します。"
},
"informationCollection": {
"title": "2. 情報収集",
"content": "安全な個人ダッシュボードを提供するために登録が必要です。アカウント管理のために最小限の情報ユーザー名などのみを収集します。IPアドレスの追跡は行いません。"
},
"emailContent": {
"title": "3. メール内容",
"content": "一時アドレスに受信したメールは、限られた時間24時間保存され、その後永久に削除されます。メールの内容を読んだり分析したりすることはありません。"
},
"thirdParty": {
"title": "4. サードパーティサービス",
"content": "ホスティングや分析Google Analyticsなどのためにサードパーティサービスを使用する場合があり、これらは匿名の使用データを収集する可能性があります。"
},
"contact": {
"title": "5. お問い合わせ",
"content": "このプライバシーポリシーについてご質問がある場合は、GitHubリポジトリを通じてお問い合わせください。"
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "利用規約",
"lastUpdated": "最終更新日2024年12月9日",
"acceptance": {
"title": "1. 規約の承諾",
"content": "MoeMail にアクセスして使用することにより、本契約の条項および規定に拘束されることに同意したものとみなされます。"
},
"description": {
"title": "2. サービスの説明",
"content": "MoeMail は一時的な使い捨てメールアドレスを提供します。このサービスは「現状のまま」提供され、合法的な目的のみを対象としています。"
},
"prohibited": {
"title": "3. 禁止事項",
"content": "以下の目的でサービスを使用しないことに同意します:",
"items": [
"スパムまたは迷惑メールの送信。",
"違法行為。",
"嫌がらせまたは悪用。"
]
},
"disclaimer": {
"title": "4. 免責事項",
"content": "サービスの信頼性または可用性について保証しません。メールは一時的なものであり、いつでも削除される可能性があります。"
},
"changes": {
"title": "5. 規約の変更",
"content": "いつでもこれらの規約を変更する権利を留保します。"
}
}

View File

@@ -0,0 +1,34 @@
{
"title": "MoeMail 소개",
"subtitle": "Next.js + Cloudflare 기술 스택으로 구축된 귀여운 임시 이메일 서비스, 개인정보를 보호하면서 간단하고 귀엽게 🎉",
"features": {
"privacy": {
"title": "개인정보 우선",
"description": "저희는 귀하를 추적하지 않습니다. 안전한 계정 액세스로 메일함의 개인정보를 보호합니다. 실제 이메일 주소를 스팸과 불필요한 구독으로부터 보호하세요."
},
"speed": {
"title": "초고속",
"description": "임시 이메일 주소를 즉시 받고 자동 폴링으로 실시간 이메일 알림을 받으세요. 빠른 설정 - 가입만 하면 바로 사용할 수 있습니다."
},
"openSource": {
"title": "오픈소스 & 무료",
"description": "저희 코드는 GitHub에서 오픈소스입니다. Cloudflare 기반으로 구축되어 완전 무료로 셀프 호스팅이 가능합니다. 데이터 처리 방법을 확인하고 직접 호스팅할 수도 있습니다."
}
},
"mission": {
"title": "우리의 미션",
"content": "디지털 개인정보가 지속적으로 위협받고 스팸이 일상적인 골칫거리가 된 시대에 MoeMail은 간단하고 효과적인 솔루션을 제공하는 것을 목표로 합니다. 우리는 모든 사람이 깨끗한 받은편지함을 가질 권리가 있으며, 일회성 서비스에 가입하거나 신뢰할 수 없는 웹사이트를 테스트할 때 개인 이메일 주소를 비공개로 유지할 권리가 있다고 믿습니다. MoeMail은 최신 웹 기술로 구축되고 Cloudflare 엣지 네트워크로 구동되어 빠르고 안전하며 완전 무료인 임시 이메일 서비스를 귀엽고 친근한 인터페이스로 제공합니다."
},
"techStack": {
"title": "기술 스택",
"content": "MoeMail은 최첨단 기술로 구축되었습니다: 프레임워크로 Next.js, 호스팅으로 Cloudflare Pages, 데이터베이스로 D1, 인증으로 NextAuth, 스타일링으로 Tailwind CSS, 타입 안전 데이터베이스 작업으로 Drizzle ORM을 사용합니다. next-intl을 통해 다국어를 지원하며 Radix UI 컴포넌트 기반의 현대적이고 반응형 UI를 제공합니다."
},
"contact": {
"title": "참여하기",
"intro": "MoeMail은 오픈소스 프로젝트입니다. 제안, 지원 또는 기여를 원하시면",
"github": "GitHub 저장소",
"joinText": "를 방문해 주세요.",
"telegram": "Telegram @moecloudflare",
"outro": "커뮤니티에 가입하여 최신 소식을 받아보세요."
}
}

View File

@@ -0,0 +1,6 @@
{
"title": "블로그",
"subtitle": "MoeMail의 최신 업데이트 및 인사이트",
"readMore": "더 읽기",
"backToBlog": "블로그로 돌아가기"
}

View File

@@ -13,7 +13,17 @@
"login": "로그인",
"profile": "프로필",
"logout": "로그아웃",
"backToMailbox": "메일함으로 돌아가기"
"backToMailbox": "메일함으로 돌아가기",
"about": "회사 소개",
"privacy": "개인정보 보호정책",
"terms": "서비스 약관",
"blog": "블로그"
},
"footer": {
"copyright": "© {year} MoeMail. All rights reserved.",
"contact": "문의하기",
"github": "GitHub",
"telegram": "Telegram"
},
"github": "소스 코드 받기"
}
}

View File

@@ -8,19 +8,69 @@
},
"instant": {
"title": "이메일 공유",
"description": "다른 사람과 메일함 공유"
"description": "메일함을 다른 사람과 공유"
},
"expiry": {
"title": "자동 만료",
"description": "기한이 되면 자동으로 만료"
"description": "기한이 되면 자동으로 무효화"
},
"openapi": {
"title": "Open API",
"description": "전체 OpenAPI 인터페이스 제공"
"title": "오픈 API",
"description": "완전한 OpenAPI 인터페이스 제공"
}
},
"actions": {
"enterMailbox": "메일함 입장",
"getStarted": "시작하기"
},
"howItWorks": {
"title": "사용 방법",
"step1": {
"title": "가입하기",
"description": "개인 대시보드에 안전하게 액세스하기 위한 계정을 만드세요."
},
"step2": {
"title": "메일함 생성",
"description": "맞춤형 또는 무작위 임시 이메일 주소를 즉시 생성합니다."
},
"step3": {
"title": "이메일 관리",
"description": "대시보드에서 실시간으로 이메일을 수신하고 확인합니다."
},
"advStep1": {
"title": "Webhooks",
"description": "이메일이 도착하면 자신의 서버로 실시간 알림을 받습니다."
},
"advStep2": {
"title": "OpenAPI",
"description": "표준 REST API를 사용하여 MoeMail을 애플리케이션에 통합합니다."
},
"advStep3": {
"title": "공유 기능",
"description": "개별 이메일 또는 전체 메일함을 다른 사람과 쉽게 공유할 수 있습니다."
}
},
"faq": {
"title": "자주 묻는 질문",
"q1": {
"question": "등록이 필요한가요?",
"answer": "네, 임시 메일함을 안전하게 관리하려면 등록이 필요합니다."
},
"q2": {
"question": "이메일은 얼마나 보관되나요?",
"answer": "이메일은 임시로 저장되며 만료 후 자동으로 삭제됩니다."
},
"q3": {
"question": "이메일을 보낼 수 있나요?",
"answer": "적절한 권한을 가진 사용자는 이메일 전송 기능을 사용할 수 있습니다."
},
"q4": {
"question": "여러 개의 메일함을 가질 수 있나요?",
"answer": "네, 하나의 계정에서 여러 임시 이메일 주소를 생성하고 관리할 수 있습니다."
},
"q5": {
"question": "OpenAPI에 어떻게 액세스하나요?",
"answer": "https://docs.moemail.app/api.html#openapi에서 OpenAPI 정의에 액세스할 수 있습니다. 인증에는 프로필의 API 키가 필요합니다."
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"title": "개인정보 보호정책",
"lastUpdated": "최종 업데이트: 2024년 12월 9일",
"introduction": {
"title": "1. 소개",
"content": "MoeMail에 오신 것을 환영합니다. 저희는 귀하의 개인정보를 존중하며 임시 이메일 서비스를 사용할 때 개인 데이터를 수집하지 않습니다."
},
"informationCollection": {
"title": "2. 정보 수집",
"content": "안전한 개인 대시보드를 제공하기 위해 등록이 필요합니다. 계정 관리를 위해 최소한의 정보(사용자 이름 등)만 수집합니다. IP 주소를 추적하지 않습니다."
},
"emailContent": {
"title": "3. 이메일 내용",
"content": "임시 주소로 수신된 이메일은 제한된 시간(예: 24시간) 동안 저장된 후 영구적으로 삭제됩니다. 이메일 내용을 읽거나 분석하지 않습니다."
},
"thirdParty": {
"title": "4. 제3자 서비스",
"content": "호스팅 또는 분석(Google Analytics 등)을 위해 제3자 서비스를 사용할 수 있으며, 이들은 익명 사용 데이터를 수집할 수 있습니다."
},
"contact": {
"title": "5. 문의하기",
"content": "이 개인정보 보호정책에 대해 질문이 있으시면 GitHub 저장소를 통해 문의해 주세요."
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "서비스 약관",
"lastUpdated": "최종 업데이트: 2024년 12월 9일",
"acceptance": {
"title": "1. 약관 동의",
"content": "MoeMail에 접근하고 사용함으로써 본 계약의 조건 및 규정에 구속되는 것에 동의합니다."
},
"description": {
"title": "2. 서비스 설명",
"content": "MoeMail은 임시 일회용 이메일 주소를 제공합니다. 이 서비스는 \"있는 그대로\" 제공되며 합법적인 목적으로만 사용됩니다."
},
"prohibited": {
"title": "3. 금지 사항",
"content": "다음 목적으로 서비스를 사용하지 않는 데 동의합니다:",
"items": [
"스팸 또는 원치 않는 이메일 발송.",
"불법 활동.",
"괴롭힘 또는 악용."
]
},
"disclaimer": {
"title": "4. 면책 조항",
"content": "서비스의 신뢰성 또는 가용성에 대해 보증하지 않습니다. 이메일은 임시적이며 언제든지 삭제될 수 있습니다."
},
"changes": {
"title": "5. 약관 변경",
"content": "언제든지 이 약관을 수정할 권리를 보유합니다."
}
}

View File

@@ -0,0 +1,34 @@
{
"title": "关于 MoeMail",
"subtitle": "一个基于 Next.js + Cloudflare 技术栈构建的可爱临时邮箱服务,在保护您的隐私的同时保持简单可爱 🎉",
"features": {
"privacy": {
"title": "隐私优先",
"description": "我们不会追踪您。安全的账户访问确保您的邮箱保持私密。保护您的真实邮箱地址,远离垃圾邮件和不必要的订阅。"
},
"speed": {
"title": "闪电般快速",
"description": "即刻获取您的临时邮箱地址,自动轮询实时接收邮件通知。快速设置,只需注册即可立即开始使用。"
},
"openSource": {
"title": "开源且免费",
"description": "我们的代码在 GitHub 上开源。基于 Cloudflare 构建,可实现免费自部署,无需任何费用。您可以验证我们如何处理您的数据,甚至可以自己托管。"
}
},
"mission": {
"title": "我们的使命",
"content": "在数字隐私不断受到威胁、垃圾邮件成为日常困扰的时代MoeMail 旨在提供一个简单有效的解决方案。我们相信每个人都应该拥有一个干净的收件箱并有权在注册一次性服务或测试不受信任的网站时保持个人电子邮件地址的私密性。MoeMail 采用现代 Web 技术构建,由 Cloudflare 边缘网络提供支持,提供快速、安全且完全免费的临时邮箱服务,界面可爱友好。"
},
"techStack": {
"title": "技术栈",
"content": "MoeMail 采用前沿技术构建Next.js 框架、Cloudflare Pages 托管、D1 数据库、NextAuth 认证、Tailwind CSS 样式、Drizzle ORM 类型安全数据库操作。我们通过 next-intl 支持多语言,并基于 Radix UI 组件提供现代化响应式界面。"
},
"contact": {
"title": "参与其中",
"intro": "MoeMail 是一个开源项目。如果您有建议、需要支持或想要贡献,请访问我们的",
"github": "GitHub 仓库",
"joinText": "。加入我们的",
"telegram": "Telegram 社区 @moecloudflare",
"outro": "以获取最新动态。"
}
}

View File

@@ -0,0 +1,6 @@
{
"title": "博客",
"subtitle": "来自 MoeMail 的最新更新和见解",
"readMore": "阅读更多",
"backToBlog": "返回博客"
}

View File

@@ -13,7 +13,17 @@
"login": "登录",
"profile": "个人中心",
"logout": "退出登录",
"backToMailbox": "返回邮箱"
"backToMailbox": "返回邮箱",
"about": "关于我们",
"privacy": "隐私政策",
"terms": "服务条款",
"blog": "博客"
},
"footer": {
"copyright": "© {year} MoeMail. 版权所有。",
"contact": "联系方式",
"github": "GitHub",
"telegram": "Telegram"
},
"github": "获取网站源代码"
}
}

View File

@@ -22,6 +22,55 @@
"actions": {
"enterMailbox": "进入邮箱",
"getStarted": "开始使用"
},
"howItWorks": {
"title": "如何使用",
"step1": {
"title": "注册账号",
"description": "创建一个账号以安全地访问您的个人控制台。"
},
"step2": {
"title": "创建邮箱",
"description": "立即生成自定义或随机的临时邮箱地址。"
},
"step3": {
"title": "管理邮件",
"description": "在控制台中实时接收和查看您的邮件。"
},
"advStep1": {
"title": "Webhooks",
"description": "当邮件到达时,实时向您的服务器发送通知。"
},
"advStep2": {
"title": "OpenAPI",
"description": "使用标准 REST API 将 MoeMail 集成到您的应用中。"
},
"advStep3": {
"title": "分享功能",
"description": "轻松与他人分享单个邮件或整个邮箱。"
}
},
"faq": {
"title": "常见问题",
"q1": {
"question": "我需要注册吗?",
"answer": "是的,为了安全地管理您的临时邮箱,需要注册账号。"
},
"q2": {
"question": "邮件会保存多久?",
"answer": "邮件会暂时存储,并在过期后自动删除。"
},
"q3": {
"question": "我可以发送邮件吗?",
"answer": "拥有相应权限的用户可以使用发送邮件功能。"
},
"q4": {
"question": "我可以拥有多个邮箱吗?",
"answer": "是的,您可以在一个账号下创建并管理多个临时邮箱地址。"
},
"q5": {
"question": "如何访问 OpenAPI",
"answer": "您可以访问 https://docs.moemail.app/api.html#openapi 查看 OpenAPI 文档。认证需要使用个人资料中的 API 密钥。"
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"title": "隐私政策",
"lastUpdated": "最后更新2024年12月9日",
"introduction": {
"title": "1. 简介",
"content": "欢迎使用 MoeMail。我们尊重您的隐私并承诺在您使用我们的临时邮箱服务时不会收集您的任何个人数据。"
},
"informationCollection": {
"title": "2. 信息收集",
"content": "我们需要您注册以提供安全的个人仪表板。我们仅收集最少的信息(如用户名)用于账户管理。我们不会追踪您的 IP 地址。"
},
"emailContent": {
"title": "3. 邮件内容",
"content": "发送到您临时地址的邮件会在有限时间内(例如 24 小时)存储,然后永久删除。我们不会阅读或分析您的邮件内容。"
},
"thirdParty": {
"title": "4. 第三方服务",
"content": "我们可能会使用第三方服务进行托管或分析(如 Google Analytics这些服务可能会收集匿名使用数据。"
},
"contact": {
"title": "5. 联系我们",
"content": "如果您对本隐私政策有任何疑问,请通过我们的 GitHub 仓库联系我们。"
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "服务条款",
"lastUpdated": "最后更新2024年12月9日",
"acceptance": {
"title": "1. 接受条款",
"content": "通过访问和使用 MoeMail您接受并同意受本协议条款和条文的约束。"
},
"description": {
"title": "2. 服务说明",
"content": "MoeMail 提供临时一次性电子邮件地址。本服务按\"原样\"提供,仅用于合法目的。"
},
"prohibited": {
"title": "3. 禁止使用",
"content": "您同意不将本服务用于:",
"items": [
"发送垃圾邮件或未经请求的邮件。",
"非法活动。",
"骚扰或滥用。"
]
},
"disclaimer": {
"title": "4. 免责声明",
"content": "我们不对服务的可靠性或可用性做出任何保证。邮件是临时的,可能随时被删除。"
},
"changes": {
"title": "5. 条款变更",
"content": "我们保留随时修改这些条款的权利。"
}
}

View File

@@ -0,0 +1,34 @@
{
"title": "關於 MoeMail",
"subtitle": "一個基於 Next.js + Cloudflare 技術棧構建的可愛臨時郵箱服務,在保護您的隱私的同時保持簡單可愛 🎉",
"features": {
"privacy": {
"title": "隱私優先",
"description": "我們不會追蹤您。安全的帳戶存取確保您的郵箱保持私密。保護您的真實郵箱地址,遠離垃圾郵件和不必要的訂閱。"
},
"speed": {
"title": "閃電般快速",
"description": "即刻獲取您的臨時郵箱地址,自動輪詢即時接收郵件通知。快速設定,只需註冊即可立即開始使用。"
},
"openSource": {
"title": "開源且免費",
"description": "我們的程式碼在 GitHub 上開源。基於 Cloudflare 構建,可實現免費自部署,無需任何費用。您可以驗證我們如何處理您的資料,甚至可以自己託管。"
}
},
"mission": {
"title": "我們的使命",
"content": "在數位隱私不斷受到威脅、垃圾郵件成為日常困擾的時代MoeMail 旨在提供一個簡單有效的解決方案。我們相信每個人都應該擁有一個乾淨的收件匣並有權在註冊一次性服務或測試不受信任的網站時保持個人電子郵件地址的私密性。MoeMail 採用現代 Web 技術構建,由 Cloudflare 邊緣網路提供支援,提供快速、安全且完全免費的臨時郵箱服務,介面可愛友好。"
},
"techStack": {
"title": "技術棧",
"content": "MoeMail 採用前沿技術構建Next.js 框架、Cloudflare Pages 託管、D1 資料庫、NextAuth 認證、Tailwind CSS 樣式、Drizzle ORM 型別安全資料庫操作。我們透過 next-intl 支援多語言,並基於 Radix UI 元件提供現代化響應式介面。"
},
"contact": {
"title": "參與其中",
"intro": "MoeMail 是一個開源專案。如果您有建議、需要支援或想要貢獻,請造訪我們的",
"github": "GitHub 儲存庫",
"joinText": "。加入我們的",
"telegram": "Telegram 社群 @moecloudflare",
"outro": "以獲取最新動態。"
}
}

View File

@@ -0,0 +1,6 @@
{
"title": "部落格",
"subtitle": "來自 MoeMail 的最新更新和見解",
"readMore": "閱讀更多",
"backToBlog": "返回部落格"
}

View File

@@ -13,7 +13,17 @@
"login": "登入",
"profile": "個人中心",
"logout": "登出",
"backToMailbox": "返回郵箱"
"backToMailbox": "返回郵箱",
"about": "關於我們",
"privacy": "隱私政策",
"terms": "服務條款",
"blog": "部落格"
},
"footer": {
"copyright": "© {year} MoeMail. 版權所有。",
"contact": "聯絡方式",
"github": "GitHub",
"telegram": "Telegram"
},
"github": "取得網站原始碼"
}
}

View File

@@ -22,6 +22,55 @@
"actions": {
"enterMailbox": "進入郵箱",
"getStarted": "開始使用"
},
"howItWorks": {
"title": "如何使用",
"step1": {
"title": "註冊帳號",
"description": "建立一個帳號以安全地存取您的個人控制台。"
},
"step2": {
"title": "建立郵箱",
"description": "立即產生自訂或隨機的臨時郵箱地址。"
},
"step3": {
"title": "管理郵件",
"description": "在控制台中即時接收和檢視您的郵件。"
},
"advStep1": {
"title": "Webhooks",
"description": "當郵件到達時,即時向您的伺服器傳送通知。"
},
"advStep2": {
"title": "OpenAPI",
"description": "使用標準 REST API 將 MoeMail 整合到您的應用程式中。"
},
"advStep3": {
"title": "分享功能",
"description": "輕鬆與他人分享單個郵件或整個郵箱。"
}
},
"faq": {
"title": "常見問題",
"q1": {
"question": "我需要註冊嗎?",
"answer": "是的,為了安全地管理您的臨時郵箱,需要註冊帳號。"
},
"q2": {
"question": "郵件會儲存多久?",
"answer": "郵件會暫時儲存,並在過期後自動刪除。"
},
"q3": {
"question": "我可以傳送郵件嗎?",
"answer": "擁有相應權限的使用者可以使用傳送郵件功能。"
},
"q4": {
"question": "我可以擁有多個郵箱嗎?",
"answer": "是的,您可以在一個帳號下建立並管理多個臨時郵箱地址。"
},
"q5": {
"question": "如何存取 OpenAPI",
"answer": "您可以存取 https://docs.moemail.app/api.html#openapi 檢視 OpenAPI 文件。認證需要使用個人資料中的 API 金鑰。"
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"title": "隱私政策",
"lastUpdated": "最後更新2024年12月9日",
"introduction": {
"title": "1. 簡介",
"content": "歡迎使用 MoeMail。我們尊重您的隱私並承諾在您使用我們的臨時郵箱服務時不會收集您的任何個人資料。"
},
"informationCollection": {
"title": "2. 資訊收集",
"content": "我們需要您註冊以提供安全的個人儀表板。我們僅收集最少的資訊(如使用者名稱)用於帳戶管理。我們不會追蹤您的 IP 位址。"
},
"emailContent": {
"title": "3. 郵件內容",
"content": "發送到您臨時地址的郵件會在有限時間內(例如 24 小時)儲存,然後永久刪除。我們不會閱讀或分析您的郵件內容。"
},
"thirdParty": {
"title": "4. 第三方服務",
"content": "我們可能會使用第三方服務進行託管或分析(如 Google Analytics這些服務可能會收集匿名使用資料。"
},
"contact": {
"title": "5. 聯絡我們",
"content": "如果您對本隱私政策有任何疑問,請透過我們的 GitHub 儲存庫聯絡我們。"
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "服務條款",
"lastUpdated": "最後更新2024年12月9日",
"acceptance": {
"title": "1. 接受條款",
"content": "透過存取和使用 MoeMail您接受並同意受本協議條款和條文的約束。"
},
"description": {
"title": "2. 服務說明",
"content": "MoeMail 提供臨時一次性電子郵件地址。本服務按「原樣」提供,僅用於合法目的。"
},
"prohibited": {
"title": "3. 禁止使用",
"content": "您同意不將本服務用於:",
"items": [
"發送垃圾郵件或未經請求的郵件。",
"非法活動。",
"騷擾或濫用。"
]
},
"disclaimer": {
"title": "4. 免責聲明",
"content": "我們不對服務的可靠性或可用性做出任何保證。郵件是臨時的,可能隨時被刪除。"
},
"changes": {
"title": "5. 條款變更",
"content": "我們保留隨時修改這些條款的權利。"
}
}

View File

@@ -1,7 +1,7 @@
import {getRequestConfig} from 'next-intl/server'
import {i18n} from '@/i18n/config'
import { getRequestConfig } from 'next-intl/server'
import { i18n } from '@/i18n/config'
export default getRequestConfig(async ({locale}) => {
export default getRequestConfig(async ({ locale }) => {
const safeLocale = (i18n.locales.includes(locale as any) ? locale : i18n.defaultLocale) as string
try {
const common = (await import(`@/i18n/messages/${safeLocale}/common.json`)).default
@@ -10,9 +10,12 @@ export default getRequestConfig(async ({locale}) => {
const metadata = (await import(`@/i18n/messages/${safeLocale}/metadata.json`)).default
const emails = (await import(`@/i18n/messages/${safeLocale}/emails.json`)).default
const profile = (await import(`@/i18n/messages/${safeLocale}/profile.json`)).default
return {locale: safeLocale, messages: {common, home, auth, metadata, emails, profile}}
const privacy = (await import(`@/i18n/messages/${safeLocale}/privacy.json`)).default
const terms = (await import(`@/i18n/messages/${safeLocale}/terms.json`)).default
const about = (await import(`@/i18n/messages/${safeLocale}/about.json`)).default
const blog = (await import(`@/i18n/messages/${safeLocale}/blog.json`)).default
return { locale: safeLocale, messages: { common, home, auth, metadata, emails, profile, privacy, terms, about, blog } }
} catch {
return {locale: safeLocale, messages: {common: {}, home: {}, auth: {}, metadata: {}, emails: {}, profile: {}}}
return { locale: safeLocale, messages: { common: {}, home: {}, auth: {}, metadata: {}, emails: {}, profile: {}, privacy: {}, terms: {}, about: {}, blog: {} } }
}
})

290
app/lib/blog-data.ts Normal file
View File

@@ -0,0 +1,290 @@
export interface BlogPost {
slug: string
title: string
excerpt: string
date: string
content: string
}
type LocalizedBlogPosts = {
[locale: string]: BlogPost[]
}
const blogPostsData: LocalizedBlogPosts = {
en: [
{
slug: "why-use-moemail",
title: "Why Use MoeMail Temporary Email?",
excerpt: "Discover how MoeMail protects your privacy with a cute, fast, and completely free temporary email service built on modern technology.",
date: "2024-12-10",
content: `
<h2>The Problem with Your Real Email</h2>
<p>Every time you sign up for a newsletter, download an ebook, or try a new service, you hand over your email address. This often leads to:</p>
<ul>
<li>Endless spam and unwanted marketing emails</li>
<li>Data breaches exposing your personal information</li>
<li>Cross-site tracking and targeted advertising</li>
<li>A cluttered inbox that's hard to manage</li>
</ul>
<h2>What is MoeMail?</h2>
<p>MoeMail is a cute temporary email service built with Next.js and Cloudflare technology stack. It provides disposable email addresses that protect your real inbox from spam and unwanted messages. With MoeMail, you can create temporary email addresses instantly and receive emails in real-time.</p>
<h2>Why Choose MoeMail?</h2>
<h3>🔒 Privacy First</h3>
<p>We don't track you. Your mailbox is protected by secure account authentication, ensuring only you have access. Unlike other services, we prioritize your privacy above all else.</p>
<h3>⚡ Lightning Fast</h3>
<p>Built on Cloudflare's global edge network, MoeMail delivers emails instantly. Our automatic polling system ensures you receive notifications in real-time without refreshing the page.</p>
<h3>💰 Completely Free</h3>
<p>MoeMail is open source and can be self-hosted on Cloudflare for free. No hidden costs, no premium tiers for basic features. Everything you need is available at no charge.</p>
<h3>🌍 Multi-Language Support</h3>
<p>MoeMail supports English, Simplified Chinese, Traditional Chinese, Japanese, and Korean. Use the service in your preferred language.</p>
<h3>🔌 Developer Friendly</h3>
<p>MoeMail provides a complete OpenAPI interface and webhook support. Integrate temporary email functionality into your applications with ease using our REST API.</p>
<h3>📱 Modern Design</h3>
<p>Enjoy a beautiful, responsive interface that works perfectly on desktop, tablet, and mobile devices. Dark mode is available for comfortable viewing at any time.</p>
<h2>When to Use MoeMail</h2>
<ul>
<li>Signing up for one-time services or trials</li>
<li>Downloading content that requires email verification</li>
<li>Testing websites during development</li>
<li>Protecting your identity on public forums</li>
<li>Avoiding marketing emails from untrusted sources</li>
</ul>
<h2>Get Started Today</h2>
<p>Join thousands of users who trust MoeMail to protect their privacy. Create your first temporary email address in seconds and experience the difference. Your real inbox will thank you!</p>
`
}
],
"zh-CN": [
{
slug: "why-use-moemail",
title: "为什么使用 MoeMail 临时邮箱?",
excerpt: "了解 MoeMail 如何通过可爱、快速且完全免费的临时邮箱服务保护您的隐私,该服务基于现代技术构建。",
date: "2024-12-10",
content: `
<h2>真实邮箱面临的问题</h2>
<p>每次您注册新闻通讯、下载电子书或尝试新服务时,都会提供您的电子邮件地址。这通常会导致:</p>
<ul>
<li>无尽的垃圾邮件和不需要的营销邮件</li>
<li>数据泄露暴露您的个人信息</li>
<li>跨站点追踪和定向广告</li>
<li>杂乱无章、难以管理的收件箱</li>
</ul>
<h2>什么是 MoeMail</h2>
<p>MoeMail 是一个基于 Next.js 和 Cloudflare 技术栈构建的可爱临时邮箱服务。它提供一次性电子邮件地址,保护您的真实收件箱免受垃圾邮件和不需要的消息的侵扰。使用 MoeMail您可以即时创建临时邮箱地址并实时接收邮件。</p>
<h2>为什么选择 MoeMail</h2>
<h3>🔒 隐私优先</h3>
<p>我们不会追踪您。您的邮箱受到安全账户认证保护,确保只有您可以访问。与其他服务不同,我们将您的隐私放在首位。</p>
<h3>⚡ 闪电般快速</h3>
<p>基于 Cloudflare 的全球边缘网络构建MoeMail 能够即时送达邮件。我们的自动轮询系统确保您无需刷新页面即可实时收到通知。</p>
<h3>💰 完全免费</h3>
<p>MoeMail 是开源的,可以在 Cloudflare 上免费自托管。没有隐藏费用,基本功能没有付费等级。您需要的一切都免费提供。</p>
<h3>🌍 多语言支持</h3>
<p>MoeMail 支持英语、简体中文、繁体中文、日语和韩语。使用您喜欢的语言享受服务。</p>
<h3>🔌 对开发者友好</h3>
<p>MoeMail 提供完整的 OpenAPI 接口和 Webhook 支持。使用我们的 REST API 轻松将临时邮箱功能集成到您的应用程序中。</p>
<h3>📱 现代化设计</h3>
<p>享受美观、响应式的界面,在桌面、平板和移动设备上都能完美运行。提供深色模式,随时舒适浏览。</p>
<h2>何时使用 MoeMail</h2>
<ul>
<li>注册一次性服务或试用</li>
<li>下载需要邮件验证的内容</li>
<li>开发过程中测试网站</li>
<li>在公共论坛保护您的身份</li>
<li>避免来自不信任来源的营销邮件</li>
</ul>
<h2>立即开始</h2>
<p>加入成千上万信任 MoeMail 保护隐私的用户。几秒钟内创建您的第一个临时邮箱地址,体验不同。您的真实收件箱会感谢您的!</p>
`
}
],
"zh-TW": [
{
slug: "why-use-moemail",
title: "為什麼使用 MoeMail 臨時郵箱?",
excerpt: "了解 MoeMail 如何透過可愛、快速且完全免費的臨時郵箱服務保護您的隱私,該服務基於現代技術構建。",
date: "2024-12-10",
content: `
<h2>真實郵箱面臨的問題</h2>
<p>每次您註冊新聞通訊、下載電子書或嘗試新服務時,都會提供您的電子郵件地址。這通常會導致:</p>
<ul>
<li>無盡的垃圾郵件和不需要的行銷郵件</li>
<li>資料洩露暴露您的個人資訊</li>
<li>跨站點追蹤和定向廣告</li>
<li>雜亂無章、難以管理的收件匣</li>
</ul>
<h2>什麼是 MoeMail</h2>
<p>MoeMail 是一個基於 Next.js 和 Cloudflare 技術棧構建的可愛臨時郵箱服務。它提供一次性電子郵件地址,保護您的真實收件匣免受垃圾郵件和不需要的訊息的侵擾。使用 MoeMail您可以即時建立臨時郵箱地址並即時接收郵件。</p>
<h2>為什麼選擇 MoeMail</h2>
<h3>🔒 隱私優先</h3>
<p>我們不會追蹤您。您的郵箱受到安全帳戶認證保護,確保只有您可以存取。與其他服務不同,我們將您的隱私放在首位。</p>
<h3>⚡ 閃電般快速</h3>
<p>基於 Cloudflare 的全球邊緣網路構建MoeMail 能夠即時送達郵件。我們的自動輪詢系統確保您無需重新整理頁面即可即時收到通知。</p>
<h3>💰 完全免費</h3>
<p>MoeMail 是開源的,可以在 Cloudflare 上免費自託管。沒有隱藏費用,基本功能沒有付費等級。您需要的一切都免費提供。</p>
<h3>🌍 多語言支援</h3>
<p>MoeMail 支援英語、簡體中文、繁體中文、日語和韓語。使用您喜歡的語言享受服務。</p>
<h3>🔌 對開發者友好</h3>
<p>MoeMail 提供完整的 OpenAPI 介面和 Webhook 支援。使用我們的 REST API 輕鬆將臨時郵箱功能整合到您的應用程式中。</p>
<h3>📱 現代化設計</h3>
<p>享受美觀、響應式的介面,在桌面、平板和行動裝置上都能完美運作。提供深色模式,隨時舒適瀏覽。</p>
<h2>何時使用 MoeMail</h2>
<ul>
<li>註冊一次性服務或試用</li>
<li>下載需要郵件驗證的內容</li>
<li>開發過程中測試網站</li>
<li>在公共論壇保護您的身份</li>
<li>避免來自不信任來源的行銷郵件</li>
</ul>
<h2>立即開始</h2>
<p>加入成千上萬信任 MoeMail 保護隱私的使用者。幾秒鐘內建立您的第一個臨時郵箱地址,體驗不同。您的真實收件匣會感謝您的!</p>
`
}
],
ja: [
{
slug: "why-use-moemail",
title: "なぜ MoeMail 一時メールを使うのか?",
excerpt: "MoeMail がかわいく、高速で、完全無料の一時メールサービスでどのようにプライバシーを保護するかをご紹介します。",
date: "2024-12-10",
content: `
<h2>本当のメールアドレスの問題</h2>
<p>ニュースレターの登録、電子書籍のダウンロード、新しいサービスの試用のたびに、メールアドレスを提供します。これは次のような問題を引き起こします:</p>
<ul>
<li>終わりのないスパムと不要なマーケティングメール</li>
<li>個人情報を露出するデータ侵害</li>
<li>クロスサイトトラッキングとターゲット広告</li>
<li>管理が難しい乱雑な受信トレイ</li>
</ul>
<h2>MoeMail とは?</h2>
<p>MoeMail は Next.js と Cloudflare テクロジースタックで構築されたかわいい一時メールサービスです。スパムや不要なメッセージから実際の受信トレイを保護する使い捨てメールアドレスを提供します。MoeMail を使えば、一時メールアドレスを即座に作成し、リアルタイムでメールを受信できます。</p>
<h2>なぜ MoeMail を選ぶ?</h2>
<h3>🔒 プライバシー第一</h3>
<p>私たちはあなたを追跡しません。メールボックスは安全なアカウント認証で保護され、あなただけがアクセスできます。他のサービスとは異なり、私たちはあなたのプライバシーを最優先にします。</p>
<h3>⚡ 超高速</h3>
<p>Cloudflare のグローバルエッジネットワーク上に構築され、MoeMail は即座にメールを配信します。自動ポーリングシステムにより、ページを更新せずにリアルタイムで通知を受け取れます。</p>
<h3>💰 完全無料</h3>
<p>MoeMail はオープンソースで、Cloudflare 上で無料でセルフホスティングできます。隠れた費用なし、基本機能のプレミアムティアなし。必要なものすべてが無料で利用できます。</p>
<h3>🌍 多言語サポート</h3>
<p>MoeMail は英語、簡体字中国語、繁体字中国語、日本語、韓国語をサポートしています。お好みの言語でサービスをご利用ください。</p>
<h3>🔌 開発者フレンドリー</h3>
<p>MoeMail は完全な OpenAPI インターフェースと Webhook サポートを提供します。REST API を使用して、アプリケーションに一時メール機能を簡単に統合できます。</p>
<h3>📱 モダンなデザイン</h3>
<p>デスクトップ、タブレット、モバイルデバイスで完璧に動作する美しいレスポンシブインターフェースをお楽しみください。いつでも快適に閲覧できるダークモードも利用可能です。</p>
<h2>MoeMail を使うタイミング</h2>
<ul>
<li>一回限りのサービスやトライアルへの登録</li>
<li>メール認証が必要なコンテンツのダウンロード</li>
<li>開発中のウェブサイトテスト</li>
<li>公開フォーラムでの身元保護</li>
<li>信頼できないソースからのマーケティングメール回避</li>
</ul>
<h2>今すぐ始めましょう</h2>
<p>プライバシー保護に MoeMail を信頼する何千ものユーザーに参加しましょう。数秒で最初の一時メールアドレスを作成し、違いを体験してください。あなたの本当の受信トレイが感謝するでしょう!</p>
`
}
],
ko: [
{
slug: "why-use-moemail",
title: "왜 MoeMail 임시 이메일을 사용하나요?",
excerpt: "MoeMail이 귀엽고 빠르며 완전 무료인 임시 이메일 서비스로 어떻게 개인정보를 보호하는지 알아보세요.",
date: "2024-12-10",
content: `
<h2>실제 이메일의 문제점</h2>
<p>뉴스레터 가입, 전자책 다운로드, 새로운 서비스 시도 시 매번 이메일 주소를 제공합니다. 이는 다음과 같은 문제를 일으킵니다:</p>
<ul>
<li>끝없는 스팸과 원치 않는 마케팅 이메일</li>
<li>개인 정보를 노출하는 데이터 침해</li>
<li>크로스 사이트 추적과 타겟 광고</li>
<li>관리하기 어려운 복잡한 받은편지함</li>
</ul>
<h2>MoeMail이란?</h2>
<p>MoeMail은 Next.js와 Cloudflare 기술 스택으로 구축된 귀여운 임시 이메일 서비스입니다. 스팸과 원치 않는 메시지로부터 실제 받은편지함을 보호하는 일회용 이메일 주소를 제공합니다. MoeMail을 사용하면 임시 이메일 주소를 즉시 만들고 실시간으로 이메일을 받을 수 있습니다.</p>
<h2>왜 MoeMail을 선택하나요?</h2>
<h3>🔒 개인정보 우선</h3>
<p>저희는 귀하를 추적하지 않습니다. 메일함은 안전한 계정 인증으로 보호되어 오직 귀하만 접근할 수 있습니다. 다른 서비스와 달리 저희는 귀하의 개인정보를 최우선으로 합니다.</p>
<h3>⚡ 초고속</h3>
<p>Cloudflare의 글로벌 엣지 네트워크에 구축되어 MoeMail은 즉시 이메일을 전달합니다. 자동 폴링 시스템으로 페이지를 새로고침하지 않고도 실시간 알림을 받을 수 있습니다.</p>
<h3>💰 완전 무료</h3>
<p>MoeMail은 오픈소스이며 Cloudflare에서 무료로 셀프 호스팅할 수 있습니다. 숨겨진 비용 없음, 기본 기능에 대한 프리미엄 티어 없음. 필요한 모든 것이 무료로 제공됩니다.</p>
<h3>🌍 다국어 지원</h3>
<p>MoeMail은 영어, 중국어 간체, 중국어 번체, 일본어, 한국어를 지원합니다. 선호하는 언어로 서비스를 이용하세요.</p>
<h3>🔌 개발자 친화적</h3>
<p>MoeMail은 완전한 OpenAPI 인터페이스와 Webhook 지원을 제공합니다. REST API를 사용하여 애플리케이션에 임시 이메일 기능을 쉽게 통합하세요.</p>
<h3>📱 현대적 디자인</h3>
<p>데스크톱, 태블릿, 모바일 기기에서 완벽하게 작동하는 아름답고 반응형 인터페이스를 즐기세요. 언제든 편안하게 볼 수 있는 다크 모드도 제공됩니다.</p>
<h2>MoeMail 사용 시기</h2>
<ul>
<li>일회성 서비스나 체험판 가입</li>
<li>이메일 인증이 필요한 콘텐츠 다운로드</li>
<li>개발 중 웹사이트 테스트</li>
<li>공개 포럼에서 신원 보호</li>
<li>신뢰할 수 없는 소스의 마케팅 이메일 회피</li>
</ul>
<h2>지금 시작하세요</h2>
<p>개인정보 보호를 위해 MoeMail을 신뢰하는 수천 명의 사용자와 함께하세요. 몇 초 만에 첫 임시 이메일 주소를 만들고 차이를 경험하세요. 실제 받은편지함이 감사할 것입니다!</p>
`
}
]
}
export function getBlogPosts(locale: string = "en"): BlogPost[] {
return blogPostsData[locale] || blogPostsData.en
}
export const blogPosts = blogPostsData.en // For backward compatibility
export function getBlogPost(slug: string, locale: string = "en"): BlogPost | undefined {
const posts = getBlogPosts(locale)
return posts.find((post) => post.slug === slug)
}

16
app/robots.ts Normal file
View File

@@ -0,0 +1,16 @@
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://moemail.app'
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/moe/', '/profile/', '/api/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
}
}

35
app/sitemap.ts Normal file
View File

@@ -0,0 +1,35 @@
import { MetadataRoute } from 'next'
import { i18n } from '@/i18n/config'
import { blogPosts } from '@/lib/blog-data'
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://moemail.app'
const routes: MetadataRoute.Sitemap = []
// Static pages for each locale
const staticPages = ['', '/about', '/blog', '/privacy', '/terms']
for (const locale of i18n.locales) {
for (const page of staticPages) {
routes.push({
url: `${baseUrl}/${locale}${page}`,
lastModified: new Date(),
changeFrequency: page === '' ? 'daily' : 'weekly',
priority: page === '' ? 1.0 : 0.8,
})
}
// Blog posts for each locale
for (const post of blogPosts) {
routes.push({
url: `${baseUrl}/${locale}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly',
priority: 0.6,
})
}
}
return routes
}

253
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@auth/drizzle-adapter": "^1.7.4",
"@cloudflare/next-on-pages": "^1.13.6",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.4",
@@ -3603,6 +3604,140 @@
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
@@ -3708,6 +3843,124 @@
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",

View File

@@ -22,6 +22,7 @@
"dependencies": {
"@auth/drizzle-adapter": "^1.7.4",
"@cloudflare/next-on-pages": "^1.13.6",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.4",

262
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@cloudflare/next-on-pages':
specifier: ^1.13.6
version: 1.13.6(@cloudflare/workers-types@4.20241205.0)(vercel@39.1.1)(wrangler@3.93.0(@cloudflare/workers-types@4.20241205.0))
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-alert-dialog':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -1750,6 +1753,22 @@ packages:
'@radix-ui/primitive@1.1.1':
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-accordion@1.2.12':
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-alert-dialog@1.1.4':
resolution: {integrity: sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==}
peerDependencies:
@@ -1776,6 +1795,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.12':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.0':
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
@@ -1802,6 +1834,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@@ -1820,6 +1865,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.0':
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
@@ -1838,6 +1892,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.2':
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
peerDependencies:
@@ -1873,6 +1936,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.1':
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
peerDependencies:
@@ -1956,6 +2028,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-label@2.1.0':
resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==}
peerDependencies:
@@ -2047,6 +2128,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@@ -2073,6 +2167,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-radio-group@1.2.1':
resolution: {integrity: sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==}
peerDependencies:
@@ -2143,6 +2250,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-switch@1.1.2':
resolution: {integrity: sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==}
peerDependencies:
@@ -2213,6 +2329,24 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.0':
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
@@ -2231,6 +2365,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.0':
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies:
@@ -7273,6 +7416,25 @@ snapshots:
'@radix-ui/primitive@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-accordion@1.2.12(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-alert-dialog@1.1.4(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
@@ -7296,6 +7458,22 @@ snapshots:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-collection@1.1.0(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.0)(react@19.0.0)
@@ -7320,6 +7498,18 @@ snapshots:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-compose-refs@1.1.0(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0
@@ -7332,6 +7522,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-context@1.1.0(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0
@@ -7344,6 +7540,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-context@1.1.2(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-dialog@1.1.2(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -7394,6 +7596,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-direction@1.1.1(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -7470,6 +7678,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-id@1.1.1(@types/react@19.0.0)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-label@2.1.0(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -7563,6 +7778,16 @@ snapshots:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-primitive@2.0.0(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@19.0.0)(react@19.0.0)
@@ -7581,6 +7806,15 @@ snapshots:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.0
'@types/react-dom': 19.0.0
'@radix-ui/react-radio-group@1.2.1(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -7676,6 +7910,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-slot@1.2.3(@types/react@19.0.0)(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-switch@1.1.2(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
@@ -7760,6 +8001,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.0.0)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.0.0)(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.0.0)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.0)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.0)(react@19.0.0)
@@ -7773,6 +8029,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.0
'@radix-ui/react-use-previous@1.1.0(@types/react@19.0.0)(react@19.0.0)':
dependencies:
react: 19.0.0