Improved user interface translations and clarity in Simplified
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 {
|
||||
@@ -35,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">;
|
||||
@@ -291,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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -250,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
|
||||
|
||||
@@ -10,7 +10,7 @@ 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";
|
||||
@@ -43,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">;
|
||||
@@ -341,7 +342,9 @@ 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 ? (
|
||||
|
||||
@@ -20,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) && (
|
||||
@@ -42,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,6 +3,7 @@
|
||||
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";
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import CountUp from "@/components/dashboard/count-up";
|
||||
|
||||
const chartConfig = {
|
||||
request: {
|
||||
@@ -105,14 +105,19 @@ export function DailyPVUVChart({ data }: { data: ScrapeMeta[] }) {
|
||||
const latestDate = timeAgo(latestEntry.updatedAt);
|
||||
const latestFrom = latestEntry.type;
|
||||
|
||||
const t = useTranslations("Components");
|
||||
|
||||
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>
|
||||
<CardTitle>{t("Total Requests of APIs in Last 30 Days")}</CardTitle>
|
||||
<CardDescription>
|
||||
Last request from <strong>{latestFrom}</strong> api about{" "}
|
||||
{latestDate}.
|
||||
{t("Last request from {latestFrom} api about {latestDate}", {
|
||||
latestFrom,
|
||||
latestDate,
|
||||
})}
|
||||
.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex">
|
||||
@@ -125,8 +130,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])}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
@@ -175,6 +176,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,
|
||||
@@ -242,9 +245,13 @@ export function DailyPVUVChart({
|
||||
<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>
|
||||
<CardTitle>{t("Link Analytics")}</CardTitle>
|
||||
<CardDescription>
|
||||
Last visitor from {latestFrom} about {latestDate}.
|
||||
{t("Last visitor from {latestFrom} about {latestDate}", {
|
||||
latestFrom,
|
||||
latestDate,
|
||||
})}
|
||||
.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@@ -291,8 +298,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 +410,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 +427,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 +444,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 +462,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 +479,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 +504,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`}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
fetcher,
|
||||
nFormatter,
|
||||
removeUrlSuffix,
|
||||
timeAgo,
|
||||
} from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -58,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";
|
||||
@@ -396,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
|
||||
@@ -563,7 +563,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
}}
|
||||
>
|
||||
<Icons.lineChart className="size-4" />
|
||||
Analytics
|
||||
{t("Analytics")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -580,7 +580,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
}}
|
||||
>
|
||||
<PenLine className="size-4" />
|
||||
Edit URL
|
||||
{t("Edit URL")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -614,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}
|
||||
|
||||
@@ -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])}
|
||||
|
||||
@@ -7,11 +7,12 @@ 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";
|
||||
@@ -278,7 +279,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" />
|
||||
|
||||
@@ -17,13 +17,14 @@ 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";
|
||||
@@ -539,7 +540,7 @@ export default function EmailSidebar({
|
||||
{isAdminModel
|
||||
? `Created by ${email.user || email.email.slice(0, 5)} at`
|
||||
: ""}{" "}
|
||||
{timeAgo(email.createdAt)}
|
||||
<TimeAgoIntl date={email.createdAt} />
|
||||
</span>
|
||||
</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} />
|
||||
);
|
||||
}
|
||||
+37
-2
@@ -137,7 +137,9 @@
|
||||
"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?"
|
||||
"How to get resend api key?": "How to get resend api key?",
|
||||
"Analytics": "Analytics",
|
||||
"Edit URL": "Edit URL"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -198,7 +200,40 @@
|
||||
"Convert website content to text": "Convert website content to text",
|
||||
"Emails": "Emails",
|
||||
"Inbox": "Inbox",
|
||||
"Users": "Users"
|
||||
"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}",
|
||||
"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}",
|
||||
"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",
|
||||
|
||||
+37
-2
@@ -137,7 +137,9 @@
|
||||
"Associate with 'Email Service' status": "与 '邮件服务' 启用状态关联",
|
||||
"API Key": "API 密钥",
|
||||
"send email service": "用于发送邮件服务",
|
||||
"How to get resend api key?": "如何获取 Resend API 密钥?"
|
||||
"How to get resend api key?": "如何获取 Resend API 密钥?",
|
||||
"Analytics": "访客分析",
|
||||
"Edit URL": "编辑短链"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "用户面板",
|
||||
@@ -198,7 +200,40 @@
|
||||
"Convert website content to text": "使用 API 将网页内容转换为文本",
|
||||
"Emails": "邮箱",
|
||||
"Inbox": "收件箱",
|
||||
"Users": "用户"
|
||||
"Users": "用户",
|
||||
"Total Requests of APIs in Last 30 Days": "过去 30 天的 API 请求总量",
|
||||
"Last request from {latestFrom} api about {latestDate}": "最近的请求来自 {latestFrom} 的 API 于 {latestDate}",
|
||||
"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}",
|
||||
"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": "设置",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"@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",
|
||||
@@ -127,6 +128,7 @@
|
||||
"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
+25
-4
@@ -128,6 +128,9 @@ 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)
|
||||
@@ -314,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
|
||||
@@ -3319,8 +3325,8 @@ 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==}
|
||||
@@ -7445,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==}
|
||||
|
||||
@@ -11236,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:
|
||||
@@ -11301,7 +11315,7 @@ 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:
|
||||
@@ -16396,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: {}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user