feats: add url status componet
This commit is contained in:
@@ -3,9 +3,9 @@ import { redirect } from "next/navigation";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import { UserRecordStatus } from "@/components/dashboard/status-card";
|
||||
|
||||
import UserRecordsList from "../../dashboard/records/record-list";
|
||||
import UserRecordStatus from "../../dashboard/records/record-statu";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "DNS Records",
|
||||
@@ -25,16 +25,7 @@ export default async function DashboardPage() {
|
||||
link="/docs/dns-records"
|
||||
linkText="DNS records"
|
||||
/>
|
||||
<UserRecordStatus
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
apiKey: user.apiKey || "",
|
||||
email: user.email || "",
|
||||
role: user.role,
|
||||
}}
|
||||
action="/api/record/admin"
|
||||
/>
|
||||
<UserRecordStatus action="/api/record/admin" />
|
||||
<UserRecordsList
|
||||
user={{
|
||||
id: user.id,
|
||||
|
||||
@@ -8,7 +8,12 @@ export default function DashboardUrlsLoading() {
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ export default async function DashboardPage() {
|
||||
link="/docs/short-urls"
|
||||
linkText="short urls"
|
||||
/>
|
||||
|
||||
<UserUrlsList
|
||||
user={{
|
||||
id: user.id,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { redirect } from "next/navigation";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import { UserRecordStatus } from "@/components/dashboard/status-card";
|
||||
|
||||
import UserRecordsList from "./record-list";
|
||||
import UserRecordStatus from "./record-statu";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "DNS Records",
|
||||
@@ -25,16 +25,7 @@ export default async function DashboardPage() {
|
||||
link="/docs/dns-records"
|
||||
linkText="DNS records"
|
||||
/>
|
||||
<UserRecordStatus
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
apiKey: user.apiKey || "",
|
||||
email: user.email || "",
|
||||
role: user.role,
|
||||
}}
|
||||
action="/api/record"
|
||||
/>
|
||||
<UserRecordStatus action="/api/record" />
|
||||
<UserRecordsList
|
||||
user={{
|
||||
id: user.id,
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { User } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { fetcher, nFormatter } from "@/lib/utils";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import CountUp from "@/components/dashboard/count-up";
|
||||
|
||||
export interface RecordStatusProps {
|
||||
user: Pick<User, "id" | "name" | "apiKey" | "email" | "role">;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export default function UserRecordStatus({ action }: RecordStatusProps) {
|
||||
const { data, isLoading, error } = useSWR<Record<string, number>>(
|
||||
`${action}/status`,
|
||||
fetcher,
|
||||
);
|
||||
|
||||
if (isLoading || error)
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
data && (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<StatuInfoCard
|
||||
title="Active"
|
||||
total={data["total"]}
|
||||
monthTotal={data["active"]}
|
||||
/>
|
||||
<StatuInfoCard
|
||||
title="Inactive"
|
||||
total={data["total"]}
|
||||
monthTotal={data["inactive"]}
|
||||
/>
|
||||
<StatuInfoCard
|
||||
title="Pending"
|
||||
total={data["total"]}
|
||||
monthTotal={data["pending"]}
|
||||
/>
|
||||
<StatuInfoCard
|
||||
title="Rejected"
|
||||
total={data["total"]}
|
||||
monthTotal={data["rejected"]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function StatuInfoCard({
|
||||
title,
|
||||
total,
|
||||
monthTotal,
|
||||
}: {
|
||||
title: string;
|
||||
total?: number;
|
||||
monthTotal: number;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const statusConfig = {
|
||||
Active: {
|
||||
color: "bg-green-500",
|
||||
textColor: "text-green-500",
|
||||
gradient: "from-green-400 to-green-600",
|
||||
shadow: "shadow-green-500/25",
|
||||
},
|
||||
Inactive: {
|
||||
color: "bg-gray-700",
|
||||
textColor: "text-gray-700",
|
||||
gradient: "from-gray-400 to-gray-600",
|
||||
shadow: "shadow-gray-500/25",
|
||||
},
|
||||
Pending: {
|
||||
color: "bg-yellow-500",
|
||||
textColor: "text-yellow-500",
|
||||
gradient: "from-yellow-400 to-yellow-600",
|
||||
shadow: "shadow-yellow-500/25",
|
||||
},
|
||||
Rejected: {
|
||||
color: "bg-red-500",
|
||||
textColor: "text-red-500",
|
||||
gradient: "from-red-400 to-red-600",
|
||||
shadow: "shadow-red-500/25",
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[title];
|
||||
const percentage = total && total > 0 ? (monthTotal / total) * 100 : 0;
|
||||
const barHeight = Math.max(8, Math.min(100, percentage));
|
||||
|
||||
return (
|
||||
<Card className="grids group relative animate-fade-in overflow-hidden bg-gray-50/70 p-4 backdrop-blur-lg transition-all duration-300 hover:shadow-lg dark:bg-primary-foreground">
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{t(title)}</div>
|
||||
<div className="mt-4 flex items-end gap-2 text-2xl font-bold">
|
||||
<CountUp count={monthTotal} />
|
||||
<p className="align-top text-base text-slate-500">
|
||||
/ {nFormatter(total || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex h-16 items-end">
|
||||
<div className="relative flex flex-col items-center">
|
||||
{/* 柱状图背景 */}
|
||||
<div className="relative h-14 w-7 overflow-hidden rounded-lg border border-gray-300/20 bg-gray-200/50 backdrop-blur-sm dark:border-gray-600/20 dark:bg-gray-700/50">
|
||||
{/* 主柱状图 */}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t ${config.gradient} rounded-lg transition-all duration-700 ease-out ${config.shadow}`}
|
||||
style={{ height: `${barHeight}%` }}
|
||||
/>
|
||||
{/* 光泽效果 */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-1/3 rounded-lg bg-gradient-to-t from-white/30 to-white/10 transition-all duration-700 ease-out"
|
||||
style={{ height: `${barHeight}%` }}
|
||||
/>
|
||||
{/* 顶部高光 */}
|
||||
{barHeight > 15 && (
|
||||
<div
|
||||
className="absolute left-0 right-0 h-1 rounded-t-lg bg-white/40"
|
||||
style={{ top: `${100 - barHeight}%` }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`absolute left-0 top-0.5 scale-75 text-xs font-semibold ${config.textColor}`}
|
||||
>
|
||||
{percentage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,12 @@ export default function DashboardUrlsLoading() {
|
||||
heading="Manage Short URLs"
|
||||
text="List and manage short urls"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { UrlStatus } from "@/components/dashboard/status-card";
|
||||
import { FormType } from "@/components/forms/record-form";
|
||||
import { UrlForm } from "@/components/forms/url-form";
|
||||
import ApiReference from "@/components/shared/api-reference";
|
||||
@@ -725,11 +726,13 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
</div>
|
||||
|
||||
<TabsContent className="space-y-3" value="List">
|
||||
<UrlStatus action={action} />
|
||||
{rendeSeachInputs()}
|
||||
{rendeList()}
|
||||
{rendLogs()}
|
||||
</TabsContent>
|
||||
<TabsContent className="space-y-3" value="Grid">
|
||||
<UrlStatus action={action} />
|
||||
{rendeSeachInputs()}
|
||||
{rendeGrid()}
|
||||
{rendLogs()}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getUrlStatusOptimized } from "@/lib/dto/short-urls";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
if (user.role !== "ADMIN") {
|
||||
return Response.json("Unauthorized", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const status = await getUrlStatusOptimized(user.id, "ADMIN");
|
||||
|
||||
return Response.json(status);
|
||||
} catch (error) {
|
||||
return Response.json(error?.statusText || error, {
|
||||
status: error.status || 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getUrlStatusOptimized } from "@/lib/dto/short-urls";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
|
||||
const status = await getUrlStatusOptimized(user.id, "USER");
|
||||
|
||||
return Response.json(status);
|
||||
} catch (error) {
|
||||
return Response.json(error?.statusText || error, {
|
||||
status: error.status || 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,16 @@ import CountUp from "react-countup";
|
||||
|
||||
import { nFormatter } from "@/lib/utils";
|
||||
|
||||
export default function CountUpFn({ count }: { count: number }) {
|
||||
export default function CountUpFn({
|
||||
count,
|
||||
className,
|
||||
}: {
|
||||
count: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<CountUp
|
||||
className={className}
|
||||
end={count}
|
||||
duration={2}
|
||||
redraw={false}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { fetcher, nFormatter } from "@/lib/utils";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export interface StatusConfig {
|
||||
[key: string]: {
|
||||
color: string;
|
||||
textColor: string;
|
||||
gradient: string;
|
||||
shadow: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 用户记录状态配置
|
||||
export const USER_RECORD_STATUS_CONFIG: StatusConfig = {
|
||||
Active: {
|
||||
color: "bg-green-500",
|
||||
textColor: "text-green-500",
|
||||
gradient: "from-green-400 to-green-600",
|
||||
shadow: "shadow-green-500/25",
|
||||
},
|
||||
Inactive: {
|
||||
color: "bg-gray-700",
|
||||
textColor: "text-gray-700",
|
||||
gradient: "from-gray-400 to-gray-600",
|
||||
shadow: "shadow-gray-500/25",
|
||||
},
|
||||
Pending: {
|
||||
color: "bg-yellow-500",
|
||||
textColor: "text-yellow-500",
|
||||
gradient: "from-yellow-400 to-yellow-600",
|
||||
shadow: "shadow-yellow-500/25",
|
||||
},
|
||||
Rejected: {
|
||||
color: "bg-red-500",
|
||||
textColor: "text-red-500",
|
||||
gradient: "from-red-400 to-red-600",
|
||||
shadow: "shadow-red-500/25",
|
||||
},
|
||||
};
|
||||
|
||||
// 短链状态配置
|
||||
export const URL_STATUS_CONFIG: StatusConfig = {
|
||||
Actived: {
|
||||
color: "bg-green-500",
|
||||
textColor: "text-green-500",
|
||||
gradient: "from-green-400 to-green-600",
|
||||
shadow: "shadow-green-500/25",
|
||||
},
|
||||
Disabled: {
|
||||
color: "bg-gray-600",
|
||||
textColor: "text-gray-600",
|
||||
gradient: "from-gray-400 to-gray-600",
|
||||
shadow: "shadow-gray-500/25",
|
||||
},
|
||||
Expired: {
|
||||
color: "bg-red-500",
|
||||
textColor: "text-red-500",
|
||||
gradient: "from-red-400 to-red-600",
|
||||
shadow: "shadow-red-500/25",
|
||||
},
|
||||
PasswordProtected: {
|
||||
color: "bg-blue-500",
|
||||
textColor: "text-blue-500",
|
||||
gradient: "from-blue-400 to-blue-600",
|
||||
shadow: "shadow-blue-500/25",
|
||||
},
|
||||
};
|
||||
|
||||
// 用户记录状态组件
|
||||
export function UserRecordStatus({ action }: { action: string }) {
|
||||
return (
|
||||
<GenericStatusDashboard
|
||||
apiEndpoint={action}
|
||||
statusItems={["Active", "Inactive", "Pending", "Rejected"]}
|
||||
statusConfig={USER_RECORD_STATUS_CONFIG}
|
||||
gridCols={4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 短链状态组件
|
||||
export function UrlStatus({ action }: { action: string }) {
|
||||
return (
|
||||
<GenericStatusDashboard
|
||||
apiEndpoint={action}
|
||||
statusItems={["Actived", "Disabled", "Expired", "PasswordProtected"]}
|
||||
statusConfig={URL_STATUS_CONFIG}
|
||||
gridCols={4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GenericStatusDashboard({
|
||||
apiEndpoint,
|
||||
statusItems,
|
||||
statusConfig,
|
||||
gridCols = 4,
|
||||
}: {
|
||||
apiEndpoint: string;
|
||||
statusItems: string[];
|
||||
statusConfig: StatusConfig;
|
||||
gridCols?: number;
|
||||
}) {
|
||||
const { data, isLoading, error } = useSWR<Record<string, number>>(
|
||||
`${apiEndpoint}/status`,
|
||||
fetcher,
|
||||
);
|
||||
|
||||
const gridClasses =
|
||||
{
|
||||
2: "grid-cols-2 sm:grid-cols-2 lg:grid-cols-2",
|
||||
3: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-3",
|
||||
4: "grid-cols-2 sm:grid-cols-4 lg:grid-cols-4",
|
||||
5: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5",
|
||||
}[gridCols] || "grid-cols-2 sm:grid-cols-4 lg:grid-cols-4";
|
||||
|
||||
if (isLoading || error) {
|
||||
return (
|
||||
<div className={`grid gap-4 ${gridClasses}`}>
|
||||
{statusItems.map((_, index) => (
|
||||
<Skeleton key={index} className="h-[102px] w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
data && (
|
||||
<div className={`grid gap-4 ${gridClasses}`}>
|
||||
{statusItems.map((status) => (
|
||||
<StatusInfoCard
|
||||
key={status}
|
||||
title={status}
|
||||
total={data["total"]}
|
||||
currentTotal={data[status.toLowerCase()]}
|
||||
statusConfig={statusConfig}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function StatusInfoCard({
|
||||
title,
|
||||
total,
|
||||
currentTotal,
|
||||
statusConfig,
|
||||
}: {
|
||||
title: string;
|
||||
total?: number;
|
||||
currentTotal: number;
|
||||
statusConfig: StatusConfig;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const config = statusConfig[title];
|
||||
const percentage =
|
||||
total && total > 0 && currentTotal > 0 ? (currentTotal / total) * 100 : 0;
|
||||
const barHeight = Math.max(8, Math.min(100, percentage));
|
||||
|
||||
return (
|
||||
<Card className="grids group relative animate-fade-in overflow-hidden bg-gray-50/70 p-4 backdrop-blur-lg transition-all duration-300 hover:shadow-lg dark:bg-primary-foreground">
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{t(title)}</div>
|
||||
<div className="mt-4 flex items-center gap-2 text-xl font-bold">
|
||||
{nFormatter(currentTotal || 0)}
|
||||
<p className="mt-1.5 text-xs text-slate-500">
|
||||
/ {nFormatter(total || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex h-16 items-end">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<div className="relative h-14 w-7 overflow-hidden rounded-lg border border-gray-300/20 bg-gray-200/50 backdrop-blur-sm dark:border-gray-600/20 dark:bg-gray-700/50">
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t ${config.gradient} rounded-lg transition-all duration-700 ease-out ${config.shadow}`}
|
||||
style={{ height: `${barHeight}%` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-1/3 rounded-lg bg-gradient-to-t from-white/30 to-white/10 transition-all duration-700 ease-out"
|
||||
style={{ height: `${barHeight}%` }}
|
||||
/>
|
||||
{barHeight > 15 && (
|
||||
<div
|
||||
className="absolute left-0 right-0 h-1 rounded-t-lg bg-white/40"
|
||||
style={{ top: `${100 - barHeight}%` }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`absolute left-0 top-0.5 scale-75 text-xs font-semibold ${config.textColor}`}
|
||||
>
|
||||
{percentage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { UrlMeta, UserRole } from "@prisma/client";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
import { EXPIRATION_ENUMS } from "../enums";
|
||||
import { getStartDate } from "../utils";
|
||||
|
||||
export interface ShortUrlFormData {
|
||||
@@ -154,6 +155,93 @@ export async function getUrlClicksByIds(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUrlStatus(userId: string, role: UserRole = "USER") {
|
||||
try {
|
||||
} catch (error) {
|
||||
return { status: error };
|
||||
}
|
||||
}
|
||||
|
||||
export interface UrlStatusStats {
|
||||
total: number;
|
||||
actived: number; // 正常可用
|
||||
disabled: number; // 已禁用
|
||||
expired: number; // 已过期
|
||||
passwordProtected: number; // 密码保护
|
||||
}
|
||||
|
||||
function isValidExpirationValue(expiration: string): boolean {
|
||||
return EXPIRATION_ENUMS.some((item) => item.value === expiration);
|
||||
}
|
||||
|
||||
export async function getUrlStatusOptimized(
|
||||
userId: string,
|
||||
role: UserRole = "USER",
|
||||
): Promise<UrlStatusStats | { status: any }> {
|
||||
try {
|
||||
const whereCondition = role === "USER" ? { userId } : {};
|
||||
|
||||
const urlRecords = await prisma.userUrl.findMany({
|
||||
where: whereCondition,
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
active: true,
|
||||
expiration: true,
|
||||
password: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const stats: UrlStatusStats = {
|
||||
total: urlRecords.length,
|
||||
actived: 0,
|
||||
disabled: 0,
|
||||
expired: 0,
|
||||
passwordProtected: 0,
|
||||
};
|
||||
|
||||
// 遍历记录并分类
|
||||
urlRecords.forEach((record) => {
|
||||
const updatedAt = new Date(
|
||||
record.updatedAt || record.createdAt!,
|
||||
).getTime();
|
||||
|
||||
// 判断是否过期
|
||||
let isExpired = false;
|
||||
if (
|
||||
record.expiration !== "-1" &&
|
||||
isValidExpirationValue(record.expiration)
|
||||
) {
|
||||
const expirationSeconds = Number(record.expiration);
|
||||
const expirationMilliseconds = expirationSeconds * 1000;
|
||||
const expirationTime = updatedAt + expirationMilliseconds;
|
||||
isExpired = now > expirationTime;
|
||||
}
|
||||
|
||||
const isDisabled = record.active === 0;
|
||||
const hasPassword = Boolean(record.password && record.password.trim());
|
||||
|
||||
if (isExpired) {
|
||||
stats.expired++;
|
||||
} else if (isDisabled) {
|
||||
stats.disabled++;
|
||||
} else if (hasPassword) {
|
||||
stats.passwordProtected++;
|
||||
} else {
|
||||
stats.actived++;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error("Error getting URL status (optimized):", error);
|
||||
return { status: error };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUserShortUrl(data: ShortUrlFormData) {
|
||||
try {
|
||||
const res = await prisma.userUrl.create({
|
||||
|
||||
+5
-1
@@ -320,7 +320,11 @@
|
||||
"retry": "Retry",
|
||||
"backToHome": "Back to Home",
|
||||
"goBack": "Go Back",
|
||||
"contactSupportIfError": "If you believe this is an error, please contact technical support"
|
||||
"contactSupportIfError": "If you believe this is an error, please contact technical support",
|
||||
"Actived": "Active",
|
||||
"Disabled": "Disabled",
|
||||
"Expired": "Expired",
|
||||
"PasswordProtected": "Password Protected"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "Settings",
|
||||
|
||||
+5
-1
@@ -320,7 +320,11 @@
|
||||
"retry": "重试",
|
||||
"backToHome": "返回首页",
|
||||
"goBack": "返回上页",
|
||||
"contactSupportIfError": "如果您认为这是一个错误,请联系技术支持"
|
||||
"contactSupportIfError": "如果您认为这是一个错误,请联系技术支持",
|
||||
"Actived": "有效",
|
||||
"Disabled": "已禁用",
|
||||
"Expired": "已过期",
|
||||
"PasswordProtected": "密码保护"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "设置",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user