Improved user interface translations and clarity in Simplified
This commit is contained in:
10
README-zh.md
10
README-zh.md
@@ -109,10 +109,18 @@ pnpm db:push
|
||||
|
||||
Follow https://localhost:3000/setup
|
||||
|
||||
## Environment Variables
|
||||
## 环境变量
|
||||
|
||||
查看 [开发者文档](https://wr.do/docs/developer).
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Next.js + React + TypeScript
|
||||
- Tailwind CSS 用于样式设计
|
||||
- Prisma ORM 作为数据库工具
|
||||
- Cloudflare 作为主要的云基础设施
|
||||
- Vercel 作为推荐的部署平台
|
||||
|
||||
## 社区群组
|
||||
|
||||
- Discord: https://discord.gg/AHPQYuZu3m
|
||||
|
||||
@@ -123,6 +123,14 @@ Follow https://localhost:3000/setup
|
||||
|
||||
Via [Installation For Developer](https://wr.do/docs/developer).
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- Next.js + React + TypeScript
|
||||
- Tailwind CSS for styling and design
|
||||
- Prisma ORM as the database toolkit
|
||||
- Cloudflare as the primary cloud infrastructure
|
||||
- Vercel as the recommended deployment platform
|
||||
|
||||
## Community Group
|
||||
|
||||
- Discord: https://discord.gg/AHPQYuZu3m
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Suspense } from "react";
|
||||
import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -14,6 +15,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations("Auth");
|
||||
return (
|
||||
<div className="container flex h-screen w-screen flex-col items-center justify-center">
|
||||
<Link
|
||||
@@ -25,20 +27,20 @@ export default function LoginPage() {
|
||||
>
|
||||
<>
|
||||
<Icons.chevronLeft className="mr-2 size-4" />
|
||||
Back
|
||||
{t("Back")}
|
||||
</>
|
||||
</Link>
|
||||
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<Icons.logo className="mx-auto size-12" />
|
||||
<div className="text-2xl font-semibold tracking-tight">
|
||||
<span>Welcome to</span>{" "}
|
||||
<span>{t("Welcome to")}</span>{" "}
|
||||
<span style={{ fontFamily: "Bahamas Bold" }}>
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose your login method to continue
|
||||
{t("Choose your login method to continue")}
|
||||
</p>
|
||||
</div>
|
||||
<Suspense>
|
||||
@@ -50,19 +52,19 @@ export default function LoginPage() {
|
||||
</p> */}
|
||||
|
||||
<p className="px-8 text-center text-sm text-muted-foreground">
|
||||
By clicking continue, you agree to our{" "}
|
||||
{t("By clicking continue, you agree to our")}{" "}
|
||||
<Link
|
||||
href="/terms"
|
||||
className="hover:text-brand underline underline-offset-4"
|
||||
>
|
||||
Terms of Service
|
||||
{t("Terms of Service")}
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
{t("and")}{" "}
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="hover:text-brand underline underline-offset-4"
|
||||
>
|
||||
Privacy Policy
|
||||
{t("Privacy Policy")}
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
<Card className="xl:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center gap-2">
|
||||
<div className="flex items-center gap-1 text-lg font-bold">
|
||||
<span className="text-nowrap">Total Domains:</span>
|
||||
<span className="text-nowrap">{t("Total Domains")}:</span>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-6 w-16" />
|
||||
) : (
|
||||
@@ -163,7 +163,7 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
}}
|
||||
>
|
||||
<Icons.add className="size-4" />
|
||||
<span className="hidden sm:inline">Add Domain</span>
|
||||
<span className="hidden sm:inline">{t("Add Domain")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -172,7 +172,7 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="h-8 text-xs md:text-xs"
|
||||
placeholder="Search by domain name..."
|
||||
placeholder={t("Search by domain name") + "..."}
|
||||
value={searchParams.target}
|
||||
onChange={(e) => {
|
||||
setSearchParams({
|
||||
@@ -199,25 +199,25 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
<TableHeader className="bg-gray-100/50 dark:bg-primary-foreground">
|
||||
<TableRow className="grid grid-cols-4 items-center text-xs sm:grid-cols-7">
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Domain
|
||||
{t("Domain Name")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center text-nowrap font-bold sm:flex">
|
||||
Shorten
|
||||
{t("Shorten Service")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center text-nowrap font-bold sm:flex">
|
||||
Email
|
||||
{t("Email Service")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center text-nowrap font-bold sm:flex">
|
||||
Subdomain
|
||||
{t("Subdomain Service")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center text-nowrap font-bold">
|
||||
Active
|
||||
{t("Active")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Updated
|
||||
{t("Updated")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Actions
|
||||
{t("Actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -305,7 +305,7 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<p className="hidden sm:block">Edit</p>
|
||||
<p className="hidden sm:block">{t("Edit")}</p>
|
||||
<PenLine className="mx-0.5 size-4 sm:ml-1 sm:size-3" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
@@ -318,7 +318,9 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
) : (
|
||||
<EmptyPlaceholder className="shadow-none">
|
||||
<EmptyPlaceholder.Icon name="globeLock" />
|
||||
<EmptyPlaceholder.Title>No Domains</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Title>
|
||||
{t("No Domains")}
|
||||
</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Description>
|
||||
You don't have any domains yet. Start creating one.
|
||||
</EmptyPlaceholder.Description>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
getScrapeStatsByTypeAndUserId,
|
||||
getScrapeStatsByUserId,
|
||||
getScrapeStatsByUserId1,
|
||||
} from "@/lib/dto/scrape";
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import JsonView from "@uiw/react-json-view";
|
||||
import { githubLightTheme } from "@uiw/react-json-view/githubLight";
|
||||
import { vscodeTheme } from "@uiw/react-json-view/vscode";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -50,6 +51,7 @@ export function ScreenshotScraping({
|
||||
}: {
|
||||
user: { id: string; apiKey: string };
|
||||
}) {
|
||||
const t = useTranslations("Scrape");
|
||||
const { theme } = useTheme();
|
||||
const [protocol, setProtocol] = useState("https://");
|
||||
|
||||
@@ -87,10 +89,12 @@ export function ScreenshotScraping({
|
||||
<CodeLight content={`https://wr.do/api/v1/scraping/screenshot`} />
|
||||
<Card className="bg-gray-50 dark:bg-gray-900">
|
||||
<CardHeader>
|
||||
<CardTitle>Playground</CardTitle>
|
||||
<CardTitle>{t("Playground")}</CardTitle>
|
||||
<CardDescription>
|
||||
Automate your website screenshots and turn them into stunning
|
||||
visuals for your applications.
|
||||
{t(
|
||||
"Automate your website screenshots and turn them into stunning visuals for your applications",
|
||||
)}
|
||||
.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -126,9 +130,9 @@ export function ScreenshotScraping({
|
||||
variant="blue"
|
||||
onClick={handleScrapingScreenshot}
|
||||
disabled={isShoting}
|
||||
className="rounded-l-none"
|
||||
className="w-28 rounded-l-none"
|
||||
>
|
||||
{isShoting ? "Scraping..." : "Send"}
|
||||
{isShoting ? t("Scraping") : t("Start")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -164,6 +168,7 @@ export function MetaScraping({
|
||||
}: {
|
||||
user: { id: string; apiKey: string };
|
||||
}) {
|
||||
const t = useTranslations("Scrape");
|
||||
const { theme } = useTheme();
|
||||
const [currentLink, setCurrentLink] = useState("wr.do");
|
||||
const [protocol, setProtocol] = useState("https://");
|
||||
@@ -203,8 +208,10 @@ export function MetaScraping({
|
||||
<CodeLight content={`https://wr.do/api/v1/scraping/meta`} />
|
||||
<Card className="bg-gray-50 dark:bg-gray-900">
|
||||
<CardHeader>
|
||||
<CardTitle>Playground</CardTitle>
|
||||
<CardDescription>Scrape the meta data of a website.</CardDescription>
|
||||
<CardTitle>{t("Playground")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("Scrape the meta data of a website")}.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center">
|
||||
@@ -239,9 +246,9 @@ export function MetaScraping({
|
||||
variant="blue"
|
||||
onClick={handleScrapingMeta}
|
||||
disabled={isScraping}
|
||||
className="rounded-l-none"
|
||||
className="w-28 rounded-l-none"
|
||||
>
|
||||
{isScraping ? "Scraping..." : "Send"}
|
||||
{isScraping ? t("Scraping") : t("Start")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -264,6 +271,7 @@ export function MarkdownScraping({
|
||||
}: {
|
||||
user: { id: string; apiKey: string };
|
||||
}) {
|
||||
const t = useTranslations("Scrape");
|
||||
const { theme } = useTheme();
|
||||
const [currentLink, setCurrentLink] = useState("wr.do");
|
||||
const [protocol, setProtocol] = useState("https://");
|
||||
@@ -334,9 +342,9 @@ export function MarkdownScraping({
|
||||
variant="blue"
|
||||
onClick={handleScrapingMeta}
|
||||
disabled={isScraping}
|
||||
className="rounded-l-none"
|
||||
className="w-28 rounded-l-none"
|
||||
>
|
||||
{isScraping ? "Scraping..." : "Send"}
|
||||
{isScraping ? t("Scraping") : t("Start")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -359,6 +367,7 @@ export function TextScraping({
|
||||
}: {
|
||||
user: { id: string; apiKey: string };
|
||||
}) {
|
||||
const t = useTranslations("Scrape");
|
||||
const { theme } = useTheme();
|
||||
const [currentLink, setCurrentLink] = useState("wr.do");
|
||||
const [protocol, setProtocol] = useState("https://");
|
||||
@@ -394,7 +403,7 @@ export function TextScraping({
|
||||
<CodeLight content={`https://wr.do/api/v1/scraping/text`} />
|
||||
<Card className="bg-gray-50 dark:bg-gray-900">
|
||||
<CardHeader>
|
||||
<CardTitle>Text</CardTitle>
|
||||
<CardTitle>{t("Text")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center">
|
||||
@@ -429,9 +438,9 @@ export function TextScraping({
|
||||
variant="blue"
|
||||
onClick={handleScrapingMeta}
|
||||
disabled={isScraping}
|
||||
className="rounded-l-none"
|
||||
className="w-28 rounded-l-none"
|
||||
>
|
||||
{isScraping ? "Scraping..." : "Send"}
|
||||
{isScraping ? t("Scraping") : t("Start")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -454,6 +463,7 @@ export function QrCodeScraping({
|
||||
}: {
|
||||
user: { id: string; apiKey: string };
|
||||
}) {
|
||||
const t = useTranslations("Scrape");
|
||||
const { theme } = useTheme();
|
||||
const [protocol, setProtocol] = useState("https://");
|
||||
|
||||
@@ -487,11 +497,7 @@ export function QrCodeScraping({
|
||||
<CodeLight content={`https://wr.do/api/v1/scraping/qrcode`} />
|
||||
<Card className="bg-gray-50 dark:bg-gray-900">
|
||||
<CardHeader>
|
||||
<CardTitle>Playground</CardTitle>
|
||||
<CardDescription>
|
||||
Automate your website screenshots and turn them into stunning
|
||||
visuals for your applications.
|
||||
</CardDescription>
|
||||
<CardTitle>{t("Playground")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
@@ -27,26 +28,22 @@ export default function StepGuide({
|
||||
const [direction, setDirection] = useState(0);
|
||||
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
||||
|
||||
const t = useTranslations("Common");
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Set up an administrator",
|
||||
description:
|
||||
"Begin by entering your website URL or selecting an example site to reimagine your website with modern themes.",
|
||||
title: t("Set up an administrator"),
|
||||
component: () => <SetAdminRole id={user.id} email={user.email} />,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Add the first domain",
|
||||
description:
|
||||
"Check out your reimagined site and click to Migrate & Download.",
|
||||
title: t("Add the first domain"),
|
||||
component: () => <AddDomain onNextStep={goToNextStep} />,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Congrats on completing setup 🎉",
|
||||
description:
|
||||
"Navigate to your GitHub dashboard where you'll manage your repository and project files.",
|
||||
title: t("Congrats on completing setup 🎉"),
|
||||
component: () => <Congrats />,
|
||||
},
|
||||
];
|
||||
@@ -92,7 +89,7 @@ export default function StepGuide({
|
||||
<Modal className="md:max-w-2xl">
|
||||
<div className="w-full px-4 py-2 md:px-8 md:py-4">
|
||||
<div className="mb-6 mt-3 flex items-center justify-between gap-4">
|
||||
<h2 className="text-2xl font-bold">Admin Setup Guide</h2>
|
||||
<h2 className="text-2xl font-bold">{t("Admin Setup Guide")}</h2>
|
||||
<div className="flex items-center gap-2 rounded-full bg-muted/50 px-3 py-1.5 text-sm font-medium">
|
||||
<span className="flex size-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
{currentStep}
|
||||
@@ -161,7 +158,7 @@ export default function StepGuide({
|
||||
)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
{t("Previous")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -171,7 +168,7 @@ export default function StepGuide({
|
||||
"flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/90",
|
||||
)}
|
||||
>
|
||||
{currentStep === steps.length ? "🚀 Start" : "Next"}
|
||||
{currentStep === steps.length ? t("🚀 Start") : t("Next")}
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</motion.div>
|
||||
@@ -182,6 +179,7 @@ export default function StepGuide({
|
||||
function SetAdminRole({ id, email }: { id: string; email: string }) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const t = useTranslations("Common");
|
||||
const handleSetAdmin = async () => {
|
||||
startTransition(async () => {
|
||||
const res = await fetch("/api/setup");
|
||||
@@ -194,7 +192,7 @@ function SetAdminRole({ id, email }: { id: string; email: string }) {
|
||||
const ReadyBadge = (
|
||||
<Badge className="text-xs font-semibold" variant="green">
|
||||
<Icons.check className="mr-1 size-3" />
|
||||
Ready
|
||||
{t("Ready")}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -202,14 +200,14 @@ function SetAdminRole({ id, email }: { id: string; email: string }) {
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-neutral-50 p-4 dark:bg-neutral-900">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-muted-foreground">
|
||||
Allow Sign Up:
|
||||
{t("Allow Sign Up")}:
|
||||
</span>
|
||||
{siteConfig.openSignup ? ReadyBadge : <Skeleton className="h-4 w-12" />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-muted-foreground">
|
||||
Set {email} as ADMIN:
|
||||
{t("Set {email} as ADMIN", { email })}:
|
||||
</span>
|
||||
{isAdmin ? (
|
||||
ReadyBadge
|
||||
@@ -223,30 +221,36 @@ function SetAdminRole({ id, email }: { id: string; email: string }) {
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
Active Now
|
||||
{t("Active Now")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-dashed p-2 text-xs text-muted-foreground">
|
||||
<p className="flex items-start gap-1">
|
||||
📢 Only by becoming an administrator can one access the admin panel
|
||||
and add domain names.
|
||||
•{" "}
|
||||
{t(
|
||||
"Only by becoming an administrator can one access the admin panel and add domain names",
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
<p className="my-1">
|
||||
📢 Administrators can set all user permissions, allocate quotas, view
|
||||
and edit all resources (short links, subdomains, email), etc.
|
||||
•{" "}
|
||||
{t(
|
||||
"Administrators can set all user permissions, allocate quotas, view and edit all resources (short links, subdomains, email), etc",
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
📢 Via{" "}
|
||||
•{t("Via")}{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-blue-500 after:content-['_↗']"
|
||||
target="_blank"
|
||||
href="/docs/developer/quick-start"
|
||||
>
|
||||
quick start
|
||||
{t("quick start")}
|
||||
</a>{" "}
|
||||
docs to get more information.
|
||||
{t("docs to get more information")}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,6 +260,7 @@ function SetAdminRole({ id, email }: { id: string; email: string }) {
|
||||
function AddDomain({ onNextStep }: { onNextStep: () => void }) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [domain, setDomain] = useState("");
|
||||
const t = useTranslations("Common");
|
||||
const handleCreateDomain = async () => {
|
||||
if (!domain) {
|
||||
toast.warning("Domain name cannot be empty");
|
||||
@@ -292,10 +297,10 @@ function AddDomain({ onNextStep }: { onNextStep: () => void }) {
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-neutral-50 p-4 dark:bg-neutral-900">
|
||||
<FormSectionColumns title="Domain Name">
|
||||
<FormSectionColumns title={t("Domain Name")}>
|
||||
<div className="flex w-full flex-col items-start justify-between gap-2">
|
||||
<Label className="sr-only" htmlFor="domain_name">
|
||||
Domain Name
|
||||
{t("Domain Name")}
|
||||
</Label>
|
||||
<div className="w-full">
|
||||
<Input
|
||||
@@ -307,7 +312,10 @@ function AddDomain({ onNextStep }: { onNextStep: () => void }) {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Please enter a valid domain name (must be hosted on Cloudflare).
|
||||
{t(
|
||||
"Please enter a valid domain name (must be hosted on Cloudflare)",
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -318,7 +326,7 @@ function AddDomain({ onNextStep }: { onNextStep: () => void }) {
|
||||
size={"sm"}
|
||||
onClick={onNextStep}
|
||||
>
|
||||
Or add later
|
||||
{t("Or add later")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
@@ -330,7 +338,7 @@ function AddDomain({ onNextStep }: { onNextStep: () => void }) {
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
Submit
|
||||
{t("Submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function UserInfoCard({
|
||||
icon?: keyof typeof Icons;
|
||||
}) {
|
||||
const Icon = Icons[icon || "arrowRight"];
|
||||
const t = useTranslations("Components");
|
||||
return (
|
||||
<Card className="grids group bg-gray-50/70 backdrop-blur-lg dark:bg-primary-foreground">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
@@ -31,7 +32,7 @@ export async function UserInfoCard({
|
||||
className="font-semibold text-slate-500 duration-500 group-hover:text-blue-500 group-hover:underline"
|
||||
href={link}
|
||||
>
|
||||
{title}
|
||||
{t(title)}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<Icon className="size-4 text-muted-foreground" />
|
||||
@@ -49,7 +50,7 @@ export async function UserInfoCard({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">total</p>
|
||||
<p className="text-xs text-muted-foreground">{t("total")}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SectionColumns } from "@/components/dashboard/section-columns";
|
||||
@@ -7,6 +9,7 @@ import { useDeleteAccountModal } from "@/components/modals/delete-account-modal"
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
|
||||
export function DeleteAccountSection() {
|
||||
const t = useTranslations("Setting");
|
||||
const { setShowDeleteAccountModal, DeleteAccountModal } =
|
||||
useDeleteAccountModal();
|
||||
|
||||
@@ -16,27 +19,31 @@ export function DeleteAccountSection() {
|
||||
<>
|
||||
<DeleteAccountModal />
|
||||
<SectionColumns
|
||||
title="Delete Account"
|
||||
description="This is a danger zone - Be careful !"
|
||||
title={t("Delete Account")}
|
||||
description={t("This is a danger zone - Be careful !")}
|
||||
>
|
||||
<div className="flex flex-col gap-4 rounded-xl border border-red-400 p-4 dark:border-red-900">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[15px] font-medium">Are you sure ?</span>
|
||||
<span className="text-[15px] font-medium">
|
||||
{t("Are you sure")} ?
|
||||
</span>
|
||||
|
||||
{userPaidPlan ? (
|
||||
<div className="flex items-center gap-1 rounded-md bg-red-600/10 p-1 pr-2 text-xs font-medium text-red-600 dark:bg-red-500/10 dark:text-red-500">
|
||||
<div className="m-0.5 rounded-full bg-red-600 p-[3px]">
|
||||
<Icons.close size={10} className="text-background" />
|
||||
</div>
|
||||
Active Subscription
|
||||
{t("Active Subscription")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-balance text-sm text-muted-foreground">
|
||||
Permanently delete your {siteConfig.name} account
|
||||
{userPaidPlan ? " and your subscription" : ""}. This action cannot
|
||||
be undone - please proceed with caution.
|
||||
{t("Permanently delete your {name} account", {
|
||||
name: siteConfig.name,
|
||||
})}
|
||||
{userPaidPlan ? t(" and your subscription") : ""}.{" "}
|
||||
{t("This action cannot be undone - please proceed with caution")}.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -46,7 +53,7 @@ export function DeleteAccountSection() {
|
||||
onClick={() => setShowDeleteAccountModal(true)}
|
||||
>
|
||||
<Icons.trash className="mr-2 size-4" />
|
||||
<span>Delete Account</span>
|
||||
<span>{t("Delete Account")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FileText,
|
||||
FileVideo,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn, downloadFile, formatDate, formatFileSize } from "@/lib/utils";
|
||||
@@ -85,7 +86,8 @@ export default function EmailDetail({
|
||||
onClose,
|
||||
onMarkAsRead,
|
||||
}: EmailDetailProps) {
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null); // 控制图片预览 Modal
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const t = useTranslations("Email");
|
||||
|
||||
function getFileIcon(type: string): React.ComponentType<any> {
|
||||
const icon = Object.keys(fileTypeIcons).find((key) =>
|
||||
@@ -178,7 +180,8 @@ export default function EmailDetail({
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger className="line-clamp-2 text-wrap text-left text-xs">
|
||||
<strong>From:</strong> {email.fromName} <{email.from}>
|
||||
<strong>{t("From")}:</strong> {email.fromName} <{email.from}
|
||||
>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="w-60 text-wrap text-xs">
|
||||
{email.fromName} <br />
|
||||
@@ -187,19 +190,19 @@ export default function EmailDetail({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className="text-xs">
|
||||
<strong>To:</strong> {email.to}
|
||||
<strong>{t("To")}:</strong> {email.to}
|
||||
</p>
|
||||
{email.replyTo && email.replyTo !== '""' && (
|
||||
<p className="text-xs">
|
||||
<strong>Reply-To:</strong> {email.replyTo}
|
||||
<strong>{t("Reply-To")}:</strong> {email.replyTo}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs">
|
||||
<strong>Date:</strong> {formatDate(email.date as any)}
|
||||
<strong>{t("Date")}:</strong> {formatDate(email.date as any)}
|
||||
</p>
|
||||
{attachments.length > 0 && (
|
||||
<p className="text-xs">
|
||||
<strong>Attachments</strong>: {attachments.length}
|
||||
<strong>{t("Attachments")}</strong>: {attachments.length}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -225,7 +228,7 @@ export default function EmailDetail({
|
||||
{attachments.length > 0 && (
|
||||
<div className="mt-auto border-t border-dashed px-2 py-3">
|
||||
<h3 className="mb-2 text-sm font-semibold text-neutral-700 dark:text-neutral-400">
|
||||
Attachments ({attachments.length})
|
||||
{t("Attachments")} ({attachments.length})
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
|
||||
{attachments.map((attachment, index) => {
|
||||
@@ -266,11 +269,11 @@ export default function EmailDetail({
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleDownload(attachment)}
|
||||
className="absolute right-0 top-0 hidden animate-fade-in px-2 group-hover:block"
|
||||
className="absolute right-1 top-1 hidden h-7 animate-fade-in px-2 group-hover:block"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
variant="default"
|
||||
>
|
||||
<Icons.download className="size-4" />
|
||||
<Icons.download className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ForwardEmail } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
|
||||
@@ -48,6 +49,7 @@ export default function EmailList({
|
||||
className,
|
||||
isAdminModel,
|
||||
}: EmailListProps) {
|
||||
const t = useTranslations("Email");
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isAutoRefresh, setIsAutoRefresh] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -148,14 +150,14 @@ export default function EmailList({
|
||||
};
|
||||
|
||||
if (!emailAddress) {
|
||||
return EmptyInboxSection();
|
||||
return <EmptyInboxSection />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("grids flex flex-1 flex-col", className)}>
|
||||
<div className="flex items-center gap-2 bg-neutral-200/40 p-2 text-base font-semibold text-neutral-600 backdrop-blur dark:bg-neutral-800 dark:text-neutral-50">
|
||||
<Icons.inbox size={20} />
|
||||
<span>INBOX</span>
|
||||
<span>{t("INBOX")}</span>
|
||||
<div className="ml-auto flex items-center justify-center gap-2">
|
||||
<SendEmailModal emailAddress={emailAddress} onSuccess={mutate} />
|
||||
<TooltipProvider>
|
||||
@@ -168,7 +170,7 @@ export default function EmailList({
|
||||
aria-label="Auto refresh"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Auto refresh</TooltipContent>
|
||||
<TooltipContent side="bottom">{t("Auto refresh")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
@@ -204,7 +206,7 @@ export default function EmailList({
|
||||
size="sm"
|
||||
className="flex w-full items-center gap-1"
|
||||
>
|
||||
<span className="text-sm">more</span>
|
||||
<span className="text-sm">{t("more")}</span>
|
||||
<Icons.chevronDown className="mt-0.5 size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -216,13 +218,13 @@ export default function EmailList({
|
||||
onClick={handleMarkSelectedAsRead}
|
||||
className="w-full"
|
||||
>
|
||||
<span className="text-xs">Mask as read</span>
|
||||
<span className="text-xs">{t("Mask as read")}</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild disabled>
|
||||
<Button variant="ghost" size="sm" className="w-full">
|
||||
<span className="text-xs">Delete selected</span>
|
||||
<span className="text-xs">{t("Delete selected")}</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -298,7 +300,7 @@ export default function EmailList({
|
||||
<div className="flex h-[calc(100vh-135px)] flex-col items-center justify-center gap-8">
|
||||
<Loader />
|
||||
<p className="font-mono font-semibold text-neutral-500">
|
||||
Waiting for emails...
|
||||
{t("Waiting for emails")}...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -321,6 +323,7 @@ export default function EmailList({
|
||||
}
|
||||
|
||||
export function EmptyInboxSection() {
|
||||
const t = useTranslations("Email");
|
||||
return (
|
||||
<div className="grids flex flex-1 animate-fade-in flex-col items-center justify-center p-4 text-center text-neutral-600 dark:text-neutral-400">
|
||||
<BlurImage
|
||||
@@ -330,10 +333,12 @@ export function EmptyInboxSection() {
|
||||
width={200}
|
||||
alt="Inbox"
|
||||
/>
|
||||
<h2 className="my-2 text-lg font-semibold">No Email Address Selected</h2>
|
||||
<h2 className="my-2 text-lg font-semibold">
|
||||
{t("No Email Address Selected")}
|
||||
</h2>
|
||||
<p className="max-w-md text-sm">
|
||||
Please select an email address from the list to view your inbox. Once
|
||||
selected, your emails will appear here automatically.
|
||||
{t("Please select an email address from the list to view your inbox")}.
|
||||
{t("Once selected, your emails will appear here automatically")}.
|
||||
</p>
|
||||
<ul className="mt-3 list-disc text-left">
|
||||
<li>
|
||||
@@ -343,7 +348,7 @@ export function EmptyInboxSection() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to use email to send or receive emails?
|
||||
{t("How to use email to send or receive emails?")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
@@ -353,7 +358,7 @@ export function EmptyInboxSection() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Will my email or inbox expire?
|
||||
{t("Will my email or inbox expire?")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
@@ -363,7 +368,7 @@ export function EmptyInboxSection() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
What is the limit? It's free?
|
||||
{t("What is the limit? It's free?")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
@@ -373,7 +378,7 @@ export function EmptyInboxSection() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to create emails with api?
|
||||
{t("How to create emails with api?")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Sparkles,
|
||||
SquarePlus,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
|
||||
@@ -67,6 +68,7 @@ export default function EmailSidebar({
|
||||
setAdminModel,
|
||||
}: EmailSidebarProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const t = useTranslations("Email");
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -261,7 +263,7 @@ export default function EmailSidebar({
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search emails..."
|
||||
placeholder={t("Search emails")}
|
||||
className="h-[30px] w-full border p-1 pl-8 text-xs placeholder:text-xs"
|
||||
/>
|
||||
<Search className="absolute left-2 top-2 size-4 text-gray-500" />
|
||||
@@ -296,7 +298,9 @@ export default function EmailSidebar({
|
||||
}}
|
||||
>
|
||||
<SquarePlus className="size-4" />
|
||||
{!isCollapsed && <span className="text-xs">Create New Email</span>}
|
||||
{!isCollapsed && (
|
||||
<span className="text-xs">{t("Create New Email")}</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{!isCollapsed && (
|
||||
@@ -306,7 +310,7 @@ export default function EmailSidebar({
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.mail className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
Email Address
|
||||
{t("Email Address")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
@@ -319,7 +323,7 @@ export default function EmailSidebar({
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.inbox className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
Inbox Emails
|
||||
{t("Inbox Emails")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
@@ -342,7 +346,7 @@ export default function EmailSidebar({
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.mailOpen className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
Unread Emails
|
||||
{t("Unread Emails")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
@@ -353,7 +357,7 @@ export default function EmailSidebar({
|
||||
<TooltipTrigger>
|
||||
<Icons.listFilter className="absolute bottom-1 right-1 size-3" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Filter unread emails</TooltipContent>
|
||||
<TooltipContent>{t("Filter unread emails")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
@@ -372,7 +376,7 @@ export default function EmailSidebar({
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.send className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
Sent Emails
|
||||
{t("Sent Emails")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
@@ -384,7 +388,7 @@ export default function EmailSidebar({
|
||||
|
||||
{!isCollapsed && user.role === "ADMIN" && (
|
||||
<div className="mt-2 flex items-center gap-2 text-sm">
|
||||
Admin Mode:{" "}
|
||||
{t("Admin Mode")}:{" "}
|
||||
<Switch
|
||||
defaultChecked={isAdminModel}
|
||||
onCheckedChange={(v) => setAdminModel(v)}
|
||||
@@ -420,7 +424,9 @@ export default function EmailSidebar({
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<EmptyPlaceholder className="shadow-none">
|
||||
<EmptyPlaceholder.Icon name="mailPlus" />
|
||||
<EmptyPlaceholder.Title>No emails</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Title>
|
||||
{t("No emails")}
|
||||
</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Description>
|
||||
You don't have any email yet. Start creating email.
|
||||
</EmptyPlaceholder.Description>
|
||||
@@ -527,7 +533,7 @@ export default function EmailSidebar({
|
||||
{email.unreadCount > 0 && (
|
||||
<Badge variant="default">{email.unreadCount}</Badge>
|
||||
)}
|
||||
{email.count} recived
|
||||
{t("{email} recived", { email: email.count })}
|
||||
</div>
|
||||
<span className="line-clamp-1 hover:line-clamp-none">
|
||||
{isAdminModel
|
||||
@@ -568,7 +574,7 @@ export default function EmailSidebar({
|
||||
<Modal showModal={showEmailModal} setShowModal={setShowEmailModal}>
|
||||
<div className="p-6">
|
||||
<h2 className="mb-4 text-lg font-semibold">
|
||||
{isEdit ? "Edit" : "Create new"} email
|
||||
{isEdit ? t("Edit email") : t("Create new email")}
|
||||
</h2>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -582,14 +588,14 @@ export default function EmailSidebar({
|
||||
htmlFor="emailAddress"
|
||||
className="mb-1 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Email Address
|
||||
{t("Email Address")}
|
||||
</label>
|
||||
<div className="flex items-center justify-center">
|
||||
<Input
|
||||
id="emailAddress"
|
||||
name="emailAddress"
|
||||
type="text"
|
||||
placeholder="Enter email suffix"
|
||||
placeholder={t("Enter email prefix")}
|
||||
className="w-full rounded-r-none"
|
||||
required
|
||||
defaultValue={
|
||||
@@ -622,7 +628,7 @@ export default function EmailSidebar({
|
||||
))
|
||||
) : (
|
||||
<Button className="w-full" variant="ghost">
|
||||
No domains configured
|
||||
{t("No domains configured")}
|
||||
</Button>
|
||||
)}
|
||||
</SelectContent>
|
||||
@@ -652,10 +658,10 @@ export default function EmailSidebar({
|
||||
variant="outline"
|
||||
onClick={() => setShowEmailModal(false)}
|
||||
>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button type="submit" variant="default" disabled={isPending}>
|
||||
{isEdit ? "Update" : "Create"}
|
||||
{isEdit ? t("Update") : t("Create")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -667,19 +673,20 @@ export default function EmailSidebar({
|
||||
{showDeleteModal && (
|
||||
<Modal showModal={showDeleteModal} setShowModal={setShowDeleteModal}>
|
||||
<div className="p-6">
|
||||
<h2 className="mb-4 text-lg font-semibold">Delete email</h2>
|
||||
<h2 className="mb-4 text-lg font-semibold">{t("Delete email")}</h2>
|
||||
<p className="mb-4 text-sm text-neutral-600">
|
||||
You are about to delete the following email, once deleted, it
|
||||
cannot be recovered. All emails in inbox will be deleted at the
|
||||
same time. Are you sure you want to continue?
|
||||
{t(
|
||||
"You are about to delete the following email, once deleted, it cannot be recovered",
|
||||
)}
|
||||
. {t("All emails in inbox will be deleted at the same time")}.{" "}
|
||||
{t("Are you sure you want to continue?")}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-neutral-600">
|
||||
To confirm, please type{" "}
|
||||
{t("To confirm, please type")}{" "}
|
||||
<strong>
|
||||
delete{" "}
|
||||
{userEmails.find((e) => e.id === emailToDelete)?.emailAddress}
|
||||
</strong>{" "}
|
||||
below:
|
||||
</strong>
|
||||
</p>
|
||||
<Input
|
||||
value={deleteInput}
|
||||
@@ -696,7 +703,7 @@ export default function EmailSidebar({
|
||||
setEmailToDelete(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -710,7 +717,7 @@ export default function EmailSidebar({
|
||||
}`
|
||||
}
|
||||
>
|
||||
Delete
|
||||
{t("Confirm Delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { Input } from "../ui/input";
|
||||
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
|
||||
|
||||
interface SendEmailModalProps {
|
||||
@@ -37,6 +39,8 @@ export function SendEmailModal({
|
||||
const [sendForm, setSendForm] = useState({ to: "", subject: "", html: "" });
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const t = useTranslations("Email");
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
if (!emailAddress) {
|
||||
toast.error("No email address selected");
|
||||
@@ -94,7 +98,7 @@ export function SendEmailModal({
|
||||
<DrawerContent className="fixed bottom-0 right-0 top-0 w-full rounded-none sm:max-w-xl">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle className="flex items-center gap-1">
|
||||
Send Email{" "}
|
||||
{t("Send Email")}{" "}
|
||||
<Icons.help className="size-5 text-neutral-600 hover:text-neutral-400" />
|
||||
</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
@@ -106,13 +110,13 @@ export function SendEmailModal({
|
||||
<div className="scrollbar-hidden h-[calc(100vh)] space-y-4 overflow-y-auto p-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
From
|
||||
{t("From")}
|
||||
</label>
|
||||
<Input value={emailAddress || ""} disabled className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
To
|
||||
{t("To")}
|
||||
</label>
|
||||
<Input
|
||||
value={sendForm.to}
|
||||
@@ -125,7 +129,7 @@ export function SendEmailModal({
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Subject
|
||||
{t("Subject")}
|
||||
</label>
|
||||
<Input
|
||||
value={sendForm.subject}
|
||||
@@ -138,7 +142,7 @@ export function SendEmailModal({
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Content
|
||||
{t("Content")}
|
||||
</label>
|
||||
<ReactQuill
|
||||
value={sendForm.html}
|
||||
@@ -152,7 +156,7 @@ export function SendEmailModal({
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline" disabled={isPending}>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
<Button
|
||||
@@ -160,7 +164,7 @@ export function SendEmailModal({
|
||||
disabled={isPending}
|
||||
variant="default"
|
||||
>
|
||||
{isPending ? "Sending..." : "Send"}
|
||||
{isPending ? t("Sending") : t("Send")}
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { UserSendEmail } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { cn, fetcher, formatDate, htmlToText } from "@/lib/utils";
|
||||
import { fetcher, formatDate, htmlToText } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -26,6 +27,8 @@ export default function SendsEmailList({
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const t = useTranslations("Email");
|
||||
|
||||
const { data, isLoading, error } = useSWR<{
|
||||
list: UserSendEmail[];
|
||||
total: number;
|
||||
@@ -49,12 +52,12 @@ export default function SendsEmailList({
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-4xl border-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Sent Emails</CardTitle>
|
||||
<CardTitle>{t("Sent Emails")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-2 flex items-center justify-between gap-4">
|
||||
<Input
|
||||
placeholder="Search by send to email..."
|
||||
placeholder={t("Search by send to email")}
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
className="w-full bg-neutral-50"
|
||||
@@ -68,11 +71,11 @@ export default function SendsEmailList({
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-red-500">
|
||||
Failed to load emails. Please try again.
|
||||
{t("Failed to load emails")}. {t("Please try again")}.
|
||||
</div>
|
||||
) : !data || data.list.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
No emails found.
|
||||
{t("No emails found")}.
|
||||
</div>
|
||||
) : (
|
||||
<div className="scrollbar-hidden max-h-[50vh] overflow-y-auto">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { User } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -52,6 +53,7 @@ export function DomainForm({
|
||||
action,
|
||||
onRefresh,
|
||||
}: DomainFormProps) {
|
||||
const t = useTranslations("List");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isDeleting, startDeleteTransition] = useTransition();
|
||||
const [isCheckingCf, startCheckCfTransition] = useTransition();
|
||||
@@ -238,24 +240,24 @@ export function DomainForm({
|
||||
>
|
||||
{isChecking && <Icons.spinner className="mr-1 size-3 animate-spin" />}
|
||||
{isChecked && !isChecking && <Icons.check className="mr-1 size-3" />}
|
||||
{isChecked ? "Ready" : "Access Check"}
|
||||
{isChecked ? t("Verified") : t("Verify Configuration")}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
|
||||
{type === "add" ? "Create" : "Edit"} Domain
|
||||
{type === "add" ? t("Create Domain") : t("Edit Domain")}
|
||||
</div>
|
||||
<form className="p-4" onSubmit={onSubmit}>
|
||||
<div className="relative flex flex-col items-center justify-start gap-0 rounded-md bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<h2 className="absolute left-2 top-2 text-xs font-semibold text-neutral-400">
|
||||
Base
|
||||
{t("Base")}
|
||||
</h2>
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="domain_name">
|
||||
Domain Name:
|
||||
{t("Domain Name")}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
@@ -271,7 +273,7 @@ export function DomainForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Required. eg: example.com
|
||||
{t("Required")}. {t("Example")} example.com
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -281,7 +283,7 @@ export function DomainForm({
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<Label className="" htmlFor="active">
|
||||
Active:
|
||||
{t("Active")}:
|
||||
</Label>
|
||||
<Switch
|
||||
id="active"
|
||||
@@ -295,12 +297,12 @@ export function DomainForm({
|
||||
|
||||
<div className="relative mt-2 flex flex-col items-center justify-start gap-4 rounded-md bg-neutral-100 p-4 pt-10 dark:bg-neutral-800">
|
||||
<h2 className="absolute left-2 top-2 text-xs font-semibold text-neutral-400">
|
||||
Services(Optional)
|
||||
{t("Services")} ({t("Optional")})
|
||||
</h2>
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<Label className="" htmlFor="short_url_service">
|
||||
Short URL Service:
|
||||
{t("Shorten Service")}:
|
||||
</Label>
|
||||
<Switch
|
||||
id="short_url_service"
|
||||
@@ -312,7 +314,7 @@ export function DomainForm({
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<Label className="" htmlFor="email_service">
|
||||
Email Service:
|
||||
{t("Email Service")}:
|
||||
</Label>
|
||||
<Switch
|
||||
id="email_service"
|
||||
@@ -327,7 +329,7 @@ export function DomainForm({
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<Label className="cursor-pointer" htmlFor="dns_record_service">
|
||||
DNS Record Service:
|
||||
{t("Subdomain Service")}:
|
||||
</Label>
|
||||
<Switch
|
||||
id="dns_record_service"
|
||||
@@ -344,7 +346,7 @@ export function DomainForm({
|
||||
<Collapsible className="relative mt-2 rounded-md bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between">
|
||||
<h2 className="absolute left-2 top-5 flex gap-2 text-xs font-semibold text-neutral-400">
|
||||
Cloudflare Configs (Optional)
|
||||
{t("Cloudflare Configs")} ({t("Optional")})
|
||||
<Icons.cloudflare className="mx-0.5 size-4" />
|
||||
</h2>
|
||||
{ReadyBadge(
|
||||
@@ -358,14 +360,14 @@ export function DomainForm({
|
||||
<CollapsibleContent>
|
||||
{!currentRecordStatus && (
|
||||
<div className="mt-3 flex items-center gap-1 rounded bg-neutral-200 p-2 text-xs dark:bg-neutral-700">
|
||||
<Icons.help className="size-3" /> Associate with "DNS Record
|
||||
Service" status
|
||||
<Icons.help className="size-3" />{" "}
|
||||
{t("Associate with 'Subdomain Service' status")}
|
||||
</div>
|
||||
)}
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="zone_id">
|
||||
Zone ID:
|
||||
{"Zone ID"}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
@@ -382,13 +384,13 @@ export function DomainForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Optional.{" "}
|
||||
{t("Optional")}.{" "}
|
||||
<Link
|
||||
className="text-blue-500"
|
||||
href="/docs/developer/cloudflare"
|
||||
target="_blank"
|
||||
>
|
||||
How to get zone id?
|
||||
{t("How to get zone id?")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
@@ -399,7 +401,7 @@ export function DomainForm({
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="api-key">
|
||||
API Token:
|
||||
{t("API Token")}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
@@ -416,13 +418,13 @@ export function DomainForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Optional.{" "}
|
||||
{t("Optional")}.{" "}
|
||||
<Link
|
||||
className="text-blue-500"
|
||||
href="/docs/developer/cloudflare"
|
||||
target="_blank"
|
||||
>
|
||||
How to get api token?
|
||||
{t("How to get api token?")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
@@ -433,7 +435,7 @@ export function DomainForm({
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="email">
|
||||
Account Email:
|
||||
{t("Account Email")}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
@@ -450,13 +452,13 @@ export function DomainForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Optional.{" "}
|
||||
{t("Optional")}.{" "}
|
||||
<Link
|
||||
className="text-blue-500"
|
||||
href="/docs/developer/cloudflare"
|
||||
target="_blank"
|
||||
>
|
||||
How to get cloudflare account email?
|
||||
{t("How to get cloudflare account email?")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
@@ -470,7 +472,7 @@ export function DomainForm({
|
||||
<Collapsible className="relative mt-2 rounded-md bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between">
|
||||
<h2 className="absolute left-2 top-5 flex gap-2 text-xs font-semibold text-neutral-400">
|
||||
Resend Configs (Optional)
|
||||
{t("Resend Configs")} ({t("Optional")})
|
||||
<Icons.resend className="mx-0.5 size-4" />
|
||||
</h2>
|
||||
{ReadyBadge(
|
||||
@@ -484,14 +486,14 @@ export function DomainForm({
|
||||
<CollapsibleContent>
|
||||
{!currentEmailStatus && (
|
||||
<div className="mt-3 flex items-center gap-1 rounded bg-neutral-200 p-2 text-xs dark:bg-neutral-700">
|
||||
<Icons.help className="size-3" /> Associate with "Email Service"
|
||||
status
|
||||
<Icons.help className="size-3" />{" "}
|
||||
{t("Associate with 'Email Service' status")}
|
||||
</div>
|
||||
)}
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="zone_id">
|
||||
API Key (send email service):
|
||||
{t("API Key")} ({t("send email service")}):
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
@@ -508,13 +510,13 @@ export function DomainForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Optional.{" "}
|
||||
{t("Optional")}.{" "}
|
||||
<Link
|
||||
className="text-blue-500"
|
||||
href="/docs/developer/email"
|
||||
target="_blank"
|
||||
>
|
||||
How to get resend api key?
|
||||
{t("How to get resend api key?")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
@@ -538,7 +540,7 @@ export function DomainForm({
|
||||
{isDeleting ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>Delete</p>
|
||||
<p>{t("Delete")}</p>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -548,7 +550,7 @@ export function DomainForm({
|
||||
className="w-[80px] px-0"
|
||||
onClick={() => setShowForm(false)}
|
||||
>
|
||||
Cancle
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -559,7 +561,7 @@ export function DomainForm({
|
||||
{isPending ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>{type === "edit" ? "Update" : "Save"}</p>
|
||||
<p>{type === "edit" ? t("Update") : t("Save")}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useTransition } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { User } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -28,6 +29,8 @@ export function UserApiKeyForm({ user }: UserNameFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [apiKey, setApiKey] = useState(user?.apiKey || "");
|
||||
|
||||
const t = useTranslations("Setting");
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
@@ -57,12 +60,12 @@ export function UserApiKeyForm({ user }: UserNameFormProps) {
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<SectionColumns
|
||||
title="API Key"
|
||||
description="Generate a new API key to access the open apis."
|
||||
title={t("API Key")}
|
||||
description={t("Generate a new API key to access the open apis")}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="name">
|
||||
API Key
|
||||
{t("API Key")}
|
||||
</Label>
|
||||
<div className="flex w-full items-center">
|
||||
<input
|
||||
@@ -89,7 +92,7 @@ export function UserApiKeyForm({ user }: UserNameFormProps) {
|
||||
{isPending ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Generate"
|
||||
t("Generate")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@@ -40,6 +41,8 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
React.useState<boolean>(false);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const t = useTranslations("Auth");
|
||||
|
||||
async function onSubmit(data: FormData) {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -76,7 +79,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
{t("Or continue with")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,8 +216,8 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
<Icons.spinner className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{type === "register"
|
||||
? "Sign Up with Email"
|
||||
: "Sign In with Email"}
|
||||
? t("Sign Up with Email")
|
||||
: t("Sign In with Email")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { updateUserName, type FormData } from "@/actions/update-user-name";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { User } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -25,6 +26,8 @@ export function UserNameForm({ user }: UserNameFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const updateUserNameWithId = updateUserName.bind(null, user.id);
|
||||
|
||||
const t = useTranslations("Setting");
|
||||
|
||||
const checkUpdate = (value) => {
|
||||
setUpdated(user.name !== value);
|
||||
};
|
||||
@@ -59,12 +62,12 @@ export function UserNameForm({ user }: UserNameFormProps) {
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<SectionColumns
|
||||
title="Your Name"
|
||||
description="Please enter a display name you are comfortable with."
|
||||
title={t("Your Name")}
|
||||
description={t("Please enter a display name you are comfortable with")}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="name">
|
||||
Name
|
||||
{t("Name")}
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -82,10 +85,7 @@ export function UserNameForm({ user }: UserNameFormProps) {
|
||||
{isPending ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>
|
||||
Save
|
||||
<span className="hidden sm:inline-flex"> Changes</span>
|
||||
</p>
|
||||
<p>{t("Save")}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -95,7 +95,9 @@ export function UserNameForm({ user }: UserNameFormProps) {
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[13px] text-muted-foreground">Max 32 characters</p>
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
{t("Max 32 characters")}
|
||||
</p>
|
||||
</div>
|
||||
</SectionColumns>
|
||||
</form>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { updateUserRole, type FormData } from "@/actions/update-user-role";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { User, UserRole } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -42,6 +43,8 @@ export function UserRoleForm({ user }: UserNameFormProps) {
|
||||
const roles = Object.values(UserRole);
|
||||
const [role, setRole] = useState(user.role);
|
||||
|
||||
const t = useTranslations("Setting");
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(userRoleSchema),
|
||||
values: {
|
||||
@@ -69,8 +72,8 @@ export function UserRoleForm({ user }: UserNameFormProps) {
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SectionColumns
|
||||
title="Your Role"
|
||||
description="Select the role what you want for test the app."
|
||||
title={t("Your Role")}
|
||||
description={t("Select the role what you want for this app")}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<FormField
|
||||
@@ -78,7 +81,7 @@ export function UserRoleForm({ user }: UserNameFormProps) {
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full space-y-0">
|
||||
<FormLabel className="sr-only">Role</FormLabel>
|
||||
<FormLabel className="sr-only">{t("Role")}</FormLabel>
|
||||
<Select
|
||||
// TODO:(FIX) Option value not update. Use useState for the moment
|
||||
onValueChange={(value: UserRole) => {
|
||||
@@ -97,7 +100,7 @@ export function UserRoleForm({ user }: UserNameFormProps) {
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role} value={role.toString()}>
|
||||
{role}
|
||||
{t(role)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -115,18 +118,10 @@ export function UserRoleForm({ user }: UserNameFormProps) {
|
||||
{isPending ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>
|
||||
Save
|
||||
<span className="hidden sm:inline-flex"> Changes</span>
|
||||
</p>
|
||||
<p>{t("Save")}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between p-1">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Remove this field on real production
|
||||
</p>
|
||||
</div>
|
||||
</SectionColumns>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -23,6 +24,8 @@ function DeleteAccountModal({
|
||||
const { data: session } = useSession();
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const t = useTranslations("Setting");
|
||||
|
||||
async function deleteAccount() {
|
||||
setDeleting(true);
|
||||
await fetch(`/api/user`, {
|
||||
@@ -63,13 +66,13 @@ function DeleteAccountModal({
|
||||
email: session?.user?.email || null,
|
||||
}}
|
||||
/>
|
||||
<h3 className="text-lg font-semibold">Delete Account</h3>
|
||||
<h3 className="text-lg font-semibold">{t("Delete Account")}</h3>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
<b>Warning:</b> This will permanently delete your account and your
|
||||
active subscription!
|
||||
<b>{t("Warning")}:</b>{" "}
|
||||
{t(
|
||||
"This will permanently delete your account and your active subscription!",
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* TODO: Use getUserSubscriptionPlan(session.user.id) to display the user's subscription if he have a paid plan */}
|
||||
</div>
|
||||
|
||||
<form
|
||||
@@ -89,7 +92,7 @@ function DeleteAccountModal({
|
||||
<span className="font-semibold text-black dark:text-white">
|
||||
confirm delete account
|
||||
</span>{" "}
|
||||
below
|
||||
below:
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
@@ -97,7 +97,6 @@ export function formatTime(input: string | number): string {
|
||||
});
|
||||
}
|
||||
|
||||
// Utils from precedent.dev
|
||||
export const timeAgo = (timestamp: Date, timeOnly?: boolean): string => {
|
||||
if (!timestamp) return "never";
|
||||
return `${ms(Date.now() - new Date(timestamp).getTime())}${
|
||||
|
||||
158
locales/en.json
158
locales/en.json
@@ -1,5 +1,26 @@
|
||||
{
|
||||
"Common": {},
|
||||
"Common": {
|
||||
"Set up an administrator": "Set up an administrator",
|
||||
"Add the first domain": "Add the first domain",
|
||||
"Congrats on completing setup 🎉": "Congrats on completing setup 🎉",
|
||||
"Admin Setup Guide": "Admin Setup Guide",
|
||||
"Previous": "Previous",
|
||||
"Next": "Next",
|
||||
"🚀 Start": "🚀 Start",
|
||||
"Ready": "Ready",
|
||||
"Allow Sign Up": "Allow Sign Up",
|
||||
"Set {email} as ADMIN": "Set {email} as ADMIN",
|
||||
"Active Now": "Active Now",
|
||||
"Only by becoming an administrator can one access the admin panel and add domain names": "Only by becoming an administrator can one access the admin panel and add domain names",
|
||||
"Administrators can set all user permissions, allocate quotas, view and edit all resources (short links, subdomains, email), etc": "Administrators can set all user permissions, allocate quotas, view and edit all resources (short links, subdomains, email), etc",
|
||||
"Via": "Via",
|
||||
"quick start": "quick start",
|
||||
"docs to get more information": "docs to get more information",
|
||||
"Domain Name": "Domain Name",
|
||||
"Please enter a valid domain name (must be hosted on Cloudflare)": "Please enter a valid domain name (must be hosted on Cloudflare)",
|
||||
"Or add later": "Or add later",
|
||||
"Submit": "Submit"
|
||||
},
|
||||
"List": {
|
||||
"Short URLs": "Short URLs",
|
||||
"Manage Short URLs": "Manage Short URLs",
|
||||
@@ -88,7 +109,35 @@
|
||||
"Example": "E.g.",
|
||||
"Time To Live": "Time To Live",
|
||||
"Proxy": "Proxy",
|
||||
"Proxy status": "DNS response is replaced by Cloudflare Anycast IP"
|
||||
"Proxy status": "DNS response is replaced by Cloudflare Anycast IP",
|
||||
"Total Domains": "Total Domains",
|
||||
"Add Domain": "Add Domain",
|
||||
"Domain Name": "Domain Name",
|
||||
"Shorten Service": "Shorten Service",
|
||||
"Email Service": "Email Service",
|
||||
"Subdomain Service": "Subdomain Service",
|
||||
"Active": "Active",
|
||||
"Search by domain name": "Search by domain name",
|
||||
"No Domains": "No Domains",
|
||||
"Verified": "Verified",
|
||||
"Verify Configuration": "Verify Configuration",
|
||||
"Create Domain": "Create Domain",
|
||||
"Edit Domain": "Edit Domain",
|
||||
"Base": "Base",
|
||||
"Services": "Services",
|
||||
"Cloudflare Configs": "Cloudflare Configs",
|
||||
"Zone ID": "Zone ID",
|
||||
"API Token": "API Token",
|
||||
"Account Email": "Account Email",
|
||||
"How to get zone id?": "How to get zone id?",
|
||||
"How to get api token?": "How to get api token?",
|
||||
"How to get cloudflare account email?": "How to get cloudflare account email?",
|
||||
"Resend Configs": "Resend Configs",
|
||||
"Associate with 'Subdomain Service' status": "Associate with 'Subdomain Service' status",
|
||||
"Associate with 'Email Service' status": "Associate with 'Email Service' status",
|
||||
"API Key": "API Key",
|
||||
"send email service": "send email service",
|
||||
"How to get resend api key?": "How to get resend api key?"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -146,7 +195,10 @@
|
||||
"Take a screenshot of the webpage": "Take a screenshot of the webpage",
|
||||
"Extract website metadata": "Extract website metadata",
|
||||
"Convert website content to Markdown format": "Convert website content to Markdown format",
|
||||
"Convert website content to text": "Convert website content to text"
|
||||
"Convert website content to text": "Convert website content to text",
|
||||
"Emails": "Emails",
|
||||
"Inbox": "Inbox",
|
||||
"Users": "Users"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "Settings",
|
||||
@@ -187,6 +239,19 @@
|
||||
"enterpriseBestFor": "For large organizations with custom needs",
|
||||
"contactUs": "Contact us"
|
||||
},
|
||||
"Auth": {
|
||||
"Back": "Back",
|
||||
"Welcome to": "Welcome to",
|
||||
"Choose your login method to continue": "Choose your login method to continue",
|
||||
"By clicking continue, you agree to our": "By clicking continue, you agree to our",
|
||||
"Terms of Service": "Terms of Service",
|
||||
"and": "and",
|
||||
"Privacy Policy": "Privacy Policy",
|
||||
"Or continue with": "Or continue with",
|
||||
"Email": "Email",
|
||||
"Sign Up with Email": "Sign Up with Email",
|
||||
"Sign In with Email": "Sign In with Email"
|
||||
},
|
||||
"System": {
|
||||
"MENU": "Menu",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -219,5 +284,92 @@
|
||||
"Admin": "Admin",
|
||||
"Sign in": "Sign in",
|
||||
"Log out": "Log out"
|
||||
},
|
||||
"Email": {
|
||||
"Search emails": "Search emails",
|
||||
"Create New Email": "Create New Email",
|
||||
"Email Address": "Email Address",
|
||||
"Inbox Emails": "Inbox Emails",
|
||||
"Unread Emails": "Unread Emails",
|
||||
"Filter unread emails": "Filter unread emails",
|
||||
"Sent Emails": "Sent Emails",
|
||||
"Admin Mode": "Admin Mode",
|
||||
"No emails": "No emails",
|
||||
"{email} recived": "{email} recived",
|
||||
"Edit email": "Edit email",
|
||||
"Create new email": "Create new email",
|
||||
"Enter email prefix": "Enter email prefix",
|
||||
"No domains configured": "No domains configured",
|
||||
"Cancel": "Cancel",
|
||||
"Update": "Update",
|
||||
"Create": "Create",
|
||||
"Failed to load emails": "Failed to load emails",
|
||||
"Please try again": "Please try again",
|
||||
"No emails found": "No emails found",
|
||||
"Search by send to email": "Search by send to email",
|
||||
"Delete email": "Delete email",
|
||||
"You are about to delete the following email, once deleted, it cannot be recovered": "You are about to delete the following email, once deleted, it cannot be recovered",
|
||||
"All emails in inbox will be deleted at the same time": "All emails in inbox will be deleted at the same time",
|
||||
"Are you sure you want to continue?": "Are you sure you want to continue?",
|
||||
"To confirm, please type": "To confirm, please type",
|
||||
"delete": "delete",
|
||||
"below": "below",
|
||||
"Confirm Delete": "Confirm Delete",
|
||||
"INBOX": "INBOX",
|
||||
"Auto refresh": "Auto refresh",
|
||||
"more": "more",
|
||||
"Mask as read": "Mask as read",
|
||||
"Delete selected": "Delete selected",
|
||||
"Waiting for emails": "Waiting for emails",
|
||||
"No Email Address Selected": "No Email Address Selected",
|
||||
"Please select an email address from the list to view your inbox": "Please select an email address from the list to view your inbox",
|
||||
"Once selected, your emails will appear here automatically": "Once selected, your emails will appear here automatically",
|
||||
"How to use email to send or receive emails?": "How to use email to send or receive emails?",
|
||||
"Will my email or inbox expire?": "Will my email or inbox expire?",
|
||||
"What is the limit? It's free?": "What is the limit? It's free?",
|
||||
"How to create emails with api?": "How to create emails with api?",
|
||||
"Send Email": "Send Email",
|
||||
"From": "From",
|
||||
"To": "To",
|
||||
"Subject": "Subject",
|
||||
"Content": "Content",
|
||||
"Send": "Send",
|
||||
"Sending": "Sending",
|
||||
"Reply-To": "Reply-To",
|
||||
"Attachments": "Attachments",
|
||||
"Date": "Date"
|
||||
},
|
||||
"Scrape": {
|
||||
"Playground": "Playground",
|
||||
"Automate your website screenshots and turn them into stunning visuals for your applications": "Automate your website screenshots and turn them into stunning visuals for your applications",
|
||||
"Start": "Start",
|
||||
"Scraping": "Scraping",
|
||||
"Scrape the meta data of a website": "Scrape the meta data of a website",
|
||||
"Text": "Text"
|
||||
},
|
||||
"Setting": {
|
||||
"Name": "Name",
|
||||
"Your Name": "Your Name",
|
||||
"Save": "Save",
|
||||
"Please enter a display name you are comfortable with": "Please enter a display name you are comfortable with",
|
||||
"Max 32 characters": "Max 32 characters",
|
||||
"Your Role": "Your Role",
|
||||
"Role": "Role",
|
||||
"ADMIN": "ADMIN",
|
||||
"USER": "USER",
|
||||
"Select the role what you want for this app": "Select the role what you want for this app",
|
||||
"API Key": "API Key",
|
||||
"Generate": "Generate",
|
||||
"Generate a new API key to access the open apis": "Generate a new API key to access the open apis",
|
||||
"Delete Account": "Delete Account",
|
||||
"This is a danger zone - Be careful !": "This is a danger zone - Be careful !",
|
||||
"Are you sure": "Are you sure",
|
||||
"Active Subscription": "Active Subscription",
|
||||
"Permanently delete your {name} account": "Permanently delete your {name} account",
|
||||
" and your subscription": " and your subscription",
|
||||
"This action cannot be undone - please proceed with caution": "This action cannot be undone - please proceed with caution",
|
||||
"Warning": "Warning",
|
||||
"This will permanently delete your account and your active subscription!": "This will permanently delete your account and your active subscription!",
|
||||
"verification": "verification"
|
||||
}
|
||||
}
|
||||
|
||||
158
locales/zh.json
158
locales/zh.json
@@ -1,5 +1,26 @@
|
||||
{
|
||||
"Common": {},
|
||||
"Common": {
|
||||
"Set up an administrator": "设置管理员",
|
||||
"Add the first domain": "添加第一个域名",
|
||||
"Congrats on completing setup 🎉": "恭喜完成初始化配置 🎉",
|
||||
"Admin Setup Guide": "初始化引导",
|
||||
"Previous": "上一步",
|
||||
"Next": "下一步",
|
||||
"🚀 Start": "🚀 开始",
|
||||
"Ready": "已开启",
|
||||
"Allow Sign Up": "允许注册",
|
||||
"Set {email} as ADMIN": "设置 {email} 为管理员",
|
||||
"Active Now": "立即激活",
|
||||
"Only by becoming an administrator can one access the admin panel and add domain names": "只有成为管理员后才能访问管理员面板并添加域名",
|
||||
"Administrators can set all user permissions, allocate quotas, view and edit all resources (short links, subdomains, email), etc": "管理员可以设置所有用户权限,分配配额,查看和编辑所有资源(短链,子域名,邮件)",
|
||||
"Via": "查看",
|
||||
"quick start": "快速开始",
|
||||
"docs to get more information": "文档获取更多信息",
|
||||
"Domain Name": "域名",
|
||||
"Please enter a valid domain name (must be hosted on Cloudflare)": "请输入有效的域名(确保已经托管到 Cloudflare)",
|
||||
"Or add later": "或稍后添加",
|
||||
"Submit": "提交"
|
||||
},
|
||||
"List": {
|
||||
"Short URLs": "短链列表",
|
||||
"Manage Short URLs": "管理短链",
|
||||
@@ -88,7 +109,35 @@
|
||||
"Example": "例如",
|
||||
"Time To Live": "生效时间",
|
||||
"Proxy": "代理记录",
|
||||
"Proxy status": "DNS 响应被 Cloudflare Anycast IP 替代"
|
||||
"Proxy status": "DNS 响应被 Cloudflare Anycast IP 替代",
|
||||
"Total Domains": "总计",
|
||||
"Add Domain": "添加域名",
|
||||
"Domain Name": "域名",
|
||||
"Shorten Service": "短链服务",
|
||||
"Email Service": "邮件服务",
|
||||
"Subdomain Service": "子域名服务",
|
||||
"Active": "启用",
|
||||
"Search by domain name": "搜索域名",
|
||||
"No Domains": "暂无域名",
|
||||
"Verified": "已就绪",
|
||||
"Verify Configuration": "验证配置",
|
||||
"Create Domain": "创建域名",
|
||||
"Edit Domain": "编辑域名",
|
||||
"Base": "基础配置",
|
||||
"Services": "服务配置",
|
||||
"Cloudflare Configs": "Cloudflare 配置",
|
||||
"Zone ID": "Zone ID",
|
||||
"API Token": "API Token",
|
||||
"Account Email": "账户邮箱",
|
||||
"How to get zone id?": "如何获取 Zone ID?",
|
||||
"How to get api token?": "如何获取 API Token?",
|
||||
"How to get cloudflare account email?": "如何获取账户邮箱?",
|
||||
"Resend Configs": "Resend 配置",
|
||||
"Associate with 'Subdomain Service' status": "与 '子域名服务' 启用状态关联",
|
||||
"Associate with 'Email Service' status": "与 '邮件服务' 启用状态关联",
|
||||
"API Key": "API 密钥",
|
||||
"send email service": "用于发送邮件服务",
|
||||
"How to get resend api key?": "如何获取 Resend API 密钥?"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "用户面板",
|
||||
@@ -146,7 +195,10 @@
|
||||
"Take a screenshot of the webpage": "使用 API 提取网页的截图",
|
||||
"Extract website metadata": "使用 API 提取网页的元数据",
|
||||
"Convert website content to Markdown format": "使用 API 将网页内容转换为 Markdown 格式",
|
||||
"Convert website content to text": "使用 API 将网页内容转换为文本"
|
||||
"Convert website content to text": "使用 API 将网页内容转换为文本",
|
||||
"Emails": "邮箱",
|
||||
"Inbox": "收件箱",
|
||||
"Users": "用户"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "设置",
|
||||
@@ -187,6 +239,19 @@
|
||||
"enterpriseBestFor": "适合有定制需求的大型组织",
|
||||
"contactUs": "联系我们"
|
||||
},
|
||||
"Auth": {
|
||||
"Back": "返回",
|
||||
"Welcome to": "欢迎使用",
|
||||
"Choose your login method to continue": "选择登录方式以继续",
|
||||
"By clicking continue, you agree to our": "点击继续即表示您同意我们的",
|
||||
"Terms of Service": "服务条款",
|
||||
"and": "和",
|
||||
"Privacy Policy": "隐私政策",
|
||||
"Or continue with": "或使用",
|
||||
"Email": "邮箱",
|
||||
"Sign Up with Email": "使用邮箱注册",
|
||||
"Sign In with Email": "使用邮箱登录"
|
||||
},
|
||||
"System": {
|
||||
"MENU": "菜单",
|
||||
"Dashboard": "控制台",
|
||||
@@ -219,5 +284,92 @@
|
||||
"Admin": "管理面板",
|
||||
"Sign in": "登录",
|
||||
"Log out": "退出登录"
|
||||
},
|
||||
"Email": {
|
||||
"Search emails": "搜索邮箱...",
|
||||
"Create New Email": "创建新邮箱",
|
||||
"Email Address": "邮箱地址",
|
||||
"Inbox Emails": "收件箱",
|
||||
"Unread Emails": "未读邮件",
|
||||
"Filter unread emails": "过滤未读邮件",
|
||||
"Sent Emails": "已发送",
|
||||
"Admin Mode": "管理员模式",
|
||||
"No emails": "暂无邮件",
|
||||
"{email} recived": "{email} 封邮件",
|
||||
"Edit email": "编辑邮箱",
|
||||
"Create new email": "创建新邮箱",
|
||||
"Enter email prefix": "请输入邮箱前缀",
|
||||
"No domains configured": "未配置域名",
|
||||
"Cancel": "取消",
|
||||
"Update": "更新",
|
||||
"Create": "创建",
|
||||
"Failed to load emails": "加载邮件失败",
|
||||
"Please try again": "请重试",
|
||||
"No emails found": "未找到邮件",
|
||||
"Search by send to email": "搜索收件人邮箱...",
|
||||
"Delete email": "删除邮箱",
|
||||
"You are about to delete the following email, once deleted, it cannot be recovered": "您即将删除此邮件,一旦删除,无法恢复",
|
||||
"All emails in inbox will be deleted at the same time": "所有收件箱中的邮件将同时删除",
|
||||
"Are you sure you want to continue?": "确定要继续吗?",
|
||||
"To confirm, please type": "若确认请在输入框输入",
|
||||
"delete": "删除",
|
||||
"below": "下面",
|
||||
"Confirm Delete": "确认删除",
|
||||
"INBOX": "收件箱",
|
||||
"Auto refresh": "自动刷新",
|
||||
"more": "更多",
|
||||
"Mask as read": "标记为已读",
|
||||
"Delete selected": "删除选中",
|
||||
"Waiting for emails": "等待接收邮件",
|
||||
"No Email Address Selected": "暂未选择邮箱地址",
|
||||
"Please select an email address from the list to view your inbox": "请从列表中选择一个邮箱地址以查看收件箱",
|
||||
"Once selected, your emails will appear here automatically": "选择后,您的邮件将自动显示在这里",
|
||||
"How to use email to send or receive emails?": "如何使用邮箱发送或接收邮件?",
|
||||
"Will my email or inbox expire?": "我的邮箱或收件箱是否会过期?",
|
||||
"What is the limit? It's free?": "邮箱使用有限制吗?是否免费?",
|
||||
"How to create emails with api?": "如何使用 API 创建邮箱地址?",
|
||||
"Send Email": "发送邮件",
|
||||
"From": "发件人",
|
||||
"To": "收件人",
|
||||
"Subject": "主题",
|
||||
"Content": "内容",
|
||||
"Send": "发送",
|
||||
"Sending": "发送中...",
|
||||
"Reply-To": "回复",
|
||||
"Attachments": "附件",
|
||||
"Date": "日期"
|
||||
},
|
||||
"Scrape": {
|
||||
"Playground": "在线体验",
|
||||
"Automate your website screenshots and turn them into stunning visuals for your applications": "自动化截图并转换成多种格式图片",
|
||||
"Start": "开始",
|
||||
"Scraping": "处理中...",
|
||||
"Scrape the meta data of a website": "快速从网站中抓取元数据",
|
||||
"Text": "文本"
|
||||
},
|
||||
"Setting": {
|
||||
"Name": "昵称",
|
||||
"Your Name": "您的昵称",
|
||||
"Save": "保存",
|
||||
"Please enter a display name you are comfortable with": "请输入您的昵称,未设置则为匿名",
|
||||
"Max 32 characters": "最多32个字符",
|
||||
"Your Role": "您的角色",
|
||||
"Role": "角色",
|
||||
"ADMIN": "系统管理员",
|
||||
"USER": "普通用户",
|
||||
"Select the role what you want for this app": "设置您的权限角色",
|
||||
"API Key": "API 密钥",
|
||||
"Generate": "生成",
|
||||
"Generate a new API key to access the open apis": "生成你的专属 API 密钥",
|
||||
"Delete Account": "删除账户",
|
||||
"This is a danger zone - Be careful !": "这是一个危险操作!",
|
||||
"Are you sure": "确定删除?",
|
||||
"Active Subscription": "包含订阅",
|
||||
"Permanently delete your {name} account": "永久删除您的 {name} 账户",
|
||||
" and your subscription": "和您的订阅",
|
||||
"This action cannot be undone - please proceed with caution": "此操作不可逆,请谨慎操作",
|
||||
"Warning": "警告",
|
||||
"This will permanently delete your account and your active subscription!": "这将永久删除您的账户和订阅!",
|
||||
"verification": "为了验证,请在下方输入 <confirm>确认删除账户</confirm>"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user