diff --git a/app/(protected)/dashboard/urls/meta-chart.tsx b/app/(protected)/dashboard/urls/meta-chart.tsx index 3abc5b6..cbe1705 100644 --- a/app/(protected)/dashboard/urls/meta-chart.tsx +++ b/app/(protected)/dashboard/urls/meta-chart.tsx @@ -10,6 +10,7 @@ import { WorldMapTopoJSON } from "@unovis/ts/maps"; import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"; import { getCountryName, getDeviceVendor } from "@/lib/contries"; +import { DATE_DIMENSION_ENUMS } from "@/lib/enums"; import { isLink, removeUrlSuffix, timeAgo } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { @@ -24,6 +25,13 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; const chartConfig = { pv: { @@ -132,7 +140,15 @@ function generateStatsList( return statsList; } -export function DailyPVUVChart({ data }: { data: UrlMeta[] }) { +export function DailyPVUVChart({ + data, + timeRange, + setTimeRange, +}: { + data: UrlMeta[]; + timeRange: string; + setTimeRange: React.Dispatch>; +}) { const [activeChart, setActiveChart] = React.useState("pv"); @@ -194,12 +210,30 @@ export function DailyPVUVChart({ data }: { data: UrlMeta[] }) {
- Daily Stats + Link Analytics Last visitor from {latestFrom} about {latestDate}.
-
+
+ {["pv", "uv"].map((key) => { const chart = key as keyof typeof chartConfig; return ( diff --git a/app/(protected)/dashboard/urls/meta.tsx b/app/(protected)/dashboard/urls/meta.tsx index 4916553..bdf186c 100644 --- a/app/(protected)/dashboard/urls/meta.tsx +++ b/app/(protected)/dashboard/urls/meta.tsx @@ -1,9 +1,18 @@ "use client"; +import { useState } from "react"; import { UrlMeta, User } from "@prisma/client"; import useSWR from "swr"; +import { DATE_DIMENSION_ENUMS } from "@/lib/enums"; import { fetcher } from "@/lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; @@ -16,8 +25,9 @@ export interface UrlMetaProps { } export default function UserUrlMetaInfo({ user, action, urlId }: UrlMetaProps) { + const [timeRange, setTimeRange] = useState("24h"); const { data, isLoading } = useSWR( - `${action}?id=${urlId}`, + `${action}?id=${urlId}&range=${timeRange}`, fetcher, { focusThrottleInterval: 30000 }, // 30 seconds, ); @@ -32,9 +42,27 @@ export default function UserUrlMetaInfo({ user, action, urlId }: UrlMetaProps) { if (!data || data.length === 0) { return ( - No Stats + No Visits - You don't have any stats yet. + You don't have any visits yet in last {timeRange}. + ); @@ -42,7 +70,11 @@ export default function UserUrlMetaInfo({ user, action, urlId }: UrlMetaProps) { return (
- +
); } diff --git a/app/api/url/meta/route.ts b/app/api/url/meta/route.ts index 1da417f..d1b282b 100644 --- a/app/api/url/meta/route.ts +++ b/app/api/url/meta/route.ts @@ -9,6 +9,7 @@ export async function GET(req: Request) { const url = new URL(req.url); const urlId = url.searchParams.get("id"); + const range = url.searchParams.get("range") || "24h"; if (!urlId) { return Response.json("url id is required", { @@ -16,7 +17,7 @@ export async function GET(req: Request) { }); } - const data = await getUserUrlMetaInfo(urlId); + const data = await getUserUrlMetaInfo(urlId, range); return Response.json(data); } catch (error) { diff --git a/components/forms/record-form.tsx b/components/forms/record-form.tsx index 3454d5d..cc6f9e1 100644 --- a/components/forms/record-form.tsx +++ b/components/forms/record-form.tsx @@ -116,7 +116,7 @@ export function RecordForm({ }); if (!response.ok || response.status !== 200) { toast.error("Update Failed", { - description: response.statusText, + description: await response.text(), }); } else { const res = await response.json(); @@ -142,7 +142,7 @@ export function RecordForm({ }); if (!response.ok || response.status !== 200) { toast.error("Delete Failed", { - description: response.statusText, + description: await response.text(), }); } else { await response.json(); diff --git a/components/forms/url-form.tsx b/components/forms/url-form.tsx index 968ab52..6c47921 100644 --- a/components/forms/url-form.tsx +++ b/components/forms/url-form.tsx @@ -88,7 +88,7 @@ export function UrlForm({ }); if (!response.ok || response.status !== 200) { toast.error("Created Failed!", { - description: await response.json(), + description: await response.text(), }); } else { // const res = await response.json(); @@ -108,7 +108,7 @@ export function UrlForm({ }); if (!response.ok || response.status !== 200) { toast.error("Update Failed", { - description: await response.json(), + description: await response.text(), }); } else { const res = await response.json(); @@ -132,7 +132,7 @@ export function UrlForm({ }); if (!response.ok || response.status !== 200) { toast.error("Delete Failed", { - description: await response.json(), + description: await response.text(), }); } else { await response.json(); diff --git a/lib/contries.ts b/lib/contries.ts index a72ee5d..67dec54 100644 --- a/lib/contries.ts +++ b/lib/contries.ts @@ -203,7 +203,7 @@ export const countryMap = { MO: "Macao,China", PS: "Palestine, State of", PR: "Puerto Rico", - TW: "Taiwan", + TW: "Taiwan,China", XK: "Kosovo", }; diff --git a/lib/dto/short-urls.ts b/lib/dto/short-urls.ts index c0a7b3d..c36955d 100644 --- a/lib/dto/short-urls.ts +++ b/lib/dto/short-urls.ts @@ -3,6 +3,8 @@ import { UrlMeta, UserRole } from "@prisma/client"; import { prisma } from "@/lib/db"; +import { getStartDate } from "../utils"; + export interface ShortUrlFormData { id?: string; userId: string; @@ -191,10 +193,18 @@ export async function deleteUserShortUrl(userId: string, urlId: string) { }); } -export async function getUserUrlMetaInfo(urlId: string) { +export async function getUserUrlMetaInfo( + urlId: string, + dateRange: string = "", +) { + const startDate = getStartDate(dateRange); + return await prisma.urlMeta.findMany({ where: { urlId, + ...(startDate && { + createdAt: { gte: startDate }, + }), }, orderBy: { updatedAt: "asc" }, }); diff --git a/lib/enums.ts b/lib/enums.ts index 6e975aa..00a9ed3 100644 --- a/lib/enums.ts +++ b/lib/enums.ts @@ -212,3 +212,23 @@ export const LOGS_LIMITEs_ENUMS = [ label: "1000", }, ]; + +export const TIME_RANGES: Record = { + "24h": 24 * 60 * 60 * 1000, // 24小时的毫秒数 + "7d": 7 * 24 * 60 * 60 * 1000, + "30d": 30 * 24 * 60 * 60 * 1000, + "90d": 90 * 24 * 60 * 60 * 1000, + "180d": 180 * 24 * 60 * 60 * 1000, + "365d": 365 * 24 * 60 * 60 * 1000, +}; + +export const DATE_DIMENSION_ENUMS = [ + { value: "24h", label: "Last 24 Hours" }, + { value: "7d", label: "Last 7 Days" }, + { value: "30d", label: "Last 30 Days" }, + { value: "60d", label: "Last 2 Months" }, + { value: "90d", label: "Last 3 Months" }, + { value: "180d", label: "Last 6 Months" }, + { value: "365d", label: "Last 1 Year" }, + { value: "All", label: "All the time" }, +] as const; diff --git a/lib/utils.ts b/lib/utils.ts index 8ac555b..127c08a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -9,6 +9,8 @@ import UAParser from "ua-parser-js"; import { env } from "@/env.mjs"; import { siteConfig } from "@/config/site"; +import { TIME_RANGES } from "./enums"; + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } @@ -261,3 +263,8 @@ export function toCamelCase(str: string) { .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(""); } + +export const getStartDate = (range: string): Date | undefined => { + if (!range || !(range in TIME_RANGES)) return undefined; + return new Date(Date.now() - TIME_RANGES[range]); +}; diff --git a/public/sw.js.map b/public/sw.js.map index 2055674..f1fd01e 100644 --- a/public/sw.js.map +++ b/public/sw.js.map @@ -1 +1 @@ -{"version":3,"file":"sw.js","sources":["../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/0b93c33b391b4e13586fa89097120639/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"sw.js","sources":["../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/f83ff6a791ead8769131962af5e0eb7f/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file