+1
-1
@@ -42,7 +42,7 @@ SCREENSHOTONE_BASE_URL=https://shot.wr.do
|
||||
# GitHub api token for getting gitHub stars count
|
||||
GITHUB_TOKEN=
|
||||
|
||||
# Skip DB check and migration (only for docker). if false, will check and migrate database each time start docker compose.
|
||||
# Skip DB check and migration. if false, will check and migrate database each time start docker compose.
|
||||
SKIP_DB_CHECK=false
|
||||
SKIP_DB_MIGRATION=false
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- fix/docker
|
||||
- i18n
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
|
||||
+9
-1
@@ -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>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import HeroLanding, { LandingImages } from "@/components/sections/hero-landing";
|
||||
import { PricingSection } from "@/components/sections/pricing";
|
||||
|
||||
export default function IndexPage() {
|
||||
export default async function IndexPage() {
|
||||
const user = await getCurrentUser();
|
||||
return (
|
||||
<>
|
||||
<HeroLanding />
|
||||
<HeroLanding userId={user?.id} />
|
||||
<LandingImages />
|
||||
<PricingSection />
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { TrendingUp } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Label,
|
||||
PolarGrid,
|
||||
@@ -29,6 +30,7 @@ export function RadialShapeChart({
|
||||
total: number;
|
||||
totalUser: number;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
const chartData = [
|
||||
{ browser: "safari", actived: total, fill: "var(--color-safari)" },
|
||||
];
|
||||
@@ -94,7 +96,7 @@ export function RadialShapeChart({
|
||||
<TrendingUp className="size-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Cumulative proportion of activated <strong>Api Key</strong> users
|
||||
{t("Activated Api Key users")}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -4,11 +4,12 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { User } from "@prisma/client";
|
||||
import { PenLine, RefreshCwIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { DomainFormData } from "@/lib/dto/domains";
|
||||
import { fetcher, timeAgo } from "@/lib/utils";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -34,6 +35,7 @@ import { FormType } from "@/components/forms/record-form";
|
||||
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
import { PaginationWrapper } from "@/components/shared/pagination";
|
||||
import { TimeAgoIntl } from "@/components/shared/time-ago";
|
||||
|
||||
export interface DomainListProps {
|
||||
user: Pick<User, "id" | "name" | "email" | "apiKey" | "role" | "team">;
|
||||
@@ -70,6 +72,7 @@ function TableColumnSekleton() {
|
||||
|
||||
export default function DomainList({ user, action }: DomainListProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const t = useTranslations("List");
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
const [currentEditDomain, setCurrentEditDomain] =
|
||||
@@ -130,7 +133,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" />
|
||||
) : (
|
||||
@@ -161,7 +164,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>
|
||||
@@ -170,7 +173,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({
|
||||
@@ -197,25 +200,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>
|
||||
@@ -289,7 +292,7 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex items-center truncate">
|
||||
{timeAgo(domain.updatedAt as Date)}
|
||||
<TimeAgoIntl date={domain.updatedAt as Date} />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex items-center gap-1">
|
||||
<Button
|
||||
@@ -303,7 +306,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>
|
||||
@@ -316,7 +319,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>
|
||||
|
||||
@@ -19,10 +19,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage Domains"
|
||||
text="List and manage domains."
|
||||
heading="Domains Management"
|
||||
text="List and manage domains"
|
||||
link="/docs/developer/cloudflare"
|
||||
linkText="domains."
|
||||
linkText="domains"
|
||||
/>
|
||||
<DomainList
|
||||
user={{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ScrapeMeta } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { useElementSize } from "@/hooks/use-element-size";
|
||||
@@ -58,6 +59,8 @@ export function LineChartMultiple({
|
||||
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
|
||||
const processedData = processChartData(chartData, type1, type2);
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const chartConfig = {
|
||||
source1: {
|
||||
label: type1,
|
||||
@@ -69,13 +72,14 @@ export function LineChartMultiple({
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const message = type2
|
||||
? t("total-requests-two-types", { type1, type2 })
|
||||
: t("total-requests-one-type", { type1 });
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
Total requests of {type1}
|
||||
{type2 && ` and ${type2}`}.
|
||||
</CardDescription>
|
||||
<CardDescription>{message}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent ref={wrapperRef}>
|
||||
<ChartContainer config={chartConfig}>
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function AdminPanelLoading() {
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Admin Panel"
|
||||
text="Access only for users with ADMIN role."
|
||||
text="Access only for users with ADMIN role"
|
||||
/>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-3">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Suspense } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record";
|
||||
import {
|
||||
@@ -134,7 +135,6 @@ async function RequestStatsSection() {
|
||||
|
||||
return hasStats ? (
|
||||
<>
|
||||
<h2 className="my-1 text-xl font-semibold">Request Statistics</h2>
|
||||
<DailyPVUVChart
|
||||
data={screenshot_stats
|
||||
.concat(meta_stats)
|
||||
@@ -200,7 +200,6 @@ async function MarkdownTextChartSection() {
|
||||
async function LogsSection({ userId }: { userId: string }) {
|
||||
return (
|
||||
<>
|
||||
<h2 className="my-1 text-xl font-semibold">Request Logs</h2>
|
||||
<LogsTable userId={userId} target={"/api/v1/scraping/admin/logs"} />
|
||||
</>
|
||||
);
|
||||
@@ -213,10 +212,7 @@ export default async function AdminPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Admin Panel"
|
||||
text="Access only for users with ADMIN role."
|
||||
/>
|
||||
<DashboardHeader heading="Admin Panel" text="" />
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 xl:grid-cols-3">
|
||||
<ErrorBoundary
|
||||
|
||||
@@ -4,7 +4,10 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="DNS Records" text="" />
|
||||
<DashboardHeader
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -19,10 +19,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records."
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records"
|
||||
link="/docs/dns-records"
|
||||
linkText="DNS records."
|
||||
linkText="DNS records"
|
||||
/>
|
||||
<UserRecordsList
|
||||
user={{
|
||||
|
||||
@@ -4,7 +4,10 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardUrlsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Short Urls" text="" />
|
||||
<DashboardHeader
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -19,10 +19,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls."
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls"
|
||||
link="/docs/short-urls"
|
||||
linkText="short urls."
|
||||
linkText="short urls"
|
||||
/>
|
||||
|
||||
<UserUrlsList
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function OrdersLoading() {
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="User Management"
|
||||
text="List and manage all users."
|
||||
text="List and manage all users"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function UsersPage() {
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="User Management"
|
||||
text="List and manage all users."
|
||||
text="List and manage all users"
|
||||
/>
|
||||
<UsersList user={{ id: user.id, name: user.name || "" }} />
|
||||
</>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { User } from "@prisma/client";
|
||||
import { PenLine, RefreshCwIcon } from "lucide-react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { fetcher, timeAgo } from "@/lib/utils";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -37,6 +37,7 @@ import { UserForm } from "@/components/forms/user-form";
|
||||
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
import { PaginationWrapper } from "@/components/shared/pagination";
|
||||
import { TimeAgoIntl } from "@/components/shared/time-ago";
|
||||
|
||||
import CountUpFn from "../../../../components/dashboard/count-up";
|
||||
|
||||
@@ -105,9 +106,7 @@ export default function UsersList({ user }: UrlListProps) {
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
<CardDescription className="text-balance text-lg font-bold">
|
||||
<span>Total Users:</span>{" "}
|
||||
<span className="font-bold">
|
||||
{data && <CountUpFn count={data.total} />}
|
||||
</span>
|
||||
<span className="font-bold">{data && data.total}</span>
|
||||
</CardDescription>
|
||||
<div className="ml-auto flex items-center justify-end gap-3">
|
||||
<Button
|
||||
@@ -252,7 +251,7 @@ export default function UsersList({ user }: UrlListProps) {
|
||||
<Switch defaultChecked={user.active === 1} />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 hidden justify-center sm:flex">
|
||||
{timeAgo(user.createdAt || "")}
|
||||
<TimeAgoIntl date={user.updatedAt as Date} />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex justify-center">
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
|
||||
export default function DashboardLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Dashboard" text="" />
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-3">
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
|
||||
@@ -13,11 +13,9 @@ import {
|
||||
DashboardInfoCard,
|
||||
HeroCard,
|
||||
} from "@/components/dashboard/dashboard-info-card";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import { ErrorBoundary } from "@/components/shared/error-boundary";
|
||||
|
||||
import UserRecordsList from "./records/record-list";
|
||||
import LiveLog from "./urls/live-logs";
|
||||
import UserUrlsList from "./urls/url-list";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
@@ -87,10 +85,6 @@ async function DnsRecordsCardSection({
|
||||
);
|
||||
}
|
||||
|
||||
async function LiveLogSection() {
|
||||
return <LiveLog admin={false} />;
|
||||
}
|
||||
|
||||
async function UserUrlsListSection({
|
||||
user,
|
||||
}: {
|
||||
@@ -148,7 +142,7 @@ export default async function DashboardPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Dashboard" text="" />
|
||||
{/* <DashboardHeader heading="Dashboard" text="" /> */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 xl:grid-cols-3">
|
||||
<ErrorBoundary
|
||||
|
||||
@@ -5,8 +5,8 @@ export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records."
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
|
||||
@@ -19,10 +19,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records."
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records"
|
||||
link="/docs/dns-records"
|
||||
linkText="DNS records."
|
||||
linkText="DNS records"
|
||||
/>
|
||||
<UserRecordsList
|
||||
user={{
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { User } from "@prisma/client";
|
||||
import { PenLine, RefreshCwIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { UserRecordFormData } from "@/lib/dto/cloudflare-dns-record";
|
||||
import { TTL_ENUMS } from "@/lib/enums";
|
||||
import { fetcher, timeAgo } from "@/lib/utils";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -42,6 +43,7 @@ import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
import { LinkInfoPreviewer } from "@/components/shared/link-previewer";
|
||||
import { PaginationWrapper } from "@/components/shared/pagination";
|
||||
import { TimeAgoIntl } from "@/components/shared/time-ago";
|
||||
|
||||
export interface RecordListProps {
|
||||
user: Pick<User, "id" | "name" | "apiKey" | "email" | "role">;
|
||||
@@ -87,9 +89,10 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
useState<UserRecordFormData | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [tab, setTab] = useState("app");
|
||||
const isAdmin = action.includes("/admin");
|
||||
|
||||
const t = useTranslations("List");
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { data, isLoading } = useSWR<{
|
||||
@@ -144,30 +147,30 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
{isAdmin ? (
|
||||
<CardDescription className="text-balance text-lg font-bold">
|
||||
<span>Total Subdomains:</span>{" "}
|
||||
<span>{t("Total Subdomains")}:</span>{" "}
|
||||
<span className="font-bold">{data && data.total}</span>
|
||||
</CardDescription>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
<CardTitle>Subdomains</CardTitle>
|
||||
<CardTitle>{t("Subdomain List")}</CardTitle>
|
||||
<CardDescription className="hidden text-balance sm:block">
|
||||
Please read the{" "}
|
||||
{t("Please read the")}{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
className="font-semibold text-yellow-600 after:content-['↗'] hover:underline"
|
||||
href="/docs/dns-records#legitimacy-review"
|
||||
>
|
||||
Legitimacy review
|
||||
{t("legitimacy review")}
|
||||
</Link>{" "}
|
||||
before using. See{" "}
|
||||
{t("before using")}. {t("See")}{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:underline"
|
||||
href="/docs/examples/vercel"
|
||||
>
|
||||
examples
|
||||
{t("examples")}
|
||||
</Link>{" "}
|
||||
for more usage.
|
||||
{t("for more usage")}.
|
||||
</CardDescription>
|
||||
</div>
|
||||
)}
|
||||
@@ -194,7 +197,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
}}
|
||||
>
|
||||
<Icons.add className="size-4" />
|
||||
<span className="hidden sm:inline">Add Record</span>
|
||||
<span className="hidden sm:inline">{t("Add Record")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -203,28 +206,28 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
<TableHeader className="bg-gray-100/50 dark:bg-primary-foreground">
|
||||
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-9">
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Type
|
||||
{t("Type")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Name
|
||||
{t("Name")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-2 hidden items-center font-bold sm:flex">
|
||||
Content
|
||||
{t("Content")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
TTL
|
||||
{t("TTL")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
|
||||
Status
|
||||
{t("Status")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
User
|
||||
{t("User")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
|
||||
Updated
|
||||
{t("Updated")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center justify-center font-bold">
|
||||
Actions
|
||||
{t("Actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -278,8 +281,11 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
onChangeStatu={handleChangeStatu}
|
||||
/>
|
||||
) : (
|
||||
<Badge className="rounded-md" variant={"yellow"}>
|
||||
Pending
|
||||
<Badge
|
||||
className="text-nowrap rounded-md"
|
||||
variant={"yellow"}
|
||||
>
|
||||
{t("Pending")}
|
||||
</Badge>
|
||||
)}
|
||||
{record.active !== 1 && (
|
||||
@@ -291,16 +297,21 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
<TooltipContent>
|
||||
{record.active === 0 && (
|
||||
<ul className="list-disc px-3">
|
||||
<li>The target is currently inaccessible.</li>
|
||||
<li>
|
||||
Please check the target and try again.
|
||||
{t("The target is currently inaccessible")}.
|
||||
</li>
|
||||
<li>
|
||||
If the target is not activated within 3
|
||||
days, <br />
|
||||
the administrator will{" "}
|
||||
{t("Please check the target and try again")}
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
"If the target is not activated within 3 days",
|
||||
)}
|
||||
, <br />
|
||||
{t("the administrator will")}{" "}
|
||||
<strong className="text-red-500">
|
||||
delete this record
|
||||
{t("delete this record")}
|
||||
</strong>
|
||||
.
|
||||
</li>
|
||||
@@ -309,8 +320,10 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
{record.active === 2 && (
|
||||
<ul className="list-disc px-3">
|
||||
<li>
|
||||
The record is currently pending for admin
|
||||
approval.
|
||||
{t(
|
||||
"The record is currently pending for admin approval",
|
||||
)}
|
||||
.
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
@@ -332,12 +345,14 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 hidden justify-center sm:flex">
|
||||
{timeAgo(record.modified_on as unknown as Date)}
|
||||
<TimeAgoIntl
|
||||
date={record.modified_on as unknown as Date}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex justify-center">
|
||||
{record.active !== 2 ? (
|
||||
<Button
|
||||
className="text-sm hover:bg-slate-100 dark:hover:text-primary-foreground"
|
||||
className="text-nowrap text-sm hover:bg-slate-100 dark:hover:text-primary-foreground"
|
||||
size="sm"
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
@@ -347,7 +362,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<p>Edit</p>
|
||||
<p>{t("Edit")}</p>
|
||||
<PenLine className="ml-1 size-4" />
|
||||
</Button>
|
||||
) : record.active === 2 &&
|
||||
@@ -364,7 +379,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<p>Review</p>
|
||||
<p>{t("Review")}</p>
|
||||
</Button>
|
||||
) : (
|
||||
"--"
|
||||
@@ -375,7 +390,9 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
) : (
|
||||
<EmptyPlaceholder className="shadow-none">
|
||||
<EmptyPlaceholder.Icon name="globe" />
|
||||
<EmptyPlaceholder.Title>No Subdomain</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Title>
|
||||
{t("No Subdomains")}
|
||||
</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Description>
|
||||
You don't have any subdomain yet. Start creating
|
||||
record.
|
||||
@@ -398,7 +415,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
className="max-h-[90vh] overflow-y-auto md:max-w-2xl"
|
||||
className="max-h-[99vh] overflow-y-auto md:max-w-2xl"
|
||||
showModal={isShowForm}
|
||||
setShowModal={setShowForm}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
getScrapeStatsByTypeAndUserId,
|
||||
getScrapeStatsByUserId,
|
||||
getScrapeStatsByUserId1,
|
||||
} from "@/lib/dto/scrape";
|
||||
|
||||
@@ -21,10 +20,7 @@ export default async function DashboardScrapeCharts({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
{all_user_logs && all_user_logs.length > 0 && (
|
||||
<>
|
||||
<h2 className="my-1 text-xl font-semibold">Request Statistics</h2>
|
||||
<DailyPVUVChart data={all_user_logs} />
|
||||
</>
|
||||
<DailyPVUVChart data={all_user_logs} />
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{(screenshot_stats.length > 0 || meta_stats.length > 0) && (
|
||||
@@ -43,7 +39,6 @@ export default async function DashboardScrapeCharts({ id }: { id: string }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 className="my-1 text-xl font-semibold">Request Logs</h2>
|
||||
<LogsTable userId={id} target={"/api/v1/scraping/logs"} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { ScrapeMeta } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { isLink, nFormatter, removeUrlSuffix, timeAgo } from "@/lib/utils";
|
||||
import { isLink, nFormatter, removeUrlSuffix } from "@/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import CountUp from "@/components/dashboard/count-up";
|
||||
import { TimeAgoIntl } from "@/components/shared/time-ago";
|
||||
|
||||
const chartConfig = {
|
||||
request: {
|
||||
@@ -102,18 +103,21 @@ export function DailyPVUVChart({ data }: { data: ScrapeMeta[] }) {
|
||||
(a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(),
|
||||
);
|
||||
const latestEntry = sort_data[sort_data.length - 1];
|
||||
const latestDate = timeAgo(latestEntry.updatedAt);
|
||||
const latestFrom = latestEntry.type;
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const lastRequestInfo = t.rich("last-request-info", {
|
||||
location: latestFrom,
|
||||
timeAgo: () => <TimeAgoIntl date={latestEntry.updatedAt} />,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-5 py-4">
|
||||
<CardTitle>Total Requests of APIs in Last 30 Days</CardTitle>
|
||||
<CardDescription>
|
||||
Last request from <strong>{latestFrom}</strong> api about{" "}
|
||||
{latestDate}.
|
||||
</CardDescription>
|
||||
<CardTitle>{t("Total Requests of APIs in Last 30 Days")}</CardTitle>
|
||||
<CardDescription>{lastRequestInfo}</CardDescription>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{["request", "ip"].map((key) => {
|
||||
@@ -125,8 +129,8 @@ export function DailyPVUVChart({ data }: { data: ScrapeMeta[] }) {
|
||||
className="relative z-30 flex flex-1 flex-col items-center justify-center gap-1 border-t px-6 py-2 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-3"
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chartConfig[chart].label}
|
||||
<span className="text-nowrap text-xs text-muted-foreground">
|
||||
{t(chartConfig[chart].label)}
|
||||
</span>
|
||||
<span className="text-lg font-bold leading-none">
|
||||
{nFormatter(dataTotal[key])}
|
||||
|
||||
@@ -5,8 +5,8 @@ export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Scraping API Overview"
|
||||
text="Quickly extract valuable structured website data. It's free and unlimited to use!"
|
||||
heading="Scraping API Overview"
|
||||
text="Quickly extract valuable structured website data"
|
||||
/>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-3">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { nFormatter } from "@/lib/utils";
|
||||
@@ -49,6 +50,8 @@ const LogsTable = ({ userId, target }) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
type: "",
|
||||
ip: "",
|
||||
@@ -141,13 +144,17 @@ const LogsTable = ({ userId, target }) => {
|
||||
<TableHeader className="bg-muted">
|
||||
<TableRow className="grid grid-cols-5 items-center sm:grid-cols-6">
|
||||
<TableHead className="hidden items-center justify-start px-2 sm:flex">
|
||||
Date
|
||||
{t("Date")}
|
||||
</TableHead>
|
||||
<TableHead className="flex items-center px-2">
|
||||
{t("Type")}
|
||||
</TableHead>
|
||||
<TableHead className="flex items-center px-2">Type</TableHead>
|
||||
<TableHead className="col-span-3 flex items-center px-2">
|
||||
Link
|
||||
{t("Link")}
|
||||
</TableHead>
|
||||
<TableHead className="flex items-center px-2">
|
||||
{t("User")}
|
||||
</TableHead>
|
||||
<TableHead className="flex items-center px-2">User</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -4,7 +4,10 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Scraping API" text="" />
|
||||
<DashboardHeader
|
||||
heading="Url to Markdown"
|
||||
text="Quickly extract website content and convert it to Markdown format"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -21,10 +21,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Url to Markdown"
|
||||
text="Quickly extract website content and convert it to Markdown format."
|
||||
heading="Url to Markdown"
|
||||
text="Quickly extract website content and convert it to Markdown format"
|
||||
link="/docs/open-api/markdown"
|
||||
linkText="Markdown API."
|
||||
linkText="Markdown API"
|
||||
/>
|
||||
<ApiReference
|
||||
badge="GET /api/v1/scraping/markdown"
|
||||
|
||||
@@ -4,7 +4,10 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Scraping API" text="" />
|
||||
<DashboardHeader
|
||||
heading="Url to Meta Info"
|
||||
text="Quickly extract valuable structured website data"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { constructMetadata } from "@/lib/utils";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import ApiReference from "@/components/shared/api-reference";
|
||||
|
||||
import DashboardScrapeCharts from "../charts";
|
||||
import { MetaScraping } from "../scrapes";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
@@ -21,10 +20,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Url to Meta Info"
|
||||
text="Quickly extract valuable structured website data."
|
||||
heading="Url to Meta Info"
|
||||
text="Quickly extract valuable structured website data"
|
||||
link="/docs/open-api/meta-info"
|
||||
linkText="Meta Info API."
|
||||
linkText="Meta Info API"
|
||||
/>
|
||||
<ApiReference
|
||||
badge="GET /api/v1/scraping/meta"
|
||||
|
||||
@@ -20,27 +20,27 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Scraping API Overview"
|
||||
text="Quickly extract valuable structured website data. It's free and unlimited to use!"
|
||||
heading="Scraping API Overview"
|
||||
text="Quickly extract valuable structured website data"
|
||||
link="/docs/open-api"
|
||||
linkText="Open API."
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<StaticInfoCard
|
||||
title="Url to Screenshot"
|
||||
desc="Take a screenshot of the webpage."
|
||||
desc="Take a screenshot of the webpage"
|
||||
link="/dashboard/scrape/screenshot"
|
||||
icon="camera"
|
||||
/>
|
||||
<StaticInfoCard
|
||||
title="Url to Meta Info"
|
||||
desc="Extract website metadata."
|
||||
desc="Extract website metadata"
|
||||
link="/dashboard/scrape/meta-info"
|
||||
icon="globe"
|
||||
/>
|
||||
<StaticInfoCard
|
||||
title="Url to QR Code"
|
||||
desc="Generate QR Code from URL."
|
||||
desc="Generate QR Code from URL"
|
||||
link="/dashboard/scrape/qrcode"
|
||||
icon="qrcode"
|
||||
/>
|
||||
@@ -48,13 +48,13 @@ export default async function DashboardPage() {
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<StaticInfoCard
|
||||
title="Url to Markdown"
|
||||
desc="Convert website content to Markdown format."
|
||||
desc="Convert website content to Markdown format"
|
||||
link="/dashboard/scrape/markdown"
|
||||
icon="heading1"
|
||||
/>
|
||||
<StaticInfoCard
|
||||
title="Url to Text"
|
||||
desc="Extract website text."
|
||||
desc="Convert website content to text"
|
||||
link="/dashboard/scrape/markdown"
|
||||
icon="fileText"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,10 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Scraping API" text="" />
|
||||
<DashboardHeader
|
||||
heading="Url to QR Code"
|
||||
text="Generate QR Code from URL"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import ApiReference from "@/components/shared/api-reference";
|
||||
import QRCodeEditor from "@/components/shared/qr";
|
||||
|
||||
import { CodeLight, QrCodeScraping } from "../scrapes";
|
||||
import { CodeLight } from "../scrapes";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Url to QR Code API - WR.DO",
|
||||
@@ -21,10 +21,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Url to QR Code"
|
||||
heading="Url to QR Code"
|
||||
text="Generate QR Code from URL"
|
||||
link="/docs/open-api/qrcode"
|
||||
linkText="QR Code API."
|
||||
linkText="QR Code API"
|
||||
/>
|
||||
<ApiReference
|
||||
badge="GET /api/v1/scraping/qrcode"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -4,7 +4,10 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardRecordsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="Scraping API" text="" />
|
||||
<DashboardHeader
|
||||
heading="Url to Screenshot"
|
||||
text="Quickly extract website screenshots"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -22,10 +22,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Url to Screenshot"
|
||||
text="Quickly extract website screenshots."
|
||||
heading="Url to Screenshot"
|
||||
text="Quickly extract website screenshots"
|
||||
link="/docs/open-api/screenshot"
|
||||
linkText="Screenshot API."
|
||||
linkText="Screenshot API"
|
||||
/>
|
||||
<ApiReference
|
||||
badge="GET /api/v1/scraping/screenshot"
|
||||
|
||||
@@ -5,8 +5,8 @@ export default function DashboardSettingsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Settings"
|
||||
text="Manage account and website settings."
|
||||
heading="Account Settings"
|
||||
text="Manage account and website settings"
|
||||
/>
|
||||
<div className="divide-y divide-muted pb-10">
|
||||
<SkeletonSection />
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function SettingsPage() {
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Account Settings"
|
||||
text="Manage account and website settings."
|
||||
text="Manage account and website settings"
|
||||
/>
|
||||
<div className="divide-y divide-muted pb-10">
|
||||
<UserNameForm user={{ id: user.id, name: user.name || "" }} />
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Bar, BarChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import StatusDot from "@/components/dashboard/status-dot";
|
||||
@@ -29,6 +23,7 @@ export const RealtimeChart = ({
|
||||
chartData,
|
||||
totalClicks,
|
||||
}: RealtimeChartProps) => {
|
||||
const t = useTranslations("Components");
|
||||
const getTickInterval = (dataLength: number) => {
|
||||
if (dataLength <= 6) return 0;
|
||||
if (dataLength <= 12) return 1;
|
||||
@@ -45,7 +40,7 @@ export const RealtimeChart = ({
|
||||
<div className={cn(`rounded-lg border p-3 backdrop-blur-2xl`, className)}>
|
||||
<div className="mb-1 flex items-center text-base font-semibold">
|
||||
<StatusDot status={1} />
|
||||
<h3 className="ml-2">Realtime Visits</h3>
|
||||
<h3 className="ml-2">{t("Realtime Visits")}</h3>
|
||||
<Icons.mousePointerClick className="ml-auto size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mb-2 text-lg font-semibold">{totalClicks}</p>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
@@ -54,6 +55,8 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
const [limitDiplay, setLimitDisplay] = useState(100);
|
||||
const newLogsRef = useRef<Set<string>>(new Set()); // Track new log keys
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const {
|
||||
data: newLogs,
|
||||
error,
|
||||
@@ -151,10 +154,10 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<CardTitle className="text-base text-gray-800 dark:text-gray-100">
|
||||
Live Log
|
||||
{t("Live Logs")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time logs of short link visits.
|
||||
{t("Real-time logs of short link visits")}.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +169,8 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
isLive ? "border-dashed border-blue-600 text-blue-500" : ""
|
||||
}`}
|
||||
>
|
||||
<Icons.CirclePlay className="h-4 w-4" /> {isLive ? "Stop" : "Live"}
|
||||
<Icons.CirclePlay className="h-4 w-4" />{" "}
|
||||
{isLive ? t("Stop") : t("Live")}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-primary-foreground"
|
||||
@@ -207,22 +211,22 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
<TableHeader>
|
||||
<TableRow className="grid grid-cols-5 bg-gray-100/50 text-sm dark:bg-primary-foreground sm:grid-cols-9">
|
||||
<TableHead className="col-span-2 flex h-8 items-center">
|
||||
Time
|
||||
{t("Time")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex h-8 items-center">
|
||||
Slug
|
||||
{t("Slug")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-3 hidden h-8 items-center sm:flex">
|
||||
Target
|
||||
{t("Target")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden h-8 items-center sm:flex">
|
||||
IP
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex h-8 items-center">
|
||||
Location
|
||||
{t("Location")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex h-8 items-center">
|
||||
Clicks
|
||||
{t("Clicks")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -283,7 +287,7 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
)}
|
||||
{isLive && (
|
||||
<div className="flex w-full items-center justify-end gap-2 border-t border-dashed pt-4 text-sm text-gray-500">
|
||||
<p>{logs.length}</p> of
|
||||
<p>{logs.length}</p> {t("of")}
|
||||
<Select
|
||||
onValueChange={(value: string) => {
|
||||
setLimitDisplay(Number(value));
|
||||
@@ -302,7 +306,7 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p>total logs</p>
|
||||
<p>{t("total logs")}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -5,8 +5,8 @@ export default function DashboardUrlsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls."
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { UrlMeta, User } from "@prisma/client";
|
||||
import { VisSingleContainer, VisTooltip, VisTopoJSONMap } from "@unovis/react";
|
||||
import { TopoJSONMap } from "@unovis/ts";
|
||||
import { WorldMapTopoJSON } from "@unovis/ts/maps";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { TeamPlanQuota } from "@/config/team";
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
getRegionName,
|
||||
} from "@/lib/contries";
|
||||
import { DATE_DIMENSION_ENUMS } from "@/lib/enums";
|
||||
import { isLink, removeUrlSuffix, timeAgo } from "@/lib/utils";
|
||||
import { isLink, removeUrlSuffix } from "@/lib/utils";
|
||||
import { useElementSize } from "@/hooks/use-element-size";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
import { TimeAgoIntl } from "@/components/shared/time-ago";
|
||||
|
||||
const chartConfig = {
|
||||
pv: {
|
||||
@@ -175,6 +177,8 @@ export function DailyPVUVChart({
|
||||
const [activeChart, setActiveChart] =
|
||||
React.useState<keyof typeof chartConfig>("pv");
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const processedData = processUrlMeta(data).map((entry) => ({
|
||||
date: entry.date,
|
||||
pv: entry.clicks,
|
||||
@@ -184,7 +188,6 @@ export function DailyPVUVChart({
|
||||
const dataTotal = calculateUVAndPV(data);
|
||||
|
||||
const latestEntry = data[data.length - 1];
|
||||
const latestDate = timeAgo(latestEntry.updatedAt);
|
||||
const latestFrom = [
|
||||
latestEntry.city ? decodeURIComponent(latestEntry.city) : "",
|
||||
latestEntry.country ? `(${getCountryName(latestEntry.country)})` : "",
|
||||
@@ -238,14 +241,17 @@ export function DailyPVUVChart({
|
||||
const regionStats = generateStatsList(data, "region");
|
||||
const isBotStats = generateStatsList(data, "isBot");
|
||||
|
||||
const lastVisitorInfo = t.rich("last-visitor-info", {
|
||||
location: latestFrom,
|
||||
timeAgo: () => <TimeAgoIntl date={latestEntry.updatedAt} />,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-2 sm:py-3">
|
||||
<CardTitle>Link Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
Last visitor from {latestFrom} about {latestDate}.
|
||||
</CardDescription>
|
||||
<CardTitle>{t("Link Analytics")}</CardTitle>
|
||||
<CardDescription>{lastVisitorInfo}</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
@@ -291,8 +297,8 @@ export function DailyPVUVChart({
|
||||
className="relative z-30 flex flex-1 flex-col items-center justify-center gap-1 border-t px-6 py-2 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-3"
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-sm font-semibold text-muted-foreground">
|
||||
{chartConfig[chart].label}
|
||||
<span className="text-nowrap text-sm font-semibold text-muted-foreground">
|
||||
{t(chartConfig[chart].label)}
|
||||
</span>
|
||||
<span className="text-lg font-bold leading-none">
|
||||
{dataTotal[key as keyof typeof dataTotal].toLocaleString()}
|
||||
@@ -403,8 +409,8 @@ export function DailyPVUVChart({
|
||||
{/* Referrers、isBotStats */}
|
||||
<Tabs defaultValue="referrer">
|
||||
<TabsList>
|
||||
<TabsTrigger value="referrer">Referrers</TabsTrigger>
|
||||
<TabsTrigger value="isBot">Traffic Type</TabsTrigger>
|
||||
<TabsTrigger value="referrer">{t("Referrers")}</TabsTrigger>
|
||||
<TabsTrigger value="isBot">{t("Traffic Type")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="referrer">
|
||||
{refererStats.length > 0 && (
|
||||
@@ -420,8 +426,8 @@ export function DailyPVUVChart({
|
||||
{/* 国家、城市 */}
|
||||
<Tabs defaultValue="country">
|
||||
<TabsList>
|
||||
<TabsTrigger value="country">Country</TabsTrigger>
|
||||
<TabsTrigger value="city">City</TabsTrigger>
|
||||
<TabsTrigger value="country">{t("Country")}</TabsTrigger>
|
||||
<TabsTrigger value="city">{t("City")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="country">
|
||||
{countryStats.length > 0 && (
|
||||
@@ -437,8 +443,8 @@ export function DailyPVUVChart({
|
||||
{/* browserStats、engineStats */}
|
||||
<Tabs defaultValue="browser">
|
||||
<TabsList>
|
||||
<TabsTrigger value="browser">Browser</TabsTrigger>
|
||||
<TabsTrigger value="engine">Browser Engine</TabsTrigger>
|
||||
<TabsTrigger value="browser">{t("Browser")}</TabsTrigger>
|
||||
<TabsTrigger value="engine">{t("Engine")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="browser">
|
||||
{browserStats.length > 0 && (
|
||||
@@ -455,8 +461,8 @@ export function DailyPVUVChart({
|
||||
{/* Languages、regionStats */}
|
||||
<Tabs className="h-full" defaultValue="language">
|
||||
<TabsList>
|
||||
<TabsTrigger value="language">Language</TabsTrigger>
|
||||
<TabsTrigger value="region">Region</TabsTrigger>
|
||||
<TabsTrigger value="language">{t("Language")}</TabsTrigger>
|
||||
<TabsTrigger value="region">{t("Region")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="language">
|
||||
{languageStats.length > 0 && (
|
||||
@@ -472,8 +478,8 @@ export function DailyPVUVChart({
|
||||
{/* deviceStats、osStats、cpuStats */}
|
||||
<Tabs defaultValue="device">
|
||||
<TabsList>
|
||||
<TabsTrigger value="device">Device</TabsTrigger>
|
||||
<TabsTrigger value="os">OS</TabsTrigger>
|
||||
<TabsTrigger value="device">{t("Device")}</TabsTrigger>
|
||||
<TabsTrigger value="os">{t("OS")}</TabsTrigger>
|
||||
<TabsTrigger value="cpu">CPU</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="device">
|
||||
@@ -497,12 +503,12 @@ export function DailyPVUVChart({
|
||||
export function StatsList({ data, title }: { data: Stat[]; title: string }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const displayedData = showAll ? data.slice(0, 50) : data.slice(0, 8);
|
||||
|
||||
const t = useTranslations("Components");
|
||||
return (
|
||||
<div className="h-full rounded-lg border">
|
||||
<div className="flex items-center justify-between border-b px-5 py-2 text-xs font-medium text-muted-foreground">
|
||||
<span>名称</span>
|
||||
<span className="">点击量</span>
|
||||
<span>{t("Name")}</span>
|
||||
<span className="">{t("Visitors")}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`scrollbar-hidden overflow-hidden overflow-y-auto px-4 pb-4 pt-2 transition-all duration-500 ease-in-out`}
|
||||
|
||||
@@ -19,10 +19,10 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls."
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls"
|
||||
link="/docs/short-urls"
|
||||
linkText="short urls."
|
||||
linkText="short urls"
|
||||
/>
|
||||
<UserUrlsList
|
||||
user={{
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { User } from "@prisma/client";
|
||||
import { PenLine, RefreshCwIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
fetcher,
|
||||
nFormatter,
|
||||
removeUrlSuffix,
|
||||
timeAgo,
|
||||
} from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -57,6 +57,7 @@ import { Icons } from "@/components/shared/icons";
|
||||
import { LinkInfoPreviewer } from "@/components/shared/link-previewer";
|
||||
import { PaginationWrapper } from "@/components/shared/pagination";
|
||||
import QRCodeEditor from "@/components/shared/qr";
|
||||
import { TimeAgoIntl } from "@/components/shared/time-ago";
|
||||
|
||||
import Globe from "./globe";
|
||||
import LiveLog from "./live-logs";
|
||||
@@ -101,6 +102,7 @@ function TableColumnSekleton() {
|
||||
export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
const pathname = usePathname();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const t = useTranslations("List");
|
||||
const [currentView, setCurrentView] = useState<string>("List");
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
@@ -185,7 +187,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
const rendeEmpty = () => (
|
||||
<EmptyPlaceholder className="col-span-full shadow-none">
|
||||
<EmptyPlaceholder.Icon name="link" />
|
||||
<EmptyPlaceholder.Title>No urls</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Title>{t("No urls")}</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Description>
|
||||
You don't have any url yet. Start creating url.
|
||||
</EmptyPlaceholder.Description>
|
||||
@@ -197,7 +199,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="h-8 text-xs md:text-xs"
|
||||
placeholder="Search by slug..."
|
||||
placeholder={t("Search by slug") + "..."}
|
||||
value={searchParams.slug}
|
||||
onChange={(e) => {
|
||||
setSearchParams({
|
||||
@@ -220,7 +222,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="h-8 text-xs md:text-xs"
|
||||
placeholder="Search by target..."
|
||||
placeholder={t("Search by target") + "..."}
|
||||
value={searchParams.target}
|
||||
onChange={(e) => {
|
||||
setSearchParams({
|
||||
@@ -244,7 +246,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="h-8 text-xs md:text-xs"
|
||||
placeholder="Search by user name..."
|
||||
placeholder={t("Search by username") + "..."}
|
||||
value={searchParams.userName}
|
||||
onChange={(e) => {
|
||||
setSearchParams({
|
||||
@@ -299,28 +301,28 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
<TableHeader className="bg-gray-100/50 dark:bg-primary-foreground">
|
||||
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-11">
|
||||
<TableHead className="col-span-1 flex items-center font-bold sm:col-span-2">
|
||||
Slug
|
||||
{t("Slug")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold sm:col-span-2">
|
||||
Target
|
||||
{t("Target")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
User
|
||||
{t("User")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
Enabled
|
||||
{t("Enabled")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
Expiration
|
||||
{t("Expiration")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
Clicks
|
||||
{t("Clicks")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 hidden items-center font-bold sm:flex">
|
||||
Updated
|
||||
{t("Updated")}
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold sm:col-span-2">
|
||||
Actions
|
||||
{t("Actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -394,7 +396,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 hidden truncate sm:flex">
|
||||
{timeAgo(short.updatedAt as Date)}
|
||||
<TimeAgoIntl date={short.updatedAt as Date} />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex items-center gap-1 sm:col-span-2">
|
||||
<Button
|
||||
@@ -408,7 +410,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
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>
|
||||
<Button
|
||||
@@ -561,7 +563,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
}}
|
||||
>
|
||||
<Icons.lineChart className="size-4" />
|
||||
Analytics
|
||||
{t("Analytics")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -578,7 +580,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
}}
|
||||
>
|
||||
<PenLine className="size-4" />
|
||||
Edit URL
|
||||
{t("Edit URL")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -612,7 +614,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
></Separator>
|
||||
</>
|
||||
)}
|
||||
{timeAgo(short.updatedAt as Date)}
|
||||
<TimeAgoIntl date={short.updatedAt as Date} />
|
||||
<Switch
|
||||
className="scale-[0.6]"
|
||||
defaultChecked={short.active === 1}
|
||||
@@ -662,7 +664,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
{/* Tabs */}
|
||||
<div className="mb-4 flex items-center justify-between gap-2">
|
||||
{pathname === "/dashboard" && (
|
||||
<h2 className="mr-3 text-lg font-semibold">Short URLs</h2>
|
||||
<h2 className="mr-3 text-lg font-semibold">{t("Short URLs")}</h2>
|
||||
)}
|
||||
<TabsList>
|
||||
<TabsTrigger onClick={() => setCurrentView("List")} value="List">
|
||||
@@ -715,7 +717,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
}}
|
||||
>
|
||||
<Icons.add className="size-4" />
|
||||
<span className="hidden sm:inline">Add URL</span>
|
||||
<span className="hidden sm:inline">{t("Add URL")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,10 +4,7 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
export default function DashboardLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls."
|
||||
/>
|
||||
<DashboardHeader heading="Setup Guide" text="" />
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { getEmailsByEmailAddress } from "@/lib/dto/email";
|
||||
import { deleteEmailsByIds, getEmailsByEmailAddress } from "@/lib/dto/email";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
@@ -35,3 +35,22 @@ export async function GET(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
|
||||
const { ids } = await req.json();
|
||||
if (!ids) {
|
||||
return Response.json("ids is required", { status: 400 });
|
||||
}
|
||||
|
||||
await deleteEmailsByIds(ids);
|
||||
|
||||
return Response.json("success", { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("[Error]", error);
|
||||
return Response.json(error.message || "Server error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function POST(req: Request) {
|
||||
const { record: reviewRecord, userId, id } = await req.json();
|
||||
const record = {
|
||||
...reviewRecord,
|
||||
comment: "Created by wr.do (review mode)",
|
||||
// comment: "Created by wr.do (review mode)",
|
||||
id,
|
||||
};
|
||||
|
||||
|
||||
+20
-14
@@ -2,6 +2,8 @@ import "@/styles/globals.css";
|
||||
|
||||
import { fontHeading, fontSans, fontSatoshi } from "@/assets/fonts";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ViewTransitions } from "next-view-transitions";
|
||||
|
||||
@@ -18,10 +20,12 @@ interface RootLayoutProps {
|
||||
|
||||
export const metadata = constructMetadata();
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
return (
|
||||
<ViewTransitions>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
defer
|
||||
@@ -37,18 +41,20 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
fontSatoshi.variable,
|
||||
)}
|
||||
>
|
||||
<SessionProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ModalProvider>{children}</ModalProvider>
|
||||
<Toaster richColors closeButton />
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<SessionProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ModalProvider>{children}</ModalProvider>
|
||||
<Toaster richColors closeButton />
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
</NextIntlClientProvider>
|
||||
<GoogleAnalytics />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import useSWR from "swr";
|
||||
|
||||
@@ -68,6 +69,8 @@ export function InteractiveBarChart() {
|
||||
const [activeChart, setActiveChart] =
|
||||
React.useState<keyof typeof chartConfig>("users");
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const { data, isLoading } = useSWR<{
|
||||
list: [
|
||||
{
|
||||
@@ -110,8 +113,8 @@ export function InteractiveBarChart() {
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex w-full flex-1 justify-between gap-2 px-6 py-5 sm:flex-col sm:py-6">
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<CardTitle>Data Increase</CardTitle>
|
||||
<CardDescription>Showing data increase in:</CardDescription>
|
||||
<CardTitle>{t("Data Increase")}</CardTitle>
|
||||
<CardDescription>{t("Showing data increase in")}:</CardDescription>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={(value: string) => {
|
||||
@@ -150,7 +153,7 @@ export function InteractiveBarChart() {
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chartConfig[chart].label}
|
||||
{t(chartConfig[chart].label)}
|
||||
</span>
|
||||
<span className="text-base font-bold leading-none sm:text-lg">
|
||||
{nFormatter(data.total[key])}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { Link as LinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { nFormatter } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -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>
|
||||
);
|
||||
@@ -72,6 +73,7 @@ export async function DashboardInfoCard({
|
||||
link: string;
|
||||
icon?: keyof typeof Icons;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
const Icon = Icons[icon || "arrowRight"];
|
||||
return (
|
||||
<Card className="grids group animate-fade-in bg-gray-50/70 backdrop-blur-lg dark:bg-primary-foreground">
|
||||
@@ -81,7 +83,7 @@ export async function DashboardInfoCard({
|
||||
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" />
|
||||
@@ -94,12 +96,15 @@ export async function DashboardInfoCard({
|
||||
<CountUp count={monthTotal} />
|
||||
{total !== undefined && (
|
||||
<p className="align-top text-base text-slate-500">
|
||||
/ {nFormatter(limit)} <span className="text-xs">(monthly)</span>
|
||||
/ {nFormatter(limit)}{" "}
|
||||
<span className="text-xs">({t("monthly")})</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">total: {total}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("total")}: {total}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -114,6 +119,7 @@ export function HeroCard({
|
||||
monthTotal: number;
|
||||
limit: number;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
return (
|
||||
<div className="grids group relative mb-4 h-full w-full shrink-0 origin-left overflow-hidden rounded-lg border bg-gray-50/70 px-5 pt-5 text-left duration-500 before:absolute before:right-1 before:top-1 before:z-[2] before:h-12 before:w-12 before:rounded-full before:bg-violet-500 before:blur-lg before:duration-500 after:absolute after:right-8 after:top-3 after:z-[2] after:h-20 after:w-20 after:rounded-full after:bg-rose-300 after:blur-lg after:duration-500 hover:border-cyan-600 hover:decoration-2 hover:duration-500 hover:before:-bottom-8 hover:before:right-12 hover:before:blur hover:before:[box-shadow:_20px_20px_20px_30px_#a21caf] hover:after:-right-8 group-hover:before:duration-500 group-hover:after:duration-500 dark:bg-primary-foreground md:max-w-[350px]">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -121,7 +127,7 @@ export function HeroCard({
|
||||
href="/emails"
|
||||
className="text-lg font-bold duration-500 group-hover:text-blue-500 group-hover:underline"
|
||||
>
|
||||
Email box
|
||||
{t("Email box")}
|
||||
</Link>
|
||||
<Icons.mail className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
@@ -134,12 +140,15 @@ export function HeroCard({
|
||||
<CountUp count={monthTotal} />
|
||||
{total !== undefined && (
|
||||
<p className="align-top text-base text-slate-500">
|
||||
/ {nFormatter(limit)} <span className="text-xs">(monthly)</span>
|
||||
/ {nFormatter(limit)}{" "}
|
||||
<span className="text-xs">({t("monthly")})</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">total: {total}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("total")}: {total}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -157,6 +166,7 @@ export async function StaticInfoCard({
|
||||
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">
|
||||
@@ -165,13 +175,13 @@ export async function StaticInfoCard({
|
||||
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" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">{desc}</p>
|
||||
{desc && <p className="text-xs text-muted-foreground">{t(desc)}</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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
heading: string;
|
||||
@@ -15,17 +16,18 @@ export function DashboardHeader({
|
||||
linkText,
|
||||
children,
|
||||
}: DashboardHeaderProps) {
|
||||
const t = useTranslations("Components");
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="grid gap-1">
|
||||
<h1 className="font-heading text-2xl font-semibold">{heading}</h1>
|
||||
<h1 className="font-heading text-2xl font-semibold">{t(heading)}</h1>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{text && <span>{text}</span>}
|
||||
{text && <span>{t(text)}.</span>}
|
||||
{link && (
|
||||
<span>
|
||||
{" "}
|
||||
See documentation about{" "}
|
||||
{t("See documentation")}:{" "}
|
||||
<Link
|
||||
href={link}
|
||||
target="_blank"
|
||||
@@ -33,6 +35,7 @@ export function DashboardHeader({
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useTransition } 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";
|
||||
|
||||
import { cn, fetcher, htmlToText, timeAgo } from "@/lib/utils";
|
||||
import { cn, fetcher, htmlToText } from "@/lib/utils";
|
||||
|
||||
import BlurImage from "../shared/blur-image";
|
||||
import { Icons } from "../shared/icons";
|
||||
import { PaginationWrapper } from "../shared/pagination";
|
||||
import { TimeAgoIntl } from "../shared/time-ago";
|
||||
// import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
@@ -48,6 +50,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);
|
||||
@@ -55,6 +58,8 @@ export default function EmailList({
|
||||
const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
|
||||
const [showMutiCheckBox, setShowMutiCheckBox] = useState(false);
|
||||
|
||||
const [isDeleting, startDeleteTransition] = useTransition();
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<{
|
||||
total: number;
|
||||
list: ForwardEmail[];
|
||||
@@ -81,15 +86,11 @@ export default function EmailList({
|
||||
}, [emailAddress, data, selectedEmailId]);
|
||||
|
||||
const handleMarkAsRead = async (emailId: string) => {
|
||||
try {
|
||||
await fetch("/api/email/read", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ emailId }),
|
||||
}).then(() => mutate());
|
||||
} catch (error) {
|
||||
console.log("Error marking email as read");
|
||||
}
|
||||
await fetch("/api/email/read", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ emailId }),
|
||||
}).then(() => mutate());
|
||||
};
|
||||
|
||||
const handleMarkSelectedAsRead = async () => {
|
||||
@@ -125,6 +126,10 @@ export default function EmailList({
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAllEmails = () => {
|
||||
setSelectedEmails(data?.list.map((email) => email.id) || []);
|
||||
};
|
||||
|
||||
const handleSetAutoRefresh = (value: boolean) => {
|
||||
setIsAutoRefresh(value);
|
||||
};
|
||||
@@ -147,15 +152,26 @@ export default function EmailList({
|
||||
onSelectEmail(emailId);
|
||||
};
|
||||
|
||||
const handleDeletEmails = async (ids: string[]) => {
|
||||
startDeleteTransition(async () => {
|
||||
await fetch("/api/email/inbox", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ ids }),
|
||||
}).then((v) => {
|
||||
v.status === 200 && mutate();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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 +184,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
|
||||
@@ -196,7 +212,7 @@ export default function EmailList({
|
||||
>
|
||||
<Icons.listChecks className="size-4" />
|
||||
</Button>
|
||||
{selectedEmails.length > 0 && (
|
||||
{showMutiCheckBox && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
@@ -204,7 +220,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>
|
||||
@@ -213,16 +229,41 @@ export default function EmailList({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleMarkSelectedAsRead}
|
||||
onClick={handleSelectAllEmails}
|
||||
className="w-full"
|
||||
>
|
||||
<span className="text-xs">Mask as read</span>
|
||||
<span className="text-xs">{t("Select all")}</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild disabled>
|
||||
<Button variant="ghost" size="sm" className="w-full">
|
||||
<span className="text-xs">Delete selected</span>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
disabled={selectedEmails.length === 0}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleMarkSelectedAsRead}
|
||||
className="w-full"
|
||||
>
|
||||
<span className="text-xs">{t("Mask as read")}</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
disabled={isDeleting || selectedEmails.length === 0}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => handleDeletEmails(selectedEmails)}
|
||||
>
|
||||
{isDeleting && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
<span className="text-xs">{t("Delete selected")}</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -261,9 +302,9 @@ export default function EmailList({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={selectedEmails.includes(email.id)}
|
||||
checked={selectedEmails.includes(email.id)}
|
||||
onCheckedChange={() => handleSelectEmail(email.id)}
|
||||
className="mr-2 size-4 border-neutral-300 bg-neutral-100 data-[state=checked]:border-neutral-900 data-[state=checked]:bg-neutral-600 data-[state=checked]:text-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 dark:data-[state=checked]:border-neutral-300 dark:data-[state=checked]:bg-neutral-300"
|
||||
className="mr-3 size-4 border-neutral-300 bg-neutral-100 data-[state=checked]:border-neutral-900 data-[state=checked]:bg-neutral-600 data-[state=checked]:text-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 dark:data-[state=checked]:border-neutral-300 dark:data-[state=checked]:bg-neutral-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -276,7 +317,9 @@ export default function EmailList({
|
||||
{email.fromName || email.subject || "Untitled"}
|
||||
</span>
|
||||
<span className="ml-auto text-xs text-neutral-600 dark:text-neutral-400">
|
||||
{timeAgo((email.date as any) || email.createdAt)}
|
||||
<TimeAgoIntl
|
||||
date={(email.date as any) || email.createdAt}
|
||||
/>
|
||||
</span>
|
||||
{email.readAt && (
|
||||
<Icons.checkCheck className="ml-2 size-3 text-green-600" />
|
||||
@@ -298,7 +341,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 +364,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 +374,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 +389,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 +399,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 +409,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 +419,7 @@ export function EmptyInboxSection() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to create emails with api?
|
||||
{t("How to create emails with api?")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||
import { User, UserEmail } from "@prisma/client";
|
||||
import randomName from "@scaleway/random-name";
|
||||
import {
|
||||
@@ -11,18 +11,20 @@ import {
|
||||
Sparkles,
|
||||
SquarePlus,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { UserEmailList } from "@/lib/dto/email";
|
||||
import { reservedAddressSuffix } from "@/lib/enums";
|
||||
import { cn, fetcher, nFormatter, timeAgo } from "@/lib/utils";
|
||||
import { cn, fetcher, nFormatter } from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
|
||||
import { CopyButton } from "../shared/copy-button";
|
||||
import { EmptyPlaceholder } from "../shared/empty-placeholder";
|
||||
import { Icons } from "../shared/icons";
|
||||
import { PaginationWrapper } from "../shared/pagination";
|
||||
import { TimeAgoIntl } from "../shared/time-ago";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
@@ -67,13 +69,14 @@ export default function EmailSidebar({
|
||||
setAdminModel,
|
||||
}: EmailSidebarProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const t = useTranslations("Email");
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [showEmailModal, setShowEmailModal] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [domainSuffix, setDomainSuffix] = useState<string | null>("wr.do");
|
||||
const [domainSuffix, setDomainSuffix] = useState<string | null>(null);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [emailToDelete, setEmailToDelete] = useState<string | null>(null);
|
||||
const [deleteInput, setDeleteInput] = useState("");
|
||||
@@ -110,6 +113,12 @@ export default function EmailSidebar({
|
||||
dedupingInterval: 10000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!domainSuffix && emailDomains && emailDomains.length > 0) {
|
||||
setDomainSuffix(emailDomains[0].domain_name);
|
||||
}
|
||||
}, [domainSuffix, emailDomains]);
|
||||
|
||||
if (!selectedEmailAddress && data && data.list.length > 0) {
|
||||
onSelectEmail(data.list[0].emailAddress);
|
||||
}
|
||||
@@ -261,7 +270,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 +305,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 +317,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 +330,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 +353,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 +364,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 +383,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 +395,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 +431,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,13 +540,13 @@ 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
|
||||
? `Created by ${email.user || email.email.slice(0, 5)} at`
|
||||
: ""}{" "}
|
||||
{timeAgo(email.createdAt)}
|
||||
<TimeAgoIntl date={email.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -568,7 +581,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 +595,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 +635,7 @@ export default function EmailSidebar({
|
||||
))
|
||||
) : (
|
||||
<Button className="w-full" variant="ghost">
|
||||
No domains configured
|
||||
{t("No domains configured")}
|
||||
</Button>
|
||||
)}
|
||||
</SelectContent>
|
||||
@@ -652,10 +665,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 +680,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 +710,7 @@ export default function EmailSidebar({
|
||||
setEmailToDelete(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -710,7 +724,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>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction, useState, useTransition } from "react";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useMemo,
|
||||
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";
|
||||
import useSWR from "swr";
|
||||
@@ -61,9 +68,11 @@ export function RecordForm({
|
||||
const [currentZoneName, setCurrentZoneName] = useState(
|
||||
initData?.zone_name || "wr.do",
|
||||
);
|
||||
const [email, setEmail] = useState(user.email);
|
||||
const [email, setEmail] = useState(initData?.user.email || user.email);
|
||||
const isAdmin = action.indexOf("admin") > -1;
|
||||
|
||||
const t = useTranslations("List");
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
@@ -92,6 +101,19 @@ export function RecordForm({
|
||||
},
|
||||
);
|
||||
|
||||
const validDefaultDomain = useMemo(() => {
|
||||
if (!recordDomains?.length) return undefined;
|
||||
|
||||
if (
|
||||
initData?.zone_name &&
|
||||
recordDomains.some((d) => d.domain_name === initData.zone_name)
|
||||
) {
|
||||
return initData.zone_name;
|
||||
}
|
||||
|
||||
return recordDomains[0].domain_name;
|
||||
}, [recordDomains, initData?.zone_name]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (isAdmin && type === "edit" && initData?.active === 2) {
|
||||
handleApplyRecord(data);
|
||||
@@ -204,24 +226,26 @@ export function RecordForm({
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
|
||||
{type === "add" ? "Create" : "Edit"} record
|
||||
{type === "add" ? t("Create record") : t("Edit record")}
|
||||
</div>
|
||||
{siteConfig.enableSubdomainApply && (
|
||||
<ul className="m-2 list-disc gap-1 rounded-md bg-yellow-600/10 p-2 px-5 pr-2 text-xs font-medium text-yellow-600 dark:bg-yellow-500/10 dark:text-yellow-500">
|
||||
<li>The administrator has enabled application mode.</li>
|
||||
<li>{t("The administrator has enabled application mode")}.</li>
|
||||
<li>
|
||||
After submission, you need to wait for administrator approval before
|
||||
the record takes effect.
|
||||
{t(
|
||||
"After submission, you need to wait for administrator approval before the record takes effect",
|
||||
)}
|
||||
.
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
<form className="p-4" onSubmit={onSubmit}>
|
||||
{isAdmin && (
|
||||
<div className="items-center justify-start gap-4 md:flex">
|
||||
<FormSectionColumns required title="User email">
|
||||
<FormSectionColumns required title={t("User email")}>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="content">
|
||||
User email
|
||||
{t("User email")}
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
@@ -229,6 +253,7 @@ export function RecordForm({
|
||||
size={32}
|
||||
defaultValue={email || ""}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={type === "edit"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between p-1">
|
||||
@@ -248,12 +273,12 @@ export function RecordForm({
|
||||
|
||||
{siteConfig.enableSubdomainApply && (
|
||||
<FormSectionColumns
|
||||
title="What are you planning to use the subdomain for?"
|
||||
title={t("What are you planning to use the subdomain for?")}
|
||||
required
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="comment">
|
||||
What are you planning to use the subdomain for?
|
||||
{t("What are you planning to use the subdomain for?")}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="comment"
|
||||
@@ -263,12 +288,12 @@ export function RecordForm({
|
||||
/>
|
||||
</div>
|
||||
<p className="p-1 text-[13px] text-muted-foreground">
|
||||
At least 20 characters
|
||||
{t("At least 20 characters")}
|
||||
</p>
|
||||
</FormSectionColumns>
|
||||
)}
|
||||
<div className="items-center justify-start gap-4 md:flex">
|
||||
<FormSectionColumns title="Domain" required>
|
||||
<FormSectionColumns title={t("Domain")} required>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-9 w-full" />
|
||||
) : (
|
||||
@@ -278,7 +303,7 @@ export function RecordForm({
|
||||
setCurrentZoneName(value);
|
||||
}}
|
||||
name="zone_name"
|
||||
defaultValue={String(initData?.zone_name || "wr.do")}
|
||||
defaultValue={validDefaultDomain}
|
||||
disabled={type === "edit"}
|
||||
>
|
||||
<SelectTrigger className="w-full shadow-inner">
|
||||
@@ -293,17 +318,17 @@ export function RecordForm({
|
||||
))
|
||||
) : (
|
||||
<Button className="w-full" variant="ghost">
|
||||
No domains configured
|
||||
{t("No domains configured")}
|
||||
</Button>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<p className="p-1 text-[13px] text-muted-foreground">
|
||||
Required. Select a domain.
|
||||
{t("Required")}. {t("Select a domain")}.
|
||||
</p>
|
||||
</FormSectionColumns>
|
||||
<FormSectionColumns title="Type" required>
|
||||
<FormSectionColumns title={t("Type")} required>
|
||||
<Select
|
||||
onValueChange={(value: RecordType) => {
|
||||
setValue("type", value);
|
||||
@@ -323,14 +348,16 @@ export function RecordForm({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="p-1 text-[13px] text-muted-foreground">Required.</p>
|
||||
<p className="p-1 text-[13px] text-muted-foreground">
|
||||
{t("Required")}.
|
||||
</p>
|
||||
</FormSectionColumns>
|
||||
</div>
|
||||
<div className="items-center justify-start gap-4 md:flex">
|
||||
<FormSectionColumns title="Name" required>
|
||||
<FormSectionColumns title={t("Name")} required>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="name">
|
||||
Name (required)
|
||||
{t("Name")}
|
||||
</Label>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
@@ -354,7 +381,7 @@ export function RecordForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Required. E.g. www.
|
||||
{t("Required")}. {t("Example")} www
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -363,15 +390,15 @@ export function RecordForm({
|
||||
required
|
||||
title={
|
||||
currentRecordType === "CNAME"
|
||||
? "Content"
|
||||
? t("Content")
|
||||
: currentRecordType === "A"
|
||||
? "IPv4 address"
|
||||
: "Content"
|
||||
? t("IPv4 address")
|
||||
: t("Content")
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="content">
|
||||
Content
|
||||
t("Content")
|
||||
</Label>
|
||||
<Input
|
||||
id="content"
|
||||
@@ -388,10 +415,10 @@ export function RecordForm({
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
{currentRecordType === "CNAME"
|
||||
? "Required. E.g. www.example.com"
|
||||
? `${t("Required")}. ${t("Example")} www.example.com`
|
||||
: currentRecordType === "A"
|
||||
? "Required. E.g. 8.8.8.8"
|
||||
: "Required."}
|
||||
? `${t("Required")}. ${t("Example")} 8.8.8.8`
|
||||
: t("Required")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -418,14 +445,14 @@ export function RecordForm({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="p-1 text-[13px] text-muted-foreground">
|
||||
Optional. Time To Live.
|
||||
{t("Optional")}. {t("Time To Live")}.
|
||||
</p>
|
||||
</FormSectionColumns>
|
||||
{["A", "CNAME"].includes(currentRecordType) && (
|
||||
<FormSectionColumns title="Proxy">
|
||||
<FormSectionColumns title={t("Proxy")}>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="proxy">
|
||||
Proxy
|
||||
{t("Proxy")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="proxied"
|
||||
@@ -434,7 +461,7 @@ export function RecordForm({
|
||||
/>
|
||||
</div>
|
||||
<p className="p-1 text-[13px] text-muted-foreground">
|
||||
Proxy status.
|
||||
{t("Proxy status")}.
|
||||
</p>
|
||||
</FormSectionColumns>
|
||||
)}
|
||||
@@ -452,7 +479,7 @@ export function RecordForm({
|
||||
{isDeleting ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>Delete</p>
|
||||
<p>{t("Delete")}</p>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -462,7 +489,7 @@ export function RecordForm({
|
||||
className="w-[80px] px-0"
|
||||
onClick={() => setShowForm(false)}
|
||||
>
|
||||
Cancle
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -473,7 +500,7 @@ export function RecordForm({
|
||||
{isPending ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>{type === "edit" ? "Update" : "Save"}</p>
|
||||
<p>{type === "edit" ? t("Update") : t("Save")}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction, useTransition } from "react";
|
||||
import { Dispatch, SetStateAction, useMemo, useTransition } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { User } from "@prisma/client";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@@ -51,6 +52,7 @@ export function UrlForm({
|
||||
}: RecordFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isDeleting, startDeleteTransition] = useTransition();
|
||||
const t = useTranslations("List");
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -81,6 +83,19 @@ export function UrlForm({
|
||||
},
|
||||
);
|
||||
|
||||
const validDefaultDomain = useMemo(() => {
|
||||
if (!shortDomains?.length) return undefined;
|
||||
|
||||
if (
|
||||
initData?.prefix &&
|
||||
shortDomains.some((d) => d.domain_name === initData.prefix)
|
||||
) {
|
||||
return initData.prefix;
|
||||
}
|
||||
|
||||
return shortDomains[0].domain_name;
|
||||
}, [shortDomains, initData?.prefix]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (type === "add") {
|
||||
handleCreateUrl(data);
|
||||
@@ -166,14 +181,14 @@ export function UrlForm({
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
|
||||
{type === "add" ? "Create" : "Edit"} short link
|
||||
{type === "add" ? t("Create short link") : t("Edit short link")}
|
||||
</div>
|
||||
<form className="p-4" onSubmit={onSubmit}>
|
||||
<div className="items-center justify-start gap-4 md:flex">
|
||||
<FormSectionColumns title="Target URL" required>
|
||||
<FormSectionColumns title={t("Target URL")} required>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="target">
|
||||
Target
|
||||
{t("Target")}
|
||||
</Label>
|
||||
<Input
|
||||
id="target"
|
||||
@@ -189,12 +204,12 @@ export function UrlForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Required. https://your-origin-url
|
||||
{t("Required")}. https://your-origin-url
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
<FormSectionColumns title="Short Link" required>
|
||||
<FormSectionColumns title={t("Short Link")} required>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="url">
|
||||
Url
|
||||
@@ -209,7 +224,7 @@ export function UrlForm({
|
||||
setValue("prefix", value);
|
||||
}}
|
||||
name="prefix"
|
||||
defaultValue={initData?.prefix || "wr.do"}
|
||||
defaultValue={validDefaultDomain}
|
||||
disabled={type === "edit"}
|
||||
>
|
||||
<SelectTrigger className="w-1/3 rounded-r-none border-r-0 shadow-inner">
|
||||
@@ -258,7 +273,8 @@ export function UrlForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
A random url suffix. Final url like「wr.do/s/suffix」
|
||||
{t("A random url suffix")}. {t("Final url like")}
|
||||
「wr.do/s/suffix」
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -266,10 +282,10 @@ export function UrlForm({
|
||||
</div>
|
||||
|
||||
<div className="items-center justify-start gap-4 md:flex">
|
||||
<FormSectionColumns title="Password (Optional)">
|
||||
<FormSectionColumns title={`${t("Password")} (${t("Optional")})`}>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="password">
|
||||
Password
|
||||
{t("Password")}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
@@ -277,7 +293,7 @@ export function UrlForm({
|
||||
size={32}
|
||||
maxLength={6}
|
||||
type="password"
|
||||
placeholder="Enter 6 character password"
|
||||
placeholder={t("Enter 6 character password")}
|
||||
{...register("password")}
|
||||
/>
|
||||
</div>
|
||||
@@ -288,12 +304,12 @@ export function UrlForm({
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
Optional. If you want to protect your link.
|
||||
{t("Optional")}. {t("If you want to protect your link")}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
<FormSectionColumns title="Expiration" required>
|
||||
<FormSectionColumns title={t("Expiration")} required>
|
||||
<Select
|
||||
onValueChange={(value: string) => {
|
||||
setValue("expiration", value);
|
||||
@@ -313,7 +329,7 @@ export function UrlForm({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="p-1 text-[13px] text-muted-foreground">
|
||||
Expiration time, default for never.
|
||||
{t("Expiration time, default for never")}.
|
||||
</p>
|
||||
</FormSectionColumns>
|
||||
</div>
|
||||
@@ -331,7 +347,7 @@ export function UrlForm({
|
||||
{isDeleting ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>Delete</p>
|
||||
<p>{t("Delete")}</p>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -341,7 +357,7 @@ export function UrlForm({
|
||||
className="w-[80px] px-0"
|
||||
onClick={() => setShowForm(false)}
|
||||
>
|
||||
Cancle
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -352,7 +368,7 @@ export function UrlForm({
|
||||
{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>
|
||||
|
||||
@@ -5,8 +5,9 @@ import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { SidebarNavItem } from "@/types";
|
||||
import { Menu, PanelLeftClose, PanelRightClose } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Link } from "next-view-transitions";
|
||||
import { name, version } from "package.json";
|
||||
import pkg from "package.json";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -28,6 +29,7 @@ interface DashboardSidebarProps {
|
||||
}
|
||||
|
||||
export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
const t = useTranslations("System");
|
||||
const path = usePathname();
|
||||
|
||||
const { isTablet } = useMediaQuery();
|
||||
@@ -98,7 +100,7 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
>
|
||||
{isSidebarExpanded ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{section.title}
|
||||
{t(section.title)}
|
||||
</p>
|
||||
) : (
|
||||
<div className="h-4" />
|
||||
@@ -122,7 +124,7 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
{item.badge && (
|
||||
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
|
||||
{item.badge}
|
||||
@@ -150,7 +152,7 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -172,9 +174,9 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
rel="noreferrer"
|
||||
className="font-medium underline-offset-2 hover:underline"
|
||||
>
|
||||
{name}
|
||||
{pkg.name}
|
||||
</Link>
|
||||
v{version}
|
||||
v{pkg.version}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -189,6 +191,7 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
const path = usePathname();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { isSm, isMobile } = useMediaQuery();
|
||||
const t = useTranslations("System");
|
||||
|
||||
if (isSm || isMobile) {
|
||||
return (
|
||||
@@ -229,7 +232,7 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
className="flex flex-col gap-0.5"
|
||||
>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{section.title}
|
||||
{t(section.title)}
|
||||
</p>
|
||||
|
||||
{section.items.map((item) => {
|
||||
@@ -253,7 +256,7 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
{item.badge && (
|
||||
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
|
||||
{item.badge}
|
||||
@@ -276,9 +279,9 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
rel="noreferrer"
|
||||
className="font-medium underline-offset-2 hover:underline"
|
||||
>
|
||||
{name}
|
||||
{pkg.name}
|
||||
</Link>
|
||||
v{version}
|
||||
v{pkg.version}
|
||||
</p>
|
||||
|
||||
{/* <div className="mt-auto">
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { docsConfig } from "@/config/docs";
|
||||
import { marketingConfig } from "@/config/marketing";
|
||||
@@ -16,6 +17,7 @@ import { Icons } from "@/components/shared/icons";
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
|
||||
export function NavMobile() {
|
||||
const t = useTranslations("System");
|
||||
const { data: session } = useSession();
|
||||
const [open, setOpen] = useState(false);
|
||||
const selectedLayout = useSelectedLayoutSegment();
|
||||
@@ -69,7 +71,7 @@ export function NavMobile() {
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex w-full font-medium capitalize"
|
||||
>
|
||||
{title}
|
||||
{t(title)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -83,7 +85,7 @@ export function NavMobile() {
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex w-full font-medium capitalize"
|
||||
>
|
||||
Admin
|
||||
{t("Admin")}
|
||||
</Link>
|
||||
</li>
|
||||
) : null}
|
||||
@@ -94,7 +96,7 @@ export function NavMobile() {
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex w-full font-medium capitalize"
|
||||
>
|
||||
Dashboard
|
||||
{t("Dashboard")}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
@@ -106,7 +108,7 @@ export function NavMobile() {
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex w-full font-medium capitalize"
|
||||
>
|
||||
Sign in
|
||||
{t("Sign in")}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
import * as React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Icons } from "@/components/shared/icons"
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
const { setTheme } = useTheme();
|
||||
const t = useTranslations("System");
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -21,23 +23,23 @@ export function ModeToggle() {
|
||||
<Button variant="ghost" size="sm" className="size-8 px-0">
|
||||
<Icons.sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Icons.moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
<span className="sr-only">{t("Toggle theme")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<Icons.sun className="mr-2 size-4" />
|
||||
<span>Light</span>
|
||||
<span>{t("Light")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<Icons.moon className="mr-2 size-4" />
|
||||
<span>Dark</span>
|
||||
<span>{t("Dark")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<Icons.laptop className="mr-2 size-4" />
|
||||
<span>System</span>
|
||||
<span>{t("System")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { docsConfig } from "@/config/docs";
|
||||
import { marketingConfig } from "@/config/marketing";
|
||||
@@ -10,13 +10,11 @@ import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useScroll } from "@/hooks/use-scroll";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { DocsSearch } from "@/components/docs/search";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
|
||||
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
import { UserAccountNav } from "./user-account-nav";
|
||||
|
||||
interface NavBarProps {
|
||||
scroll?: boolean;
|
||||
@@ -25,7 +23,6 @@ interface NavBarProps {
|
||||
|
||||
export function NavBar({ scroll = false }: NavBarProps) {
|
||||
const scrolled = useScroll(50);
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
const selectedLayout = useSelectedLayoutSegment();
|
||||
const documentation = selectedLayout === "docs";
|
||||
@@ -37,6 +34,8 @@ export function NavBar({ scroll = false }: NavBarProps) {
|
||||
const links =
|
||||
(selectedLayout && configMap[selectedLayout]) || marketingConfig.mainNav;
|
||||
|
||||
const t = useTranslations("System");
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`sticky top-0 z-40 flex w-full justify-center bg-background/60 backdrop-blur-xl transition-all ${
|
||||
@@ -75,7 +74,7 @@ export function NavBar({ scroll = false }: NavBarProps) {
|
||||
item.disabled && "cursor-not-allowed opacity-80",
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
@@ -108,7 +107,7 @@ export function NavBar({ scroll = false }: NavBarProps) {
|
||||
className="hidden text-sm font-medium text-foreground/60 transition-colors hover:text-foreground/80 md:block"
|
||||
>
|
||||
<Button className="" variant="outline" size="sm" rounded="lg">
|
||||
Dashboard
|
||||
{t("Dashboard")}
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="hidden md:flex">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { name, version } from "package.json";
|
||||
|
||||
import { footerLinks, siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -56,16 +57,17 @@ export function SiteFooter({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||
|
||||
<div className="border-t py-4">
|
||||
<div className="container flex max-w-6xl items-center justify-between">
|
||||
<p className="font-mono text-sm text-muted-foreground/70">
|
||||
© 2024{" "}
|
||||
<p className="mx-3 mt-auto flex items-center gap-1 pb-3 pt-6 font-mono text-xs text-muted-foreground/90">
|
||||
© 2024
|
||||
<Link
|
||||
href={siteConfig.links.github}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium text-primary underline underline-offset-2"
|
||||
className="font-medium underline-offset-2 hover:underline"
|
||||
>
|
||||
oiov
|
||||
{name}
|
||||
</Link>
|
||||
v{version}
|
||||
. <br className="block sm:hidden" /> Built with{" "}
|
||||
<Link
|
||||
href="https://www.cloudflare.com?ref=wrdo"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { LayoutDashboard, Lock, LogOut, Settings } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Drawer } from "vaul";
|
||||
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
@@ -28,6 +29,7 @@ export function UserAccountNav() {
|
||||
};
|
||||
|
||||
const { isMobile } = useMediaQuery();
|
||||
const t = useTranslations("System");
|
||||
|
||||
if (!user)
|
||||
return (
|
||||
@@ -85,7 +87,7 @@ export function UserAccountNav() {
|
||||
className="flex w-full items-center gap-3 px-2.5 py-2"
|
||||
>
|
||||
<Lock className="size-4" />
|
||||
<p className="text-sm">Admin</p>
|
||||
<p className="text-sm">{t("Admin")}</p>
|
||||
</Link>
|
||||
</li>
|
||||
) : null}
|
||||
@@ -97,7 +99,7 @@ export function UserAccountNav() {
|
||||
className="flex w-full items-center gap-3 px-2.5 py-2"
|
||||
>
|
||||
<LayoutDashboard className="size-4" />
|
||||
<p className="text-sm">Dashboard</p>
|
||||
<p className="text-sm">{t("Dashboard")}</p>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -108,7 +110,7 @@ export function UserAccountNav() {
|
||||
className="flex w-full items-center gap-3 px-2.5 py-2"
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
<p className="text-sm">Settings</p>
|
||||
<p className="text-sm">{t("Settings")}</p>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -123,7 +125,7 @@ export function UserAccountNav() {
|
||||
>
|
||||
<div className="flex w-full items-center gap-3 px-2.5 py-2">
|
||||
<LogOut className="size-4" />
|
||||
<p className="text-sm">Log out </p>
|
||||
<p className="text-sm">{t("Log out")}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -170,7 +172,7 @@ export function UserAccountNav() {
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin" className="flex items-center space-x-2.5">
|
||||
<Lock className="size-4" />
|
||||
<p className="text-sm">Admin</p>
|
||||
<p className="text-sm">{t("Admin")}</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
@@ -178,7 +180,7 @@ export function UserAccountNav() {
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/dashboard" className="flex items-center space-x-2.5">
|
||||
<LayoutDashboard className="size-4" />
|
||||
<p className="text-sm">Dashboard</p>
|
||||
<p className="text-sm">{t("Dashboard")}</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -188,7 +190,7 @@ export function UserAccountNav() {
|
||||
className="flex items-center space-x-2.5"
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
<p className="text-sm">Settings</p>
|
||||
<p className="text-sm">{t("Settings")}</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -203,7 +205,7 @@ export function UserAccountNav() {
|
||||
>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<LogOut className="size-4" />
|
||||
<p className="text-sm">Log out </p>
|
||||
<p className="text-sm">{t("Log out")}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"user client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
@@ -11,8 +13,12 @@ import { Icons } from "@/components/shared/icons";
|
||||
import EmailManagerExp from "./email";
|
||||
import UrlShortener from "./url-shortener";
|
||||
|
||||
export default async function HeroLanding() {
|
||||
const user = await getCurrentUser();
|
||||
export default function HeroLanding({
|
||||
userId,
|
||||
}: {
|
||||
userId: string | undefined;
|
||||
}) {
|
||||
const t = useTranslations("Landing");
|
||||
return (
|
||||
<section className="custom-bg relative space-y-6 py-12 sm:py-20 lg:py-24">
|
||||
<div className="container flex max-w-screen-lg flex-col items-center gap-5 text-center">
|
||||
@@ -24,23 +30,24 @@ export default async function HeroLanding() {
|
||||
"px-4",
|
||||
)}
|
||||
>
|
||||
<span className="mr-1">🚀</span>Deploy with
|
||||
<span className="mr-1">🚀</span>
|
||||
{t("deployWithVercel")}
|
||||
<span className="font-bold" style={{ fontFamily: "Bahamas Bold" }}>
|
||||
Vercel
|
||||
</span>
|
||||
now!
|
||||
|
||||
{t("now")}
|
||||
</Link>
|
||||
|
||||
<h1 className="text-balance font-satoshi text-[40px] font-black leading-[1.15] tracking-tight sm:text-5xl md:text-6xl md:leading-[1.15]">
|
||||
One platform powers{" "}
|
||||
{t("onePlatformPowers")}
|
||||
<span className="bg-gradient-to-r from-violet-600 via-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
||||
endless solutions
|
||||
{t("endlessSolutions")}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-2xl text-balance text-muted-foreground sm:text-lg">
|
||||
Link shortening, domain hosting, email manager and <br />
|
||||
open api, everything you need to build better.
|
||||
{t("platformDescription")}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
@@ -57,7 +64,7 @@ export default async function HeroLanding() {
|
||||
"gap-2 bg-primary-foreground px-4 text-[15px] font-semibold text-primary hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<span>Documents</span>
|
||||
<span>{t("documents")}</span>
|
||||
<Icons.bookOpen className="size-4" />
|
||||
</Link>
|
||||
<Link
|
||||
@@ -68,7 +75,7 @@ export default async function HeroLanding() {
|
||||
"px-4 text-[15px] font-semibold",
|
||||
)}
|
||||
>
|
||||
<span>{user?.id ? "Dashboard" : "Sign in for free"}</span>
|
||||
<span>{userId ? t("Dashboard") : t("signInForFree")}</span>
|
||||
{/* <Icons.arrowRight className="size-4" /> */}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -80,13 +87,14 @@ export default async function HeroLanding() {
|
||||
}
|
||||
|
||||
export function LandingImages() {
|
||||
const t = useTranslations("Landing");
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-10 w-full max-w-6xl px-6">
|
||||
<div className="my-14 flex flex-col items-center justify-around gap-10 md:flex-row-reverse">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={"example"}
|
||||
alt={t("exampleImageAlt")}
|
||||
src="/_static/landing/link.svg"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAACCSURBVBhXZYzBCgIxDEQnTdPau+hveBB/XtiLn+NJQdoNS2Orq6zuO0zgZRhSVbvegeAJGx7hvUeMAUSEzu1RUesEKuNkIgyrFaoFzB4i8i1+cDEwXHOuRc65lbVpe38XuPm+YMdIKa3WOj9F60vWcj0IOg8Xy7ngdDxgv9vO+h/gCZNAKuSRdQ2rAAAAAElFTkSuQmCC"
|
||||
@@ -95,21 +103,16 @@ export function LandingImages() {
|
||||
/>
|
||||
<div className="grids grids-dark px-2 py-4">
|
||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">
|
||||
URL Shortening
|
||||
{t("urlShorteningTitle")}
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
📖 Instantly transform long, unwieldy URLs into short, memorable
|
||||
links that are easy to share. Enjoy built-in analytics to track
|
||||
clicks, monitor performance, and gain insights into your
|
||||
audience—all in real time.
|
||||
</p>
|
||||
<p className="text-lg">{t("urlShorteningDescription")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-around gap-10 md:flex-row">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={"example"}
|
||||
alt={t("exampleImageAlt")}
|
||||
src="/_static/landing/hosting.svg"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAACCSURBVBhXZYzBCgIxDEQnTdPau+hveBB/XtiLn+NJQdoNS2Orq6zuO0zgZRhSVbvegeAJGx7hvUeMAUSEzu1RUesEKuNkIgyrFaoFzB4i8i1+cDEwXHOuRc65lbVpe38XuPm+YMdIKa3WOj9F60vWcj0IOg8Xy7ngdDxgv9vO+h/gCZNAKuSRdQ2rAAAAAElFTkSuQmCC"
|
||||
@@ -118,21 +121,16 @@ export function LandingImages() {
|
||||
/>
|
||||
<div className="grids grids-dark px-2 py-4">
|
||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">
|
||||
Free Subdomain Hosting
|
||||
{t("freeSubdomainHostingTitle")}
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
🎉 Kickstart your online presence with free, fully customizable
|
||||
subdomains. Whether you're launching a personal project or
|
||||
testing a business idea, get started quickly with no cost and
|
||||
reliable hosting you can trust.
|
||||
</p>
|
||||
<p className="text-lg">{t("freeSubdomainHostingDescription")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-14 flex flex-col items-center justify-around gap-10 md:flex-row-reverse">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={"example"}
|
||||
alt={t("exampleImageAlt")}
|
||||
src="/_static/landing/email.svg"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAACCSURBVBhXZYzBCgIxDEQnTdPau+hveBB/XtiLn+NJQdoNS2Orq6zuO0zgZRhSVbvegeAJGx7hvUeMAUSEzu1RUesEKuNkIgyrFaoFzB4i8i1+cDEwXHOuRc65lbVpe38XuPm+YMdIKa3WOj9F60vWcj0IOg8Xy7ngdDxgv9vO+h/gCZNAKuSRdQ2rAAAAAElFTkSuQmCC"
|
||||
@@ -141,13 +139,17 @@ export function LandingImages() {
|
||||
/>
|
||||
<div className="grids grids-dark px-2 py-4">
|
||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">
|
||||
Email Receivers & Senders
|
||||
{t("emailReceiversSendersTitle")}
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
📧 Seamlessly receive and send emails from any email provider with
|
||||
top-notch security. Stay connected and manage your communications
|
||||
effortlessly, knowing your data is protected with robust
|
||||
encryption and privacy features.
|
||||
{t("emailReceiversSendersDescription")}{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href="/dashboard/settings"
|
||||
target="_blank"
|
||||
>
|
||||
{t("applyYourApiKey")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +157,7 @@ export function LandingImages() {
|
||||
<div className="flex flex-col items-center justify-around gap-10 md:flex-row">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={"example"}
|
||||
alt={t("exampleImageAlt")}
|
||||
src="/_static/landing/domain.svg"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAACCSURBVBhXZYzBCgIxDEQnTdPau+hveBB/XtiLn+NJQdoNS2Orq6zuO0zgZRhSVbvegeAJGx7hvUeMAUSEzu1RUesEKuNkIgyrFaoFzB4i8i1+cDEwXHOuRc65lbVpe38XuPm+YMdIKa3WOj9F60vWcj0IOg8Xy7ngdDxgv9vO+h/gCZNAKuSRdQ2rAAAAAElFTkSuQmCC"
|
||||
@@ -164,21 +166,16 @@ export function LandingImages() {
|
||||
/>
|
||||
<div className="grids grids-dark px-2 py-4">
|
||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">
|
||||
Multiple Domains
|
||||
{t("multipleDomainsTitle")}
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
🤩 Empower your business with the flexibility of multiple domains,
|
||||
such as wr.do, uv.do, and more. Establish a strong digital
|
||||
footprint, create branded links, or manage diverse projects—all
|
||||
under one unified platform.
|
||||
</p>
|
||||
<p className="text-lg">{t("multipleDomainsDescription")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-around gap-10 md:flex-row-reverse">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={"example"}
|
||||
alt={t("exampleImageAlt")}
|
||||
src="/_static/landing/screenshot.svg"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAACCSURBVBhXZYzBCgIxDEQnTdPau+hveBB/XtiLn+NJQdoNS2Orq6zuO0zgZRhSVbvegeAJGx7hvUeMAUSEzu1RUesEKuNkIgyrFaoFzB4i8i1+cDEwXHOuRc65lbVpe38XuPm+YMdIKa3WOj9F60vWcj0IOg8Xy7ngdDxgv9vO+h/gCZNAKuSRdQ2rAAAAAElFTkSuQmCC"
|
||||
@@ -187,19 +184,16 @@ export function LandingImages() {
|
||||
/>
|
||||
<div className="grids grids-dark px-2 py-4">
|
||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">
|
||||
Website Screenshot API
|
||||
{t("websiteScreenshotApiTitle")}
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
📷 Capture high-quality screenshots of any webpage instantly with
|
||||
our powerful Screenshot API. Integrate seamlessly into your
|
||||
applications, access third-party services, and unlock advanced
|
||||
features by applying your unique API key.
|
||||
{t("websiteScreenshotApiDescription")}{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href="/dashboard/settings"
|
||||
target="_blank"
|
||||
>
|
||||
Apply your api key-->
|
||||
{t("applyYourApiKey")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -208,7 +202,7 @@ export function LandingImages() {
|
||||
<div className="my-14 flex flex-col items-center justify-around gap-10 md:flex-row">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={"example"}
|
||||
alt={t("exampleImageAlt")}
|
||||
src="/_static/landing/info.svg"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAACCSURBVBhXZYzBCgIxDEQnTdPau+hveBB/XtiLn+NJQdoNS2Orq6zuO0zgZRhSVbvegeAJGx7hvUeMAUSEzu1RUesEKuNkIgyrFaoFzB4i8i1+cDEwXHOuRc65lbVpe38XuPm+YMdIKa3WOj9F60vWcj0IOg8Xy7ngdDxgv9vO+h/gCZNAKuSRdQ2rAAAAAElFTkSuQmCC"
|
||||
@@ -217,19 +211,16 @@ export function LandingImages() {
|
||||
/>
|
||||
<div className="grids grids-dark px-2 py-4">
|
||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">
|
||||
Meta Information API
|
||||
{t("metaInformationApiTitle")}
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
🍥 Extract rich, structured web data effortlessly with our smart
|
||||
Meta Information API. Perfect for developers, businesses, or
|
||||
researchers, this tool offers seamless integration, third-party
|
||||
service access, and enhanced functionality.
|
||||
{t("metaInformationApiDescription")}{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href="/dashboard/settings"
|
||||
target="_blank"
|
||||
>
|
||||
Apply your api key-->
|
||||
{t("applyYourApiKey")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { CSSProperties, ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { X } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { TeamPlanQuota } from "@/config/team";
|
||||
import { cn, nFormatter } from "@/lib/utils";
|
||||
@@ -66,6 +67,7 @@ const getBenefits = (plan) => [
|
||||
];
|
||||
|
||||
export const PricingSection = () => {
|
||||
const t = useTranslations("Landing");
|
||||
return (
|
||||
<section
|
||||
id="pricing"
|
||||
@@ -75,49 +77,48 @@ export const PricingSection = () => {
|
||||
<div className="relative z-10 mx-auto max-w-5xl px-4 py-20 md:px-8">
|
||||
<div className="mb-12 space-y-3">
|
||||
<h2 className="text-center text-xl font-semibold leading-tight sm:text-3xl sm:leading-tight md:text-4xl md:leading-tight">
|
||||
Pricing
|
||||
{t("pricingTitle")}
|
||||
</h2>
|
||||
<p className="text-center text-base text-zinc-600 dark:text-zinc-400 md:text-lg">
|
||||
Use it for free for yourself, upgrade when your team needs advanced
|
||||
control.
|
||||
{t("pricingDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<PriceCard
|
||||
tier="Free"
|
||||
price="$0/mo"
|
||||
bestFor="For hobbyists and individuals looking to manage their links"
|
||||
tier={t("freeTier")}
|
||||
price={t("freePrice")}
|
||||
bestFor={t("freeBestFor")}
|
||||
CTA={
|
||||
<Link href={"/dashboard"}>
|
||||
<Button className="w-full" variant={"default"}>
|
||||
Get started free
|
||||
{t("getStartedFree")}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
benefits={getBenefits(TeamPlanQuota.free)}
|
||||
/>
|
||||
{/* <PriceCard
|
||||
tier="Premium"
|
||||
price="$5/mo"
|
||||
bestFor="Best for 5-50 users"
|
||||
tier={t("premiumTier")}
|
||||
price={t("premiumPrice")}
|
||||
bestFor={t("premiumBestFor")}
|
||||
CTA={
|
||||
<Link href={"/pricing"}>
|
||||
<Button className="w-full bg-zinc-800 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50 dark:bg-zinc-50 dark:text-zinc-950 dark:hover:bg-zinc-200 dark:hover:text-zinc-900">
|
||||
Get free trial
|
||||
{t("getFreeTrial")}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
benefits={getBenefits(TeamPlanQuota.premium)}
|
||||
/> */}
|
||||
<PriceCard
|
||||
tier="Enterprise"
|
||||
price="Contact us"
|
||||
bestFor="For large organizations with custom needs"
|
||||
tier={t("enterpriseTier")}
|
||||
price={t("enterprisePrice")}
|
||||
bestFor={t("enterpriseBestFor")}
|
||||
CTA={
|
||||
<Link href={"mailto:support@wr.do"}>
|
||||
<Button className="w-full" variant="outline">
|
||||
Contact us
|
||||
{t("contactUs")}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function ApiReference({
|
||||
badge,
|
||||
@@ -18,22 +13,24 @@ export default function ApiReference({
|
||||
target: string;
|
||||
link: string;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Reference</CardTitle>
|
||||
<CardTitle>{t("API Reference")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Badge>{badge}</Badge>
|
||||
<div className="mt-2">
|
||||
<span style={{ fontFamily: "Bahamas Bold" }}>WR.DO</span> provide a
|
||||
api for {target}. View the usage tutorial document{" "}
|
||||
<span style={{ fontFamily: "Bahamas Bold" }}>WR.DO</span>{" "}
|
||||
{t("provide a api for {target}", { target: t(target) })}.{" "}
|
||||
{t("View the usage tutorial")}{" "}
|
||||
<Link
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="font-semibold after:content-['_↗'] hover:text-blue-500 hover:underline"
|
||||
>
|
||||
here
|
||||
{t("document")}
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -44,6 +45,8 @@ export function PaginationWrapper({
|
||||
// Calculate total pages based on pageSize
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
const t = useTranslations("List");
|
||||
|
||||
const shouldShowPage = (page: number) => {
|
||||
return (
|
||||
page === 1 || page === totalPages || Math.abs(page - currentPage) <= 2
|
||||
@@ -144,7 +147,7 @@ export function PaginationWrapper({
|
||||
>
|
||||
{setPageSize && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Show</span>
|
||||
<span className="text-muted-foreground">{t("Show")}</span>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={handlePageSizeChange}
|
||||
@@ -172,7 +175,7 @@ export function PaginationWrapper({
|
||||
"hidden sm:inline-block",
|
||||
)}
|
||||
>
|
||||
per page
|
||||
{t("per page")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+20
-18
@@ -10,6 +10,7 @@ import {
|
||||
} from "react";
|
||||
import Link from "next/link";
|
||||
import { debounce } from "lodash";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -45,6 +46,7 @@ export default function QRCodeEditor({
|
||||
user: { id: string; apiKey: string; team: string };
|
||||
url: string;
|
||||
}) {
|
||||
const t = useTranslations("List");
|
||||
const [params, setParams] = useState({
|
||||
key: user.apiKey,
|
||||
url,
|
||||
@@ -145,13 +147,13 @@ export default function QRCodeEditor({
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-lg rounded-lg bg-white p-4 shadow-lg dark:bg-neutral-900">
|
||||
<h2 className="mb-4 text-lg font-semibold">QR Code Design</h2>
|
||||
<h2 className="mb-4 text-lg font-semibold">{t("QR Code Design")}</h2>
|
||||
|
||||
{/* QR Code Preview */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<h3 className="text-sm font-semibold text-neutral-600 dark:text-neutral-300">
|
||||
Preview
|
||||
{t("Preview")}
|
||||
</h3>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="ml-auto px-2 py-2 hover:bg-accent hover:text-accent-foreground">
|
||||
@@ -166,7 +168,7 @@ export default function QRCodeEditor({
|
||||
>
|
||||
<div className="flex items-center gap-2 text-neutral-500">
|
||||
<Icons.media className="size-4" />
|
||||
<span className="font-semibold">Download SVG</span>
|
||||
<span className="font-semibold">{t("Download SVG")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -181,7 +183,7 @@ export default function QRCodeEditor({
|
||||
>
|
||||
<div className="flex items-center gap-2 text-neutral-500">
|
||||
<Icons.media className="size-4" />
|
||||
<span className="font-semibold">Download PNG</span>
|
||||
<span className="font-semibold">{t("Download PNG")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -196,7 +198,7 @@ export default function QRCodeEditor({
|
||||
>
|
||||
<div className="flex items-center gap-2 text-neutral-500">
|
||||
<Icons.media className="size-4" />
|
||||
<span className="font-semibold">Download JPG</span>
|
||||
<span className="font-semibold">{t("Download JPG")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -234,7 +236,7 @@ export default function QRCodeEditor({
|
||||
|
||||
<div className="group mb-3 flex items-center justify-between">
|
||||
<h3 className="text-nowrap text-sm font-semibold text-neutral-600 transition-all group-hover:ml-1 group-hover:font-bold dark:text-neutral-300">
|
||||
Url
|
||||
{t("Url")}
|
||||
</h3>
|
||||
<Input
|
||||
className="ml-auto w-3/5"
|
||||
@@ -248,7 +250,7 @@ export default function QRCodeEditor({
|
||||
<div className="mb-3">
|
||||
<div className="group mb-3 flex items-center justify-between">
|
||||
<h3 className="text-nowrap text-sm font-semibold text-neutral-600 transition-all group-hover:ml-1 group-hover:font-bold dark:text-neutral-300">
|
||||
Logo
|
||||
{t("Logo")}
|
||||
</h3>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
@@ -256,13 +258,13 @@ export default function QRCodeEditor({
|
||||
<Icons.help className="ml-1 size-4 text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64 text-left">
|
||||
Display your logo in the center of the QR code.{" "}
|
||||
{t("Display your logo in the center of the QR code")}.{" "}
|
||||
<Link
|
||||
className="border-b text-neutral-500"
|
||||
href="/docs/open-api/qrcode"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
{t("Learn more")}
|
||||
</Link>
|
||||
.
|
||||
</TooltipContent>
|
||||
@@ -277,7 +279,7 @@ export default function QRCodeEditor({
|
||||
<details className="group">
|
||||
<summary className="flex w-full cursor-pointer items-center justify-between">
|
||||
<h3 className="text-nowrap text-sm font-semibold text-neutral-600 transition-all group-hover:ml-1 group-hover:font-bold dark:text-neutral-300">
|
||||
Custom Logo
|
||||
{t("Custom Logo")}
|
||||
</h3>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
@@ -291,13 +293,13 @@ export default function QRCodeEditor({
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64 text-left">
|
||||
Customize your QR code logo.{" "}
|
||||
{t("Customize your QR code logo")}.{" "}
|
||||
<Link
|
||||
className="border-b text-neutral-500"
|
||||
href="/docs/open-api/qrcode"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
{t("Learn more")}
|
||||
</Link>
|
||||
.
|
||||
</TooltipContent>
|
||||
@@ -319,7 +321,7 @@ export default function QRCodeEditor({
|
||||
<details className="group mb-3">
|
||||
<summary className="flex w-full cursor-pointer items-center justify-between">
|
||||
<h3 className="text-nowrap text-sm font-semibold text-neutral-600 transition-all group-hover:ml-1 group-hover:font-bold dark:text-neutral-300">
|
||||
Front Color
|
||||
{t("Front Color")}
|
||||
</h3>
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
</summary>
|
||||
@@ -373,7 +375,7 @@ export default function QRCodeEditor({
|
||||
<details className="group" open={true}>
|
||||
<summary className="flex w-full cursor-pointer items-center justify-between">
|
||||
<h3 className="text-nowrap text-sm font-semibold text-neutral-600 transition-all group-hover:ml-1 group-hover:font-bold dark:text-neutral-300">
|
||||
Background Color
|
||||
{t("Background Color")}
|
||||
</h3>
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
</summary>
|
||||
@@ -428,20 +430,20 @@ export default function QRCodeEditor({
|
||||
{!user.apiKey && (
|
||||
<div className="absolute left-0 top-0 z-20 flex size-full flex-col items-center justify-center gap-2 bg-neutral-100/20 px-4 backdrop-blur">
|
||||
<p className="text-center text-sm">
|
||||
Please create a <strong>api key</strong> before use this feature.{" "}
|
||||
<br /> Learn more about{" "}
|
||||
{t("Please create a api key before use this feature")}. <br />{" "}
|
||||
{t("Learn more about")}{" "}
|
||||
<Link
|
||||
className="py-1 text-blue-600 hover:text-blue-400 hover:underline dark:hover:text-primary-foreground"
|
||||
href={"/docs/open-api#api-key"}
|
||||
target="_blank"
|
||||
>
|
||||
api key
|
||||
Api key
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<Link href={"/dashboard/settings"}>
|
||||
<Button>Create Api Key</Button>
|
||||
<Button>{t("Create Api Key")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useLocale } from "next-intl";
|
||||
import TimeAgo from "timeago-react";
|
||||
|
||||
export function TimeAgoIntl({ date }: { date: Date }) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<TimeAgo datetime={date} locale={locale === "zh" ? "zh_CN" : locale} />
|
||||
);
|
||||
}
|
||||
@@ -81,8 +81,8 @@ git clone https://github.com/oiov/cf-email-forwarding-worker.git
|
||||
cd cf-email-forwarding-worker
|
||||
pnpm install
|
||||
|
||||
wranler login
|
||||
wranler deploy
|
||||
wrangler login
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
在部署前,记得在 `wrangler.jsonc` 中添加你的环境变量。
|
||||
|
||||
@@ -83,8 +83,8 @@ git clone https://github.com/oiov/cf-email-forwarding-worker.git
|
||||
cd cf-email-forwarding-worker
|
||||
pnpm install
|
||||
|
||||
wranler login
|
||||
wranler deploy
|
||||
wrangler login
|
||||
wrangler deploy
|
||||
```
|
||||
Remember to add your environment variables in `wrangler.jsonc` before deploy.
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ description: 选择你的部署方式
|
||||
|
||||
<DocsLang en="/docs/developer/deploy" zh="/docs/developer/deploy-zh" />
|
||||
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
在阅读此文档之前,建议首先阅读 [快速开始](/docs/developer/quick-start-zh),以确认准备好依赖的环境、变量。
|
||||
</Callout>
|
||||
|
||||
## 使用 Vercel 部署(推荐)
|
||||
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
|
||||
@@ -5,6 +5,11 @@ description: Choose your deployment method
|
||||
|
||||
<DocsLang en="/docs/developer/deploy" zh="/docs/developer/deploy-zh" />
|
||||
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
Before reading this document, it is recommended to first read [Quick Start](/docs/developer/quick-start),
|
||||
to confirm that the necessary environment variables are ready.
|
||||
</Callout>
|
||||
|
||||
## Deploy with Vercel (Recommended)
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/oiov/wr.do.git&project-name=wrdo&env=DATABASE_URL&env=AUTH_SECRET&env=RESEND_API_KEY&env=NEXT_PUBLIC_EMAIL_R2_DOMAIN&env=NEXT_PUBLIC_OPEN_SIGNUP&env=GITHUB_TOKEN)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { headers } from "next/headers";
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
const locales = ["en", "zh"];
|
||||
const defaultLocale = "en";
|
||||
|
||||
const headersList = headers();
|
||||
// 获取 cookie 中的语言设置
|
||||
const cookieLanguage = headersList
|
||||
.get("cookie")
|
||||
?.split(";")
|
||||
.map((cookie) => cookie.trim())
|
||||
.find((cookie) => cookie.startsWith("language="))
|
||||
?.split("=")[1];
|
||||
// 如果 cookie 中有有效的语言设置,直接使用
|
||||
if (cookieLanguage && locales.includes(cookieLanguage)) {
|
||||
return {
|
||||
locale: cookieLanguage,
|
||||
messages: (await import(`../locales/${cookieLanguage}.json`)).default,
|
||||
};
|
||||
}
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
// 解析用户偏好的语言列表
|
||||
const userLanguages = acceptLanguage
|
||||
.split(",")
|
||||
.map((lang) => {
|
||||
const [language, weight = "q=1.0"] = lang.split(";");
|
||||
return {
|
||||
language: language.split("-")[0], // 只取主要语言代码
|
||||
weight: parseFloat(weight.split("=")[1]),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.weight - a.weight);
|
||||
|
||||
// 查找第一个匹配的支持语言
|
||||
const matchedLocale = userLanguages.find(({ language }) =>
|
||||
locales.includes(language),
|
||||
);
|
||||
const locale = matchedLocale ? matchedLocale.language : defaultLocale;
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../locales/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
@@ -97,6 +97,9 @@ export async function getDomainsByFeatureClient(feature: string) {
|
||||
select: {
|
||||
domain_name: true,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
return domains;
|
||||
} catch (error) {
|
||||
|
||||
+7
-107
@@ -69,112 +69,6 @@ export async function saveForwardEmail(emailData: OriginalEmail) {
|
||||
return res.id;
|
||||
}
|
||||
|
||||
// 查询所有 UserEmail
|
||||
// export async function getAllUserEmails(
|
||||
// userId: string,
|
||||
// page: number,
|
||||
// size: number,
|
||||
// search: string,
|
||||
// admin: boolean,
|
||||
// unreadOnly: boolean = false,
|
||||
// ) {
|
||||
// let whereOptions = {};
|
||||
// if (admin) {
|
||||
// whereOptions = {
|
||||
// // deletedAt: null,
|
||||
// emailAddress: { contains: search, mode: "insensitive" },
|
||||
// };
|
||||
// } else {
|
||||
// whereOptions = {
|
||||
// userId,
|
||||
// deletedAt: null,
|
||||
// emailAddress: { contains: search, mode: "insensitive" },
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Fetch paginated UserEmail records
|
||||
// const userEmailsPromise = prisma.userEmail.findMany({
|
||||
// where: { ...whereOptions },
|
||||
// select: {
|
||||
// id: true,
|
||||
// userId: true,
|
||||
// emailAddress: true,
|
||||
// createdAt: true,
|
||||
// updatedAt: true,
|
||||
// deletedAt: true,
|
||||
// _count: { select: { forwardEmails: true } }, // Count of forwardEmails for this UserEmail
|
||||
// user: { select: { name: true, email: true } },
|
||||
// forwardEmails: {
|
||||
// select: {
|
||||
// readAt: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// skip: (page - 1) * size,
|
||||
// take: size,
|
||||
// orderBy: {
|
||||
// updatedAt: "desc",
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Fetch total count of UserEmail records
|
||||
// const totalPromise = prisma.userEmail.count({
|
||||
// where: { ...whereOptions },
|
||||
// });
|
||||
|
||||
// // Fetch all emailAddress values that match the whereOptions
|
||||
// const emailAddressesPromise = prisma.userEmail.findMany({
|
||||
// where: { ...whereOptions },
|
||||
// select: { emailAddress: true },
|
||||
// });
|
||||
|
||||
// const [userEmails, total, emailAddresses] = await Promise.all([
|
||||
// userEmailsPromise,
|
||||
// totalPromise,
|
||||
// emailAddressesPromise,
|
||||
// ]);
|
||||
|
||||
// // Extract all email addresses for the total counts query
|
||||
// const emailAddressList = emailAddresses.map((e) => e.emailAddress);
|
||||
|
||||
// // Fetch total inbox and unread counts across all matching UserEmails
|
||||
// const [totalInboxCount, totalUnreadCount] = await Promise.all([
|
||||
// prisma.forwardEmail.count({
|
||||
// where: {
|
||||
// to: { in: emailAddressList },
|
||||
// },
|
||||
// }),
|
||||
// prisma.forwardEmail.count({
|
||||
// where: {
|
||||
// to: { in: emailAddressList },
|
||||
// readAt: null,
|
||||
// },
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
// const result = userEmails.map((email) => {
|
||||
// const unreadCount = email.forwardEmails.filter(
|
||||
// (mail) => mail.readAt === null,
|
||||
// ).length;
|
||||
|
||||
// return {
|
||||
// ...email,
|
||||
// count: email._count.forwardEmails, // Total emails for this specific UserEmail
|
||||
// unreadCount: unreadCount, // Unread emails for this specific UserEmail
|
||||
// user: email.user.name,
|
||||
// email: email.user.email,
|
||||
// forwardEmails: undefined,
|
||||
// };
|
||||
// });
|
||||
|
||||
// return {
|
||||
// list: result,
|
||||
// total, // Total number of UserEmail records
|
||||
// totalInboxCount, // Total number of ForwardEmail records for all matching UserEmails
|
||||
// totalUnreadCount, // Total number of unread ForwardEmail records for all matching UserEmails
|
||||
// };
|
||||
// }
|
||||
|
||||
// 查询所有 UserEmail
|
||||
export async function getAllUserEmails(
|
||||
userId: string,
|
||||
@@ -396,7 +290,6 @@ export async function deleteUserEmailByAddress(email: string) {
|
||||
const userEmail = await prisma.userEmail.findFirst({
|
||||
where: { emailAddress: email, deletedAt: null },
|
||||
});
|
||||
console.log("userEmail", userEmail);
|
||||
|
||||
if (userEmail) {
|
||||
await prisma.userEmail.update({
|
||||
@@ -574,6 +467,13 @@ export async function markAllEmailsAsRead(userEmailId: string, userId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除邮件
|
||||
export async function deleteEmailsByIds(ids: string[]) {
|
||||
return prisma.forwardEmail.deleteMany({
|
||||
where: { id: { in: ids } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveUserSendEmail(
|
||||
userId: string,
|
||||
from: string,
|
||||
|
||||
@@ -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())}${
|
||||
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
{
|
||||
"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",
|
||||
"Add URL": "Add URL",
|
||||
"Slug": "Slug",
|
||||
"Target": "Target",
|
||||
"User": "User",
|
||||
"Enabled": "Enabled",
|
||||
"Expiration": "Expiration",
|
||||
"Clicks": "Clicks",
|
||||
"Updated": "Updated",
|
||||
"Actions": "Actions",
|
||||
"Edit": "Edit",
|
||||
"Search by slug": "Search by slug",
|
||||
"Search by target": "Search by target",
|
||||
"Search by username": "Search by username",
|
||||
"Create short link": "Create short link",
|
||||
"Edit short link": "Edit short link",
|
||||
"Delete": "Delete",
|
||||
"Target URL": "Target URL",
|
||||
"Required": "Required",
|
||||
"Short Link": "Short Link",
|
||||
"A random url suffix": "A random url suffix",
|
||||
"Final url like": "Final url like",
|
||||
"Password": "Password",
|
||||
"Optional": "Optional",
|
||||
"If you want to protect your link": "If you want to protect your link",
|
||||
"Enter 6 character password": "Enter 6 character password",
|
||||
"Expiration time, default for never": "Expiration time, default for never",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"Update": "Update",
|
||||
"QR Code Design": "QR Code Design",
|
||||
"Preview": "Preview",
|
||||
"Download SVG": "Download SVG",
|
||||
"Download PNG": "Download PNG",
|
||||
"Download JPG": "Download JPG",
|
||||
"Url": "Url",
|
||||
"Logo": "Logo",
|
||||
"Custom Logo": "Custom Logo",
|
||||
"Front Color": "Front Color",
|
||||
"Background Color": "Background Color",
|
||||
"Display your logo in the center of the QR code": "Display your logo in the center of the QR code",
|
||||
"Learn more": "Learn more",
|
||||
"Customize your QR code logo": "Customize your QR code logo",
|
||||
"Please create a api key before use this feature": "Please create a api key before use this feature",
|
||||
"Learn more about": "Learn more about",
|
||||
"Create Api Key": "Create Api Key",
|
||||
"Show": "Show",
|
||||
"per page": "per page",
|
||||
"Total Subdomains": "Total Subdomains",
|
||||
"Subdomain List": "Subdomains",
|
||||
"Please read the": "Please read the",
|
||||
"Legitimacy review": "Legitimacy review",
|
||||
"before using": "before using",
|
||||
"See": "See",
|
||||
"examples": "examples",
|
||||
"for more usage": "for more usage",
|
||||
"Add Record": "Add Record",
|
||||
"Type": "Type",
|
||||
"Name": "Name",
|
||||
"Content": "Content",
|
||||
"TTL": "TTL",
|
||||
"Status": "Status",
|
||||
"Pending": "Pending",
|
||||
"The record is currently pending for admin approval": "The record is currently pending for admin approval",
|
||||
"The target is currently inaccessible": "The target is currently inaccessible",
|
||||
"Please check the target and try again": "Please check the target and try again",
|
||||
"If the target is not activated within 3 days": "If the target is not activated within 3 days",
|
||||
"the administrator will": "the administrator will",
|
||||
"delete this record": "delete this record",
|
||||
"Review": "Review",
|
||||
"No Subdomains": "No Subdomains",
|
||||
"No urls": "No urls",
|
||||
"Create record": "Create record",
|
||||
"Edit record": "Edit record",
|
||||
"The administrator has enabled application mode": "The administrator has enabled application mode",
|
||||
"After submission, you need to wait for administrator approval before the record takes effect": "After submission, you need to wait for administrator approval before the record takes effect",
|
||||
"What are you planning to use the subdomain for?": "What are you planning to use the subdomain for?",
|
||||
"At least 20 characters": "At least 20 characters",
|
||||
"User email": "User email",
|
||||
"Domain": "Domain",
|
||||
"No domains configured": "No domains configured",
|
||||
"Select a domain": "Select a domain",
|
||||
"IPv4 address": "IPv4 address",
|
||||
"Example": "E.g.",
|
||||
"Time To Live": "Time To Live",
|
||||
"Proxy": "Proxy",
|
||||
"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?",
|
||||
"Analytics": "Analytics",
|
||||
"Edit URL": "Edit URL"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "Dashboard",
|
||||
"Live Logs": "Live Logs",
|
||||
"Real-time logs of short link visits": "Real-time logs of short link visits",
|
||||
"Stop": "Stop",
|
||||
"Live": "Live",
|
||||
"Time": "Time",
|
||||
"Slug": "Slug",
|
||||
"Target": "Target",
|
||||
"Location": "Location",
|
||||
"Clicks": "Clicks",
|
||||
"of": "/",
|
||||
"total logs": "total logs",
|
||||
"API Reference": "API Reference",
|
||||
"provide a api for {target}": "provide a api for {target}",
|
||||
"creating short urls": "creating short urls",
|
||||
"View the usage tutorial": "View the usage tutorial",
|
||||
"document": "document",
|
||||
"Realtime Visits": "Realtime Visits",
|
||||
"See documentation": "See documentation",
|
||||
"Manage Short URLs": "Manage Short URLs",
|
||||
"List and manage short urls": "List and manage short urls",
|
||||
"Manage DNS Records": "Manage DNS Records",
|
||||
"List and manage records": "List and manage records",
|
||||
"Scraping API Overview": "Scraping API Overview",
|
||||
"Quickly extract valuable structured website data": "Quickly extract valuable structured website data",
|
||||
"Url to Screenshot": "Url to Screenshot",
|
||||
"Quickly extract website screenshots": "Quickly extract website screenshots",
|
||||
"extracting url as screenshot": "extracting url as screenshot",
|
||||
"Url to QR Code": "Url to QR Code",
|
||||
"Quickly extract website QR codes": "Quickly extract website QR codes",
|
||||
"extracting url as qrcode": "extracting url as qrcode",
|
||||
"Url to Meta Info": "Url to Meta Info",
|
||||
"extracting url as meta info": "extracting url as meta info",
|
||||
"Url to Markdown": "Url to Markdown",
|
||||
"Quickly extract website content and convert it to Markdown format": "Quickly extract website content and convert it to Markdown format",
|
||||
"extracting url as markdown": "extracting url as markdown",
|
||||
"extracting url as text": "extracting url as text",
|
||||
"Account Settings": "Account Settings",
|
||||
"Manage account and website settings": "Manage account and website settings",
|
||||
"Setup Guide": "Setup Guide",
|
||||
"Admin Panel": "Admin Panel",
|
||||
"Access only for users with ADMIN role": "Access only for users with ADMIN role",
|
||||
"Domains Management": "Domains Management",
|
||||
"List and manage domains": "List and manage domains",
|
||||
"User Management": "User Management",
|
||||
"List and manage all users": "List and manage all users",
|
||||
"Email box": "Email box",
|
||||
"monthly": "monthly",
|
||||
"total": "total",
|
||||
"Short URLs": "Short URLs",
|
||||
"DNS Records": "DNS Records",
|
||||
"Url to Text": "Url to Text",
|
||||
"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",
|
||||
"Emails": "Emails",
|
||||
"Inbox": "Inbox",
|
||||
"Users": "Users",
|
||||
"Total Requests of APIs in Last 30 Days": "Total Requests of APIs in Last 30 Days",
|
||||
"Last request from {latestFrom} api about {latestDate}": "Last request from {latestFrom} api about {latestDate}",
|
||||
"last-request-info": "Last request from {location} about <timeAgo></timeAgo>",
|
||||
"Requests": "Requests",
|
||||
"IP": "IP",
|
||||
"Date": "Date",
|
||||
"Type": "Type",
|
||||
"Link": "Link",
|
||||
"User": "User",
|
||||
"Data Increase": "Data Increase",
|
||||
"Showing data increase in": "Showing data increase in",
|
||||
"Records": "Records",
|
||||
"URLs": "URLs",
|
||||
"Sends": "Sends",
|
||||
"Link Analytics": "Link Analytics",
|
||||
"Last visitor from {latestFrom} about {latestDate}": "Last visitor from {latestFrom} about {latestDate}",
|
||||
"last-visitor-info": "Last visitor from {location} about <timeAgo></timeAgo>",
|
||||
"Views": "Views",
|
||||
"Visits": "Visits",
|
||||
"Referrers": "Referrers",
|
||||
"Traffic Type": "Traffic Type",
|
||||
"Country": "Country",
|
||||
"City": "City",
|
||||
"Browser": "Browser",
|
||||
"Engine": "Engine",
|
||||
"Language": "Language",
|
||||
"Region": "Region",
|
||||
"Device": "Device",
|
||||
"OS": "OS",
|
||||
"CPU": "CPU",
|
||||
"Visitors": "Visitors",
|
||||
"Name": "Name",
|
||||
"Activated Api Key users": "Activated Api Key users",
|
||||
"total-requests-one-type": "Total requests of {type1}",
|
||||
"total-requests-two-types": "Total requests of {type1} and {type2}"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "Settings",
|
||||
"Dashboard": "Dashboard",
|
||||
"deployWithVercel": "Deploy with",
|
||||
"now": "now!",
|
||||
"onePlatformPowers": "One platform powers",
|
||||
"endlessSolutions": "endless solutions",
|
||||
"platformDescription": "Link shortening, domain hosting, email manager and open api, everything you need to build better.",
|
||||
"documents": "Documents",
|
||||
"signInForFree": "Sign in for free",
|
||||
"exampleImageAlt": "example",
|
||||
"urlShorteningTitle": "URL Shortening",
|
||||
"urlShorteningDescription": "📖 Instantly transform long, unwieldy URLs into short, memorable links that are easy to share. Enjoy built-in analytics to track clicks, monitor performance, and gain insights into your audience—all in real time.",
|
||||
"freeSubdomainHostingTitle": "Free Subdomain Hosting",
|
||||
"freeSubdomainHostingDescription": "🎉 Kickstart your online presence with free, fully customizable subdomains. Whether you're launching a personal project or testing a business idea, get started quickly with no cost and reliable hosting you can trust.",
|
||||
"emailReceiversSendersTitle": "Email Receivers & Senders",
|
||||
"emailReceiversSendersDescription": "📧 Seamlessly receive and send emails from any email provider with top-notch security. Stay connected and manage your communications effortlessly, knowing your data is protected with robust encryption and privacy features.",
|
||||
"multipleDomainsTitle": "Multiple Domains",
|
||||
"multipleDomainsDescription": "🤩 Empower your business with the flexibility of multiple domains, such as wr.do, uv.do, and more. Establish a strong digital footprint, create branded links, or manage diverse projects—all under one unified platform.",
|
||||
"websiteScreenshotApiTitle": "Website Screenshot API",
|
||||
"websiteScreenshotApiDescription": "📷 Capture high-quality screenshots of any webpage instantly with our powerful Screenshot API. Integrate seamlessly into your applications, access third-party services, and unlock advanced features by applying your unique API key.",
|
||||
"metaInformationApiTitle": "Meta Information API",
|
||||
"metaInformationApiDescription": "🍥 Extract rich, structured web data effortlessly with our smart Meta Information API. Perfect for developers, businesses, or researchers, this tool offers seamless integration, third-party service access, and enhanced functionality.",
|
||||
"applyYourApiKey": "Apply your api key",
|
||||
"pricingTitle": "Pricing",
|
||||
"pricingDescription": "Use it for free for yourself, upgrade when your team needs advanced control.",
|
||||
"freeTier": "Free",
|
||||
"freePrice": "$0/mo",
|
||||
"freeBestFor": "For hobbyists and individuals looking to manage their links",
|
||||
"getStartedFree": "Get started free",
|
||||
"premiumTier": "Premium",
|
||||
"premiumPrice": "$5/mo",
|
||||
"premiumBestFor": "Best for 5-50 users",
|
||||
"getFreeTrial": "Get free trial",
|
||||
"enterpriseTier": "Enterprise",
|
||||
"enterprisePrice": "Contact us",
|
||||
"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",
|
||||
"Short Urls": "Short Urls",
|
||||
"Emails": "Emails",
|
||||
"DNS Records": "DNS Records",
|
||||
"WRoom": "WRoom",
|
||||
"OPEN API": "OPEN API",
|
||||
"Overview": "Overview",
|
||||
"Screenshot": "Screenshot",
|
||||
"QR Code": "QR Code",
|
||||
"Meta Info": "Meta Info",
|
||||
"Markdown": "Markdown",
|
||||
"ADMIN": "ADMIN",
|
||||
"Admin Panel": "Admin Panel",
|
||||
"Domains": "Domains",
|
||||
"Users": "Users",
|
||||
"URLs": "URLs",
|
||||
"Records": "Records",
|
||||
"OPTIONS": "OPTIONS",
|
||||
"Settings": "Settings",
|
||||
"Documentation": "Documentation",
|
||||
"Feedback": "Feedback",
|
||||
"Support": "Support",
|
||||
"Docs": "Docs",
|
||||
"Toggle theme": "Toggle theme",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"System": "System",
|
||||
"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",
|
||||
"Select all": "Select all",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
{
|
||||
"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": "管理短链",
|
||||
"Add URL": "创建短链",
|
||||
"Slug": "短链",
|
||||
"Target": "目标",
|
||||
"User": "用户",
|
||||
"Enabled": "启用",
|
||||
"Expiration": "有效期",
|
||||
"Clicks": "点击量",
|
||||
"Updated": "更新",
|
||||
"Actions": "操作",
|
||||
"Edit": "编辑",
|
||||
"Search by slug": "搜索短链",
|
||||
"Search by target": "搜索目标链接",
|
||||
"Search by username": "搜索用户名",
|
||||
"Create short link": "创建短链",
|
||||
"Edit short link": "编辑短链",
|
||||
"Delete": "删除",
|
||||
"Target URL": "目标链接",
|
||||
"Required": "必填",
|
||||
"Short Link": "短链",
|
||||
"A random url suffix": "短链后缀",
|
||||
"Final url like": "最终链接为",
|
||||
"Password": "访问密码",
|
||||
"Optional": "可选",
|
||||
"If you want to protect your link": "如果您想保护您的链接",
|
||||
"Enter 6 character password": "请输入6位密码",
|
||||
"Expiration time, default for never": "过期时间,默认为永不过期",
|
||||
"Save": "保存",
|
||||
"Cancel": "取消",
|
||||
"Update": "更新",
|
||||
"QR Code Design": "自定义二维码",
|
||||
"Preview": "预览",
|
||||
"Download SVG": "下载 SVG 图片",
|
||||
"Download PNG": "下载 PNG 图片",
|
||||
"Download JPG": "下载 JPG 图片",
|
||||
"Url": "链接",
|
||||
"Logo": "图标",
|
||||
"Custom Logo": "自定义图标",
|
||||
"Front Color": "前景色",
|
||||
"Background Color": "背景色",
|
||||
"Display your logo in the center of the QR code": "在二维码中央显示您的图标",
|
||||
"Learn more": "了解更多",
|
||||
"Customize your QR code logo": "自定义二维码图标",
|
||||
"Please create a api key before use this feature": "请在使用此功能之前创建一个 API 密钥",
|
||||
"Learn more about": "了解更多关于",
|
||||
"Create Api Key": "创建 API 密钥",
|
||||
"Show": "显示",
|
||||
"per page": "条/页",
|
||||
"Total Subdomains": "总计",
|
||||
"Subdomain List": "子域名列表",
|
||||
"Please read the": "请阅读",
|
||||
"legitimacy review": "链接合法性审查",
|
||||
"before using": "在使用之前",
|
||||
"See": "查看",
|
||||
"examples": "示例",
|
||||
"for more usage": "了解更多用法",
|
||||
"Add Record": "添加记录",
|
||||
"Type": "类型",
|
||||
"Name": "名称",
|
||||
"Content": "内容",
|
||||
"TTL": "TTL",
|
||||
"Status": "状态",
|
||||
"Pending": "审核中",
|
||||
"The record is currently pending for admin approval": "正在等待管理员审核",
|
||||
"The target is currently inaccessible": "目标链接目前无法访问",
|
||||
"Please check the target and try again": "请检查解析记录并重试",
|
||||
"If the target is not activated within 3 days": "如果目标链接在 3 天内依然无法访问",
|
||||
"the administrator will": "管理员将",
|
||||
"delete this record": "删除此记录",
|
||||
"Review": "审核",
|
||||
"No Subdomains": "暂无子域名",
|
||||
"No urls": "暂无短链接",
|
||||
"Create record": "创建记录",
|
||||
"Edit record": "编辑记录",
|
||||
"The administrator has enabled application mode": "管理员已启用 [用户申请 - 管理员审核] 模式",
|
||||
"After submission, you need to wait for administrator approval before the record takes effect": "提交后, 您需要等待管理员审核才能生效",
|
||||
"What are you planning to use the subdomain for?": "您计划使用此域名做什么?",
|
||||
"At least 20 characters": "至少 20 个字符",
|
||||
"User email": "用户邮箱",
|
||||
"Domain": "根域名",
|
||||
"No domains configured": "未配置域名",
|
||||
"Select a domain": "选择一个域名",
|
||||
"IPv4 address": "IPv4 地址",
|
||||
"Example": "例如",
|
||||
"Time To Live": "生效时间",
|
||||
"Proxy": "代理记录",
|
||||
"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 密钥?",
|
||||
"Analytics": "访客分析",
|
||||
"Edit URL": "编辑短链"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "用户面板",
|
||||
"Live Logs": "实时日志",
|
||||
"Real-time logs of short link visits": "展示短链实时访问日志",
|
||||
"Stop": "停止",
|
||||
"Live": "实时",
|
||||
"Time": "时间戳",
|
||||
"Slug": "短链",
|
||||
"Target": "目标",
|
||||
"Location": "位置",
|
||||
"Clicks": "点击量",
|
||||
"of": "/",
|
||||
"total logs": "条日志",
|
||||
"API Reference": "API 使用说明",
|
||||
"provide a api for {target}": "为 {target} 提供了 API",
|
||||
"creating short urls": "创建短链",
|
||||
"View the usage tutorial": "查看使用教程",
|
||||
"document": "文档",
|
||||
"Realtime Visits": "访问量",
|
||||
"See documentation": "查看相关文档",
|
||||
"Manage Short URLs": "管理短链",
|
||||
"List and manage short urls": "展示短链接列表并管理你的短链",
|
||||
"Manage DNS Records": "管理 DNS 记录",
|
||||
"List and manage records": "展示 DNS 记录列表并管理你的 DNS 记录",
|
||||
"Scraping API Overview": "Scraping API 概览",
|
||||
"Quickly extract valuable structured website data": "快速提取有价值的网站数据",
|
||||
"Url to Screenshot": "网址转截图",
|
||||
"Quickly extract website screenshots": "快速提取网站截图",
|
||||
"extracting url as screenshot": "提取 URL 为截图",
|
||||
"Url to QR Code": "网址转二维码",
|
||||
"Generate QR Code from URL": "从 URL 生成二维码",
|
||||
"extracting url as QR code": "提取 URL 为二维码",
|
||||
"Url to Meta Info": "网站元数据提取",
|
||||
"extracting url as meta info": "提取 URL 网址中的元数据",
|
||||
"Url to Markdown": "网页内容转 Markdown",
|
||||
"Quickly extract website content and convert it to Markdown format": "快速提取网站内容并将其转换为 Markdown 格式",
|
||||
"extracting url as markdown": "提取 URL 为 Markdown",
|
||||
"extracting url as text": "提取 URL 为文本",
|
||||
"Account Settings": "账户设置",
|
||||
"Manage account and website settings": "管理账户信息和网站设置",
|
||||
"Setup Guide": "初始化引导",
|
||||
"Admin Panel": "管理员面板",
|
||||
"Access only for users with ADMIN role": "仅管理员可访问",
|
||||
"Domains Management": "域名管理",
|
||||
"List and manage domains": "展示域名列表并管理你的域名服务",
|
||||
"User Management": "用户管理",
|
||||
"List and manage all users": "展示用户列表并管理所有用户",
|
||||
"Email box": "邮件箱",
|
||||
"monthly": "每月",
|
||||
"total": "总计",
|
||||
"Short URLs": "短链接",
|
||||
"DNS Records": "DNS 记录",
|
||||
"Url to Text": "网址转文本",
|
||||
"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 将网页内容转换为文本",
|
||||
"Emails": "邮箱",
|
||||
"Inbox": "收件箱",
|
||||
"Users": "用户",
|
||||
"Total Requests of APIs in Last 30 Days": "过去 30 天的 API 请求总量",
|
||||
"Last request from {latestFrom} api about {latestDate}": "最近的请求来自 {latestFrom} 的 API 于 {latestDate}",
|
||||
"last-request-info": "最近的请求来自 {location} 于 <timeAgo></timeAgo>",
|
||||
"Requests": "请求量",
|
||||
"IP": "IP 数",
|
||||
"Date": "日期",
|
||||
"Type": "类型",
|
||||
"Link": "链接",
|
||||
"User": "用户",
|
||||
"Data Increase": "数据增长",
|
||||
"Showing data increase in": "Showing data increase in",
|
||||
"Records": "子域名",
|
||||
"URLs": "短链接",
|
||||
"Sends": "发件箱",
|
||||
"Link Analytics": "访客分析",
|
||||
"Last visitor from {latestFrom} about {latestDate}": "最近的访客来自 {latestFrom} 于 {latestDate}",
|
||||
"last-visitor-info": "最后访问者来自 {location} 于 <timeAgo></timeAgo>",
|
||||
"Views": "浏览量",
|
||||
"Visits": "访问次数",
|
||||
"Referrers": "来源域名",
|
||||
"Traffic Type": "流量类型",
|
||||
"Country": "国家",
|
||||
"City": "城市",
|
||||
"Browser": "浏览器",
|
||||
"Engine": "引擎",
|
||||
"Language": "语言",
|
||||
"Region": "地区",
|
||||
"Device": "设备",
|
||||
"OS": "操作系统",
|
||||
"CPU": "CPU",
|
||||
"Visitors": "访客",
|
||||
"Name": "名称",
|
||||
"Activated Api Key users": "已启用 API Key 的用户",
|
||||
"total-requests-one-type": "Open API 的 {type1} 的总请求数",
|
||||
"total-requests-two-types": "Open API 的 {type1} 和 {type2} 的总请求数"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "设置",
|
||||
"Dashboard": "控制面板",
|
||||
"deployWithVercel": "使用",
|
||||
"now": "部署私有版本",
|
||||
"onePlatformPowers": " ",
|
||||
"endlessSolutions": "一站式域名服务平台",
|
||||
"platformDescription": "集成短链生成、子域名托管、无限邮箱服务,以及开放API接口,一站式域名管理解决方案,释放你的域名潜力",
|
||||
"documents": "参考文档",
|
||||
"signInForFree": "免费登录",
|
||||
"exampleImageAlt": "示例",
|
||||
"urlShorteningTitle": "短链生成器",
|
||||
"urlShorteningDescription": "📖 立即将冗长、复杂的URL转换为短小易记的链接,便于分享。享受内置的分析功能,实时跟踪点击量、监控性能并深入了解您的受众。支持设置密码保护、自定义二维码生成,支持调用API生成。",
|
||||
"freeSubdomainHostingTitle": "子域名托管",
|
||||
"freeSubdomainHostingDescription": "🎉 管理多 Cloudflare 账户下的多个域名的 DNS 记录,支持创建多种 DNS 记录类型(CNAME、A、TXT 等)。",
|
||||
"emailReceiversSendersTitle": "电子邮件接收与发送",
|
||||
"emailReceiversSendersDescription": "📧 创建无限个自定义前缀邮箱,接收无限制邮件,支持调用 API 创建邮箱、调用 API 获取收件箱邮件,支持过滤未读邮件列表。",
|
||||
"multipleDomainsTitle": "多域名支持",
|
||||
"multipleDomainsDescription": "🤩 通过多个域名的灵活性增强您的业务,例如wr.do、uv.do等。建立强大的数字足迹,创建品牌链接,或在一个统一平台下管理多样化项目。",
|
||||
"websiteScreenshotApiTitle": "网站截图API",
|
||||
"websiteScreenshotApiDescription": "📷 使用我们强大的截图API即时捕获任何网页的高质量截图。无缝集成到您的应用程序中,访问第三方服务,并通过申请您的唯一API密钥解锁功能。",
|
||||
"metaInformationApiTitle": "元信息API",
|
||||
"metaInformationApiDescription": "🍥 使用我们智能的元信息API轻松提取丰富、结构化的网页数据。适合开发者、商家或研究人员,此工具提供无缝集成、第三方服务访问和增强功能。",
|
||||
"applyYourApiKey": "申请您的API密钥",
|
||||
"pricingTitle": "定价",
|
||||
"pricingDescription": "免费为个人使用,当您的团队需要高级控制时可升级。",
|
||||
"freeTier": "免费体验",
|
||||
"freePrice": "$0/mo",
|
||||
"freeBestFor": "适合爱好者和需要管理链接的个人",
|
||||
"getStartedFree": "免费开始",
|
||||
"premiumTier": "高级",
|
||||
"premiumPrice": "$5/mo",
|
||||
"premiumBestFor": "最适合5-50人的团队",
|
||||
"getFreeTrial": "获取免费试用",
|
||||
"enterpriseTier": "企业计划",
|
||||
"enterprisePrice": "联系我们",
|
||||
"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": "控制台",
|
||||
"Short Urls": "短链接",
|
||||
"Emails": "邮件箱",
|
||||
"DNS Records": "子域名",
|
||||
"WRoom": "聊天室",
|
||||
"OPEN API": "开放API",
|
||||
"Overview": "概览面板",
|
||||
"Screenshot": "截图API",
|
||||
"QR Code": "二维码API",
|
||||
"Meta Info": "元数据API",
|
||||
"Markdown": "Markdown",
|
||||
"ADMIN": "管理员",
|
||||
"Admin Panel": "概览面板",
|
||||
"Domains": "域名管理",
|
||||
"Users": "用户管理",
|
||||
"URLs": "短链管理",
|
||||
"Records": "子域名管理",
|
||||
"OPTIONS": "选项",
|
||||
"Settings": "账户设置",
|
||||
"Documentation": "使用文档",
|
||||
"Feedback": "反馈",
|
||||
"Support": "联系站长",
|
||||
"Docs": "文档",
|
||||
"Toggle theme": "主题切换",
|
||||
"Light": "浅色",
|
||||
"Dark": "深色",
|
||||
"System": "跟随系统",
|
||||
"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": "更多",
|
||||
"Select all": "全部选中",
|
||||
"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>"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
const { withContentlayer } = require("next-contentlayer2");
|
||||
import { withContentlayer } from "next-contentlayer2";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
import nextPWA from "next-pwa";
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
import("./env.mjs");
|
||||
|
||||
@@ -113,9 +117,10 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const withPWA = require("next-pwa")({
|
||||
const withPWA = nextPWA({
|
||||
dest: "public",
|
||||
disable: false,
|
||||
});
|
||||
|
||||
module.exports = withContentlayer(withPWA(nextConfig));
|
||||
// module.exports = withContentlayer(withPWA(withNextIntl(nextConfig)));
|
||||
export default withContentlayer(withPWA(withNextIntl(nextConfig)));
|
||||
+6
-2
@@ -65,6 +65,8 @@
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-scale-chromatic": "^3.1.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/next-pwa": "^5.6.9",
|
||||
"@types/three": "^0.176.0",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@uiw/react-json-view": "2.0.0-alpha.26",
|
||||
@@ -73,8 +75,8 @@
|
||||
"@vercel/analytics": "^1.3.1",
|
||||
"@vercel/functions": "^1.4.0",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"chalk": "^4.1.1",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
@@ -96,6 +98,7 @@
|
||||
"next": "14.2.28",
|
||||
"next-auth": "5.0.0-beta.18",
|
||||
"next-contentlayer2": "^0.5.0",
|
||||
"next-intl": "^4.1.0",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
@@ -117,14 +120,15 @@
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"recharts": "^2.12.7",
|
||||
"resend": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.4",
|
||||
"shiki": "^1.11.0",
|
||||
"sonner": "^1.5.0",
|
||||
"swr": "^2.2.5",
|
||||
"semver": "^7.5.4",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.176.0",
|
||||
"timeago-react": "^3.0.7",
|
||||
"turndown": "^7.2.0",
|
||||
"ua-parser-js": "^1.0.38",
|
||||
"vaul": "^0.9.1",
|
||||
|
||||
Generated
+284
-31
@@ -128,6 +128,12 @@ importers:
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
'@types/ms':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
'@types/next-pwa':
|
||||
specifier: ^5.6.9
|
||||
version: 5.6.9(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@types/three':
|
||||
specifier: ^0.176.0
|
||||
version: 0.176.0
|
||||
@@ -221,6 +227,9 @@ importers:
|
||||
next-contentlayer2:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(contentlayer2@0.5.0(esbuild@0.19.11))(esbuild@0.19.11)(next@14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(next@14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.5.3)
|
||||
next-pwa:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0(@babel/core@7.24.5)(esbuild@0.19.11)(next@14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.90.3(@swc/core@1.3.101(@swc/helpers@0.5.5))(esbuild@0.19.11))
|
||||
@@ -308,6 +317,9 @@ importers:
|
||||
three:
|
||||
specifier: ^0.176.0
|
||||
version: 0.176.0
|
||||
timeago-react:
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7(react@18.3.1)
|
||||
turndown:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
@@ -1412,6 +1424,24 @@ packages:
|
||||
'@floating-ui/utils@0.1.6':
|
||||
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.4':
|
||||
resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||
resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==}
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||
resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.1':
|
||||
resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
|
||||
|
||||
'@grpc/grpc-js@1.10.6':
|
||||
resolution: {integrity: sha512-xP58G7wDQ4TCmN/cMUHh00DS7SRDv/+lC+xFLrTkMIN8h55X5NhZMLYbvy7dSELP15qlI6hPhNCRWVMtZMwqLA==}
|
||||
engines: {node: '>=12.10.0'}
|
||||
@@ -1477,84 +1507,72 @@ packages:
|
||||
engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.2':
|
||||
resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==}
|
||||
engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.0.2':
|
||||
resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==}
|
||||
engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.2':
|
||||
resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==}
|
||||
engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.2':
|
||||
resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==}
|
||||
engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.2':
|
||||
resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==}
|
||||
engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.4':
|
||||
resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==}
|
||||
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.33.4':
|
||||
resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==}
|
||||
engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.33.4':
|
||||
resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==}
|
||||
engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.33.4':
|
||||
resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==}
|
||||
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.4':
|
||||
resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==}
|
||||
engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.4':
|
||||
resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==}
|
||||
engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.33.4':
|
||||
resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==}
|
||||
@@ -1685,6 +1703,12 @@ packages:
|
||||
'@next/eslint-plugin-next@14.2.5':
|
||||
resolution: {integrity: sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==}
|
||||
|
||||
'@next/swc-darwin-arm64@13.5.9':
|
||||
resolution: {integrity: sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-arm64@14.1.4':
|
||||
resolution: {integrity: sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -1697,6 +1721,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@13.5.9':
|
||||
resolution: {integrity: sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@14.1.4':
|
||||
resolution: {integrity: sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -1709,61 +1739,83 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@13.5.9':
|
||||
resolution: {integrity: sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@14.1.4':
|
||||
resolution: {integrity: sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@14.2.28':
|
||||
resolution: {integrity: sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@13.5.9':
|
||||
resolution: {integrity: sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.1.4':
|
||||
resolution: {integrity: sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.2.28':
|
||||
resolution: {integrity: sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@13.5.9':
|
||||
resolution: {integrity: sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.1.4':
|
||||
resolution: {integrity: sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.2.28':
|
||||
resolution: {integrity: sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@13.5.9':
|
||||
resolution: {integrity: sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@14.1.4':
|
||||
resolution: {integrity: sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-musl@14.2.28':
|
||||
resolution: {integrity: sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@13.5.9':
|
||||
resolution: {integrity: sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@14.1.4':
|
||||
resolution: {integrity: sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==}
|
||||
@@ -1777,6 +1829,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-ia32-msvc@13.5.9':
|
||||
resolution: {integrity: sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-ia32-msvc@14.1.4':
|
||||
resolution: {integrity: sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -1789,6 +1847,12 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@13.5.9':
|
||||
resolution: {integrity: sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@14.1.4':
|
||||
resolution: {integrity: sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -2934,6 +2998,9 @@ packages:
|
||||
resolution: {integrity: sha512-DelRK+56UBUEoRr8pQGwTE7oyC019pExis6iG6zZPcjuz4nLbMB5LIosmclnsvajfIBEQiD1Dv9JI/MgnPwygQ==}
|
||||
engines: {node: '>=20.x'}
|
||||
|
||||
'@schummar/icu-type-parser@1.21.5':
|
||||
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
@@ -2974,28 +3041,24 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.3.101':
|
||||
resolution: {integrity: sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.3.101':
|
||||
resolution: {integrity: sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.3.101':
|
||||
resolution: {integrity: sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.3.101':
|
||||
resolution: {integrity: sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==}
|
||||
@@ -3262,8 +3325,11 @@ packages:
|
||||
'@types/minimatch@5.1.2':
|
||||
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
||||
|
||||
'@types/ms@0.7.34':
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/next-pwa@5.6.9':
|
||||
resolution: {integrity: sha512-KcymH+MtFYB5KVKIOH1DMqd0wUb8VLCxzHtsaRQQ7S8sGOaTH24Lo2vGZf6/0Ok9e+xWCKhqsSt6cgDJTk91Iw==}
|
||||
|
||||
'@types/node@20.14.11':
|
||||
resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==}
|
||||
@@ -4340,6 +4406,9 @@ packages:
|
||||
decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
|
||||
decimal.js@10.5.0:
|
||||
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
|
||||
@@ -5232,6 +5301,9 @@ packages:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||
|
||||
invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
|
||||
@@ -5979,6 +6051,10 @@ packages:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
@@ -6006,6 +6082,16 @@ packages:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
next-intl@4.1.0:
|
||||
resolution: {integrity: sha512-JNJRjc7sdnfUxhZmGcvzDszZ60tQKrygV/VLsgzXhnJDxQPn1cN2rVpc53adA1SvBJwPK2O6Sc6b4gYSILjCzw==}
|
||||
peerDependencies:
|
||||
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||
typescript: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
next-pwa@5.6.0:
|
||||
resolution: {integrity: sha512-XV8g8C6B7UmViXU8askMEYhWwQ4qc/XqJGnexbLV68hzKaGHZDMtHsm2TNxFcbR7+ypVuth/wwpiIlMwpRJJ5A==}
|
||||
peerDependencies:
|
||||
@@ -6031,6 +6117,21 @@ packages:
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
|
||||
next@13.5.11:
|
||||
resolution: {integrity: sha512-WUPJ6WbAX9tdC86kGTu92qkrRdgRqVrY++nwM+shmWQwmyxt4zhZfR59moXSI4N8GDYCBY3lIAqhzjDd4rTC8Q==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
next@14.1.4:
|
||||
resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
@@ -7350,6 +7451,14 @@ packages:
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
timeago-react@3.0.7:
|
||||
resolution: {integrity: sha512-5LSQuq+mzfEpMHkJQgMWnOs27dGh25aUQhffQpAW6q431vVVmHP194KYsM4bhzli0XfohUgN8+r3Pq6GVprN4A==}
|
||||
peerDependencies:
|
||||
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
timeago.js@4.0.2:
|
||||
resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
@@ -7570,6 +7679,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
use-intl@4.1.0:
|
||||
resolution: {integrity: sha512-mQvDYFvoGn+bm/PWvlQOtluKCknsQ5a9F1Cj0hMfBjMBVTwnOqLPd6srhjvVdEQEQFVyHM1PfyifKqKYb11M9Q==}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||
|
||||
use-isomorphic-layout-effect@1.1.2:
|
||||
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
|
||||
peerDependencies:
|
||||
@@ -8593,7 +8707,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/types': 7.24.6
|
||||
'@babel/types': 7.26.0
|
||||
esutils: 2.0.3
|
||||
|
||||
'@babel/runtime@7.23.2':
|
||||
@@ -9069,6 +9183,36 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.1.6': {}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.4':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/intl-localematcher': 0.6.1
|
||||
decimal.js: 10.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
'@formatjs/icu-skeleton-parser': 1.8.14
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@grpc/grpc-js@1.10.6':
|
||||
dependencies:
|
||||
'@grpc/proto-loader': 0.7.12
|
||||
@@ -9317,54 +9461,81 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 10.3.10
|
||||
|
||||
'@next/swc-darwin-arm64@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-arm64@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-arm64@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-ia32-msvc@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-ia32-msvc@14.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-ia32-msvc@14.2.28':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@13.5.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@14.1.4':
|
||||
optional: true
|
||||
|
||||
@@ -10764,9 +10935,11 @@ snapshots:
|
||||
'@rollup/plugin-babel@5.3.1(@babel/core@7.24.5)(rollup@2.79.2)':
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-module-imports': 7.24.6
|
||||
'@babel/helper-module-imports': 7.25.9
|
||||
'@rollup/pluginutils': 3.1.0(rollup@2.79.2)
|
||||
rollup: 2.79.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rollup/plugin-node-resolve@11.2.1(rollup@2.79.2)':
|
||||
dependencies:
|
||||
@@ -10795,6 +10968,8 @@ snapshots:
|
||||
|
||||
'@scaleway/random-name@5.1.1': {}
|
||||
|
||||
'@schummar/icu-type-parser@1.21.5': {}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
@@ -11075,7 +11250,7 @@ snapshots:
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 0.7.34
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
@@ -11140,7 +11315,24 @@ snapshots:
|
||||
|
||||
'@types/minimatch@5.1.2': {}
|
||||
|
||||
'@types/ms@0.7.34': {}
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/next-pwa@5.6.9(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@types/node': 20.14.11
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
next: 13.5.11(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
workbox-build: 6.6.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@opentelemetry/api'
|
||||
- '@types/babel__core'
|
||||
- babel-plugin-macros
|
||||
- react
|
||||
- react-dom
|
||||
- sass
|
||||
- supports-color
|
||||
|
||||
'@types/node@20.14.11':
|
||||
dependencies:
|
||||
@@ -12365,6 +12557,8 @@ snapshots:
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
decimal.js@10.5.0: {}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
@@ -13537,6 +13731,13 @@ snapshots:
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||
tslib: 2.8.1
|
||||
|
||||
invariant@2.2.4:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -14526,6 +14727,8 @@ snapshots:
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next-auth@5.0.0-beta.18(next@14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.14)(react@18.3.1):
|
||||
@@ -14550,6 +14753,16 @@ snapshots:
|
||||
- markdown-wasm
|
||||
- supports-color
|
||||
|
||||
next-intl@4.1.0(next@14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.5.3):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.10
|
||||
negotiator: 1.0.0
|
||||
next: 14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
use-intl: 4.1.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.3
|
||||
|
||||
next-pwa@5.6.0(@babel/core@7.24.5)(esbuild@0.19.11)(next@14.2.28(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.90.3(@swc/core@1.3.101(@swc/helpers@0.5.5))(esbuild@0.19.11)):
|
||||
dependencies:
|
||||
babel-loader: 8.4.1(@babel/core@7.24.5)(webpack@5.90.3(@swc/core@1.3.101(@swc/helpers@0.5.5))(esbuild@0.19.11))
|
||||
@@ -14587,6 +14800,32 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next@13.5.11(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 13.5.11
|
||||
'@swc/helpers': 0.5.2
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001707
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.3.1)
|
||||
watchpack: 2.4.0
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 13.5.9
|
||||
'@next/swc-darwin-x64': 13.5.9
|
||||
'@next/swc-linux-arm64-gnu': 13.5.9
|
||||
'@next/swc-linux-arm64-musl': 13.5.9
|
||||
'@next/swc-linux-x64-gnu': 13.5.9
|
||||
'@next/swc-linux-x64-musl': 13.5.9
|
||||
'@next/swc-win32-arm64-msvc': 13.5.9
|
||||
'@next/swc-win32-ia32-msvc': 13.5.9
|
||||
'@next/swc-win32-x64-msvc': 13.5.9
|
||||
'@opentelemetry/api': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@14.1.4(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 14.1.4
|
||||
@@ -15566,7 +15805,7 @@ snapshots:
|
||||
|
||||
rollup-plugin-terser@7.0.2(rollup@2.79.2):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.24.6
|
||||
'@babel/code-frame': 7.26.2
|
||||
jest-worker: 26.6.2
|
||||
rollup: 2.79.2
|
||||
serialize-javascript: 4.0.0
|
||||
@@ -16171,6 +16410,13 @@ snapshots:
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
timeago-react@3.0.7(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
timeago.js: 4.0.2
|
||||
|
||||
timeago.js@4.0.2: {}
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
@@ -16395,6 +16641,13 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
use-intl@4.1.0(react@18.3.1):
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@schummar/icu-type-parser': 1.21.5
|
||||
intl-messageformat: 10.7.16
|
||||
react: 18.3.1
|
||||
|
||||
use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
+4
-43
@@ -1,46 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
<url><loc>https://wr.do/api/feature</loc><lastmod>2025-06-04T09:40:19.516Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/pricing</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/privacy</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/terms</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker-zh</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/deploy</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/deploy-zh</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/email-zh</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/installation-zh</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/quick-start-zh</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/chat</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-06-04T09:40:19.517Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/api/feature</loc><lastmod>2025-06-07T14:03:50.540Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-06-07T14:03:50.541Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-06-07T14:03:50.541Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-06-07T14:03:50.541Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
</urlset>
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-1
@@ -32,7 +32,8 @@
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".contentlayer/generated",
|
||||
"contentlayer.config.ts"
|
||||
"contentlayer.config.ts",
|
||||
"next.config.mjs"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user