Compare commits
18 Commits
v0.6.4
...
fix/docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c9bad648 | ||
|
|
80796cdcca | ||
|
|
970dc5bbe8 | ||
|
|
6cade53ec5 | ||
|
|
34981f821d | ||
|
|
22f1686ff7 | ||
|
|
5d34f3707a | ||
|
|
d8ec5683d1 | ||
|
|
06a70b6680 | ||
|
|
938fcd4422 | ||
|
|
d86467674e | ||
|
|
a21ce6e8d6 | ||
|
|
0a4507bbd0 | ||
|
|
2f27c330a1 | ||
|
|
cff4579ff1 | ||
|
|
f2de129ba8 | ||
|
|
2a9a242f50 | ||
|
|
4e74053017 |
@@ -30,6 +30,9 @@ RESEND_FROM_EMAIL="wrdo <support@wr.do>"
|
||||
# Open Signup
|
||||
NEXT_PUBLIC_OPEN_SIGNUP=1
|
||||
|
||||
# Enable subdomain apply, default is false(0). If set to 1, will enable subdomain apply
|
||||
NEXT_PUBLIC_ENABLE_SUBDOMAIN_APPLY=1
|
||||
|
||||
# Google Analytics
|
||||
NEXT_PUBLIC_GOOGLE_ID=
|
||||
|
||||
@@ -42,3 +45,4 @@ GITHUB_TOKEN=
|
||||
# Skip DB check and migration (only for docker), default is true. if false, will check and migrate database each time start docker compose.
|
||||
SKIP_DB_CHECK=true
|
||||
SKIP_DB_MIGRATION=true
|
||||
|
||||
|
||||
3
.github/workflows/docker-build-push.yml
vendored
3
.github/workflows/docker-build-push.yml
vendored
@@ -4,8 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- fix/docker
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<h1>WR.DO</h1>
|
||||
<p><a href="https://discord.gg/AHPQYuZu3m">Discord</a> · English | <a href="/README-zh.md">简体中文</a></p>
|
||||
<p><a href="https://wr.do/docs/developer">开发文档</a> · <a href="https://discord.gg/AHPQYuZu3m">Discord</a> · English | <a href="/README-zh.md">简体中文</a></p>
|
||||
<p>生成短链接, 创建 DNS 记录, 管理临时邮箱</p>
|
||||
<!-- <img src="https://wr.do/_static/images/light-preview.png"/> -->
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<h1>WR.DO</h1>
|
||||
<p><a href="https://discord.gg/AHPQYuZu3m">Discord</a> · English | <a href="/README-zh.md">简体中文</a></p>
|
||||
<p><a href="https://wr.do/docs/developer">Docs</a> · <a href="https://discord.gg/AHPQYuZu3m">Discord</a> · English | <a href="/README-zh.md">简体中文</a></p>
|
||||
<p>Make Short Links, Manage DNS Records, Receive Emails.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { DomainFormData } from "@/lib/dto/domains";
|
||||
import { fetcher, timeAgo } from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -68,6 +69,7 @@ function TableColumnSekleton() {
|
||||
}
|
||||
|
||||
export default function DomainList({ user, action }: DomainListProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
const [currentEditDomain, setCurrentEditDomain] =
|
||||
@@ -326,6 +328,7 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
</TableBody>
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
layout={isMobile ? "right" : "split"}
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ScrapeMeta } from "@prisma/client";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { useElementSize } from "@/hooks/use-element-size";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -54,6 +55,7 @@ export function LineChartMultiple({
|
||||
type1,
|
||||
type2,
|
||||
}: LineChartMultipleProps) {
|
||||
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
|
||||
const processedData = processChartData(chartData, type1, type2);
|
||||
|
||||
const chartConfig = {
|
||||
@@ -75,12 +77,13 @@ export function LineChartMultiple({
|
||||
{type2 && ` and ${type2}`}.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent ref={wrapperRef}>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
className="mt-6"
|
||||
accessibilityLayer
|
||||
data={processedData}
|
||||
width={wrapperWidth}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
|
||||
@@ -73,6 +73,7 @@ function TableColumnSekleton({ className }: { className?: string }) {
|
||||
}
|
||||
|
||||
export default function UsersList({ user }: UrlListProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -282,6 +283,7 @@ export default function UsersList({ user }: UrlListProps) {
|
||||
</TableBody>
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
layout={isMobile ? "right" : "split"}
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
|
||||
@@ -10,6 +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 { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -79,12 +80,15 @@ function TableColumnSekleton() {
|
||||
}
|
||||
|
||||
export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
const [currentEditRecord, setCurrentEditRecord] =
|
||||
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 { mutate } = useSWRConfig();
|
||||
|
||||
@@ -134,11 +138,13 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const rendeApplyList = () => {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="xl:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
{action.includes("/admin") ? (
|
||||
{isAdmin ? (
|
||||
<CardDescription className="text-balance text-lg font-bold">
|
||||
<span>Total Subdomains:</span>{" "}
|
||||
<span className="font-bold">{data && data.total}</span>
|
||||
@@ -328,6 +334,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
|
||||
</TableBody>
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
layout={isMobile ? "right" : "split"}
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
|
||||
@@ -98,12 +98,12 @@ const LogsTable = ({ userId, target }) => {
|
||||
onChange={(e) => handleFilterChange("type", e.target.value)}
|
||||
className="h-8 max-w-xs placeholder:text-xs"
|
||||
/>
|
||||
<Input
|
||||
{/* <Input
|
||||
placeholder="Filter by IP..."
|
||||
value={filters.ip}
|
||||
onChange={(e) => handleFilterChange("ip", e.target.value)}
|
||||
className="h-8 max-w-xs placeholder:text-xs"
|
||||
/>
|
||||
/> */}
|
||||
{
|
||||
<>
|
||||
<Input
|
||||
@@ -139,16 +139,15 @@ const LogsTable = ({ userId, target }) => {
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader className="bg-muted">
|
||||
<TableRow className="">
|
||||
<TableRow className="grid grid-cols-5 items-center sm:grid-cols-6">
|
||||
<TableHead className="hidden items-center justify-start px-2 sm:flex">
|
||||
Date
|
||||
</TableHead>
|
||||
<TableHead className="px-2">Type</TableHead>
|
||||
<TableHead className="hidden items-center justify-start px-2 sm:flex">
|
||||
IP
|
||||
<TableHead className="flex items-center px-2">Type</TableHead>
|
||||
<TableHead className="col-span-3 flex items-center px-2">
|
||||
Link
|
||||
</TableHead>
|
||||
<TableHead className="px-2">Link</TableHead>
|
||||
<TableHead className="px-2">User</TableHead>
|
||||
<TableHead className="flex items-center px-2">User</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -161,9 +160,6 @@ const LogsTable = ({ userId, target }) => {
|
||||
<TableCell>
|
||||
<Skeleton className="h-2 w-[80px]" />
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:inline-block">
|
||||
<Skeleton className="h-2 w-[120px]" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-2 w-[200px]" />
|
||||
</TableCell>
|
||||
@@ -173,15 +169,15 @@ const LogsTable = ({ userId, target }) => {
|
||||
</TableRow>
|
||||
))
|
||||
: logs.map((log) => (
|
||||
<TableRow className="text-xs hover:bg-muted" key={log.id}>
|
||||
<TableRow
|
||||
className="grid grid-cols-5 items-center text-xs hover:bg-muted sm:grid-cols-6"
|
||||
key={log.id}
|
||||
>
|
||||
<TableCell className="hidden truncate p-2 sm:inline-block">
|
||||
{new Date(log.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="p-2">{log.type}</TableCell>
|
||||
<TableCell className="hidden p-2 sm:inline-block">
|
||||
{log.ip}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-md truncate p-2">
|
||||
<TableCell className="col-span-3 max-w-full truncate p-2">
|
||||
{log.link}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-md truncate p-2">
|
||||
@@ -193,12 +189,13 @@ const LogsTable = ({ userId, target }) => {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-start justify-between gap-2 sm:items-center">
|
||||
<p className="ml-auto text-nowrap text-sm">
|
||||
{nFormatter(data?.total || 0)} logs
|
||||
</p>
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
className="m-0"
|
||||
total={data.total}
|
||||
currentPage={page}
|
||||
setCurrentPage={setPage}
|
||||
|
||||
@@ -200,19 +200,30 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
{error ? (
|
||||
<div className="text-center text-red-500">{error.message}</div>
|
||||
) : logs.length === 0 && !newLogs ? (
|
||||
// <Skeleton className="h-8 w-full" />
|
||||
<></>
|
||||
) : (
|
||||
<div className="scrollbar-hidden h-96 overflow-y-auto bg-primary-foreground">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-100/50 text-sm dark:bg-primary-foreground">
|
||||
<TableHead className="h-8 w-1/6 px-1">Time</TableHead>
|
||||
<TableHead className="h-8 w-1/12 px-1">Slug</TableHead>
|
||||
<TableHead className="h-8 px-1">Target</TableHead>
|
||||
<TableHead className="h-8 w-1/12 px-1">IP</TableHead>
|
||||
<TableHead className="h-8 w-1/6 px-1">Location</TableHead>
|
||||
<TableHead className="h-8 w-1/12 px-1">Clicks</TableHead>
|
||||
<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
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex h-8 items-center">
|
||||
Slug
|
||||
</TableHead>
|
||||
<TableHead className="col-span-3 hidden h-8 items-center sm:flex">
|
||||
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
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex h-8 items-center">
|
||||
Clicks
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -234,23 +245,24 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
className="font-mono text-xs hover:bg-gray-200 dark:border-gray-800"
|
||||
className="grid grid-cols-5 font-mono text-xs hover:bg-gray-200 dark:border-gray-800 sm:grid-cols-9"
|
||||
>
|
||||
<TableCell className="whitespace-nowrap px-1 py-1.5">
|
||||
<TableCell className="col-span-2 truncate py-1.5">
|
||||
{new Date(log.updatedAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="font-midium px-1 py-1.5 text-green-700">
|
||||
<TableCell className="font-midium col-span-1 truncate py-1.5 text-green-700">
|
||||
{log.slug}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-10 truncate px-1 py-1.5 hover:underline">
|
||||
<TableCell className="col-span-3 hidden max-w-full truncate py-1.5 hover:underline sm:flex">
|
||||
<a href={log.target} target="_blank" title={log.target}>
|
||||
{log.target}
|
||||
</a>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-1 py-1.5">{log.ip}</TableCell>
|
||||
<TableCell className="col-span-1 hidden truncate py-1.5 sm:flex">
|
||||
{log.ip}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="max-w-6 truncate px-1 py-1.5"
|
||||
className="col-span-1 truncate py-1.5"
|
||||
title={getCountryName(log.country || "")}
|
||||
>
|
||||
{decodeURIComponent(
|
||||
@@ -259,7 +271,7 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
|
||||
: "-",
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="px-1 py-1.5 text-green-700">
|
||||
<TableCell className="col-span-1 py-1.5 text-green-700">
|
||||
{log.click}
|
||||
</TableCell>
|
||||
</motion.tr>
|
||||
|
||||
@@ -10,9 +10,17 @@ import { WorldMapTopoJSON } from "@unovis/ts/maps";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { TeamPlanQuota } from "@/config/team";
|
||||
import { getCountryName, getDeviceVendor } from "@/lib/contries";
|
||||
import {
|
||||
getBotName,
|
||||
getCountryName,
|
||||
getDeviceVendor,
|
||||
getEngineName,
|
||||
getLanguageName,
|
||||
getRegionName,
|
||||
} from "@/lib/contries";
|
||||
import { DATE_DIMENSION_ENUMS } from "@/lib/enums";
|
||||
import { isLink, removeUrlSuffix, timeAgo } from "@/lib/utils";
|
||||
import { useElementSize } from "@/hooks/use-element-size";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -34,6 +42,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
|
||||
const chartConfig = {
|
||||
@@ -117,6 +126,14 @@ function generateStatsList(
|
||||
? getCountryName(rawValue as string) // 国家代码转为国家名称
|
||||
: dimension === "device"
|
||||
? getDeviceVendor(rawValue as string) // 设备型号转为厂商名称
|
||||
: dimension === "engine"
|
||||
? getEngineName(rawValue as string) // 引擎名称
|
||||
: dimension === "region"
|
||||
? getRegionName(rawValue as string) // 区域名称
|
||||
: dimension === "lang"
|
||||
? getLanguageName(rawValue as string) // 语言名称
|
||||
: dimension === "isBot"
|
||||
? getBotName(rawValue as boolean) // 是否为机器人
|
||||
: rawValue; // 其他维度直接使用原始值
|
||||
|
||||
const click = record.click || 0; // 确保 click 是数字,默认 0 如果未定义
|
||||
@@ -154,6 +171,7 @@ export function DailyPVUVChart({
|
||||
setTimeRange: React.Dispatch<React.SetStateAction<string>>;
|
||||
user: Pick<User, "id" | "name" | "team">;
|
||||
}) {
|
||||
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
|
||||
const [activeChart, setActiveChart] =
|
||||
React.useState<keyof typeof chartConfig>("pv");
|
||||
|
||||
@@ -162,7 +180,6 @@ export function DailyPVUVChart({
|
||||
pv: entry.clicks,
|
||||
uv: new Set(entry.ips).size,
|
||||
}));
|
||||
// .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
|
||||
const dataTotal = calculateUVAndPV(data);
|
||||
|
||||
@@ -214,9 +231,15 @@ export function DailyPVUVChart({
|
||||
const deviceStats = generateStatsList(data, "device");
|
||||
const browserStats = generateStatsList(data, "browser");
|
||||
const countryStats = generateStatsList(data, "country");
|
||||
const osStats = generateStatsList(data, "os");
|
||||
const cpuStats = generateStatsList(data, "cpu");
|
||||
const engineStats = generateStatsList(data, "engine");
|
||||
const languageStats = generateStatsList(data, "lang");
|
||||
const regionStats = generateStatsList(data, "region");
|
||||
const isBotStats = generateStatsList(data, "isBot");
|
||||
|
||||
return (
|
||||
<Card className="">
|
||||
<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>
|
||||
@@ -279,7 +302,7 @@ export function DailyPVUVChart({
|
||||
})}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">
|
||||
<CardContent className="px-2 sm:p-6" ref={wrapperRef}>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[225px] w-full"
|
||||
@@ -351,9 +374,6 @@ export function DailyPVUVChart({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* <Bar dataKey="uv" fill={`var(--color-uv)`} stackId="a" />
|
||||
<Bar dataKey="pv" fill={`var(--color-pv)`} stackId="a" /> */}
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="uv"
|
||||
@@ -371,31 +391,103 @@ export function DailyPVUVChart({
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
|
||||
<VisSingleContainer data={{ areas: areaData }}>
|
||||
<VisTopoJSONMap
|
||||
topojson={WorldMapTopoJSON}
|
||||
// pointRadius={1.6}
|
||||
// mapFitToPoints={true}
|
||||
/>
|
||||
<VisSingleContainer
|
||||
data={{ areas: areaData }}
|
||||
width={wrapperWidth * 0.99}
|
||||
>
|
||||
<VisTopoJSONMap topojson={WorldMapTopoJSON} />
|
||||
<VisTooltip triggers={triggers} />
|
||||
</VisSingleContainer>
|
||||
|
||||
<div className="my-5 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{/* Referrers、isBotStats */}
|
||||
<Tabs defaultValue="referrer">
|
||||
<TabsList>
|
||||
<TabsTrigger value="referrer">Referrers</TabsTrigger>
|
||||
<TabsTrigger value="isBot">Traffic Type</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="referrer">
|
||||
{refererStats.length > 0 && (
|
||||
<StatsList data={refererStats} title="Referrers" />
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="isBot">
|
||||
{isBotStats.length > 0 && (
|
||||
<StatsList data={isBotStats} title="Is Bot" />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{/* 国家、城市 */}
|
||||
<Tabs defaultValue="country">
|
||||
<TabsList>
|
||||
<TabsTrigger value="country">Country</TabsTrigger>
|
||||
<TabsTrigger value="city">City</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="country">
|
||||
{countryStats.length > 0 && (
|
||||
<StatsList data={countryStats} title="Countries" />
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="city">
|
||||
{cityStats.length > 0 && (
|
||||
<StatsList data={cityStats} title="Cities" />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{/* browserStats、engineStats */}
|
||||
<Tabs defaultValue="browser">
|
||||
<TabsList>
|
||||
<TabsTrigger value="browser">Browser</TabsTrigger>
|
||||
<TabsTrigger value="engine">Browser Engine</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="browser">
|
||||
{browserStats.length > 0 && (
|
||||
<StatsList data={browserStats} title="Browsers" />
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="engine">
|
||||
{engineStats.length > 0 && (
|
||||
<StatsList data={engineStats} title="Engines" />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Languages、regionStats */}
|
||||
<Tabs className="h-full" defaultValue="language">
|
||||
<TabsList>
|
||||
<TabsTrigger value="language">Language</TabsTrigger>
|
||||
<TabsTrigger value="region">Region</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="language">
|
||||
{languageStats.length > 0 && (
|
||||
<StatsList data={languageStats} title="Languages" />
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="region">
|
||||
{regionStats.length > 0 && (
|
||||
<StatsList data={regionStats} title="Regions" />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{/* deviceStats、osStats、cpuStats */}
|
||||
<Tabs defaultValue="device">
|
||||
<TabsList>
|
||||
<TabsTrigger value="device">Device</TabsTrigger>
|
||||
<TabsTrigger value="os">OS</TabsTrigger>
|
||||
<TabsTrigger value="cpu">CPU</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="device">
|
||||
{deviceStats.length > 0 && (
|
||||
<StatsList data={deviceStats} title="Devices" />
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="os">
|
||||
{osStats.length > 0 && <StatsList data={osStats} title="OS" />}
|
||||
</TabsContent>
|
||||
<TabsContent className="h-[calc(100%-40px)]" value="cpu">
|
||||
{cpuStats.length > 0 && <StatsList data={cpuStats} title="CPU" />}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -407,12 +499,15 @@ export function StatsList({ data, title }: { data: Stat[]; title: string }) {
|
||||
const displayedData = showAll ? data.slice(0, 50) : data.slice(0, 8);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border p-4">
|
||||
<h1 className="text-lg font-bold">{title}</h1>
|
||||
<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>
|
||||
</div>
|
||||
<div
|
||||
className={`scrollbar-hidden overflow-hidden overflow-y-auto transition-all duration-500 ease-in-out`}
|
||||
className={`scrollbar-hidden overflow-hidden overflow-y-auto px-4 pb-4 pt-2 transition-all duration-500 ease-in-out`}
|
||||
style={{
|
||||
maxHeight: "18rem", // 动态计算最大高度
|
||||
maxHeight: "18rem",
|
||||
}}
|
||||
>
|
||||
{displayedData.map((ref) => (
|
||||
@@ -454,7 +549,7 @@ export function StatsList({ data, title }: { data: Stat[]; title: string }) {
|
||||
</div>
|
||||
|
||||
{data.length > 8 && (
|
||||
<div className="mt-3 text-center">
|
||||
<div className="mb-3 mt-1 text-center">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
|
||||
@@ -18,12 +18,12 @@ import {
|
||||
removeUrlSuffix,
|
||||
timeAgo,
|
||||
} from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
@@ -100,6 +100,7 @@ function TableColumnSekleton() {
|
||||
|
||||
export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
const pathname = usePathname();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [currentView, setCurrentView] = useState<string>("List");
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
@@ -447,6 +448,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
</TableBody>
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
layout={isMobile ? "right" : "split"}
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
@@ -470,23 +472,24 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
data.list.map((short) => (
|
||||
<div
|
||||
className={cn(
|
||||
"h-24 rounded-lg border bg-neutral-50/50 p-1 dark:bg-neutral-800",
|
||||
"h-24 rounded-lg border p-1 shadow-inner dark:bg-neutral-800",
|
||||
)}
|
||||
key={short.id}
|
||||
>
|
||||
<div className="flex h-full flex-col rounded-lg border border-dotted bg-white px-3 py-1.5 shadow backdrop-blur-lg dark:bg-black">
|
||||
<div className="flex h-full flex-col rounded-lg border border-dotted bg-white px-3 py-1.5 backdrop-blur-lg dark:bg-black">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<BlurImage
|
||||
src={`https://unavatar.io/${extractHostname(short.target)}?fallback=https://wr.do/logo.png`}
|
||||
alt="logo"
|
||||
width={30}
|
||||
height={30}
|
||||
className="rounded-md"
|
||||
/>
|
||||
<div className="ml-2 mr-auto flex flex-col justify-between truncate">
|
||||
{/* url */}
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
className="overflow-hidden text-ellipsis whitespace-normal text-sm font-semibold text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-400"
|
||||
className="overflow-hidden text-ellipsis whitespace-normal text-sm font-semibold text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-300"
|
||||
href={`https://${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
target="_blank"
|
||||
prefetch={false}
|
||||
@@ -501,6 +504,17 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
"duration-250 transition-all group-hover:opacity-100",
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="duration-250 size-[26px] p-1.5 text-foreground transition-all hover:border hover:text-foreground dark:text-foreground"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedUrl(short);
|
||||
setShowQrcode(!isShowQrcode);
|
||||
}}
|
||||
>
|
||||
<Icons.qrcode className="size-4" />
|
||||
</Button>
|
||||
{short.password && (
|
||||
<Icons.lock className="size-3 text-neutral-600 dark:text-neutral-400" />
|
||||
)}
|
||||
@@ -532,38 +546,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
className="flex w-full items-center gap-2"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setCurrentEditUrl(short);
|
||||
setShowForm(false);
|
||||
setFormType("edit");
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<PenLine className="size-4" />
|
||||
Edit URL
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
className="flex w-full items-center gap-2"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedUrl(short);
|
||||
setShowQrcode(!isShowQrcode);
|
||||
}}
|
||||
>
|
||||
<Icons.qrcode className="size-4" />
|
||||
QR Code
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
className="flex w-full items-center gap-2"
|
||||
@@ -582,19 +564,54 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
Analytics
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
className="flex w-full items-center gap-2"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setCurrentEditUrl(short);
|
||||
setShowForm(false);
|
||||
setFormType("edit");
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<PenLine className="size-4" />
|
||||
Edit URL
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center justify-end gap-1.5 text-xs text-muted-foreground">
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger className="truncate">
|
||||
{short.userName ?? "Anonymous"}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{short.userName ?? "Anonymous"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Separator
|
||||
className="h-4/5"
|
||||
orientation="vertical"
|
||||
></Separator>
|
||||
{short.expiration !== "-1" && (
|
||||
<>
|
||||
<span>
|
||||
Expiration:
|
||||
Expiration:{" "}
|
||||
{expirationTime(short.expiration, short.updatedAt)}
|
||||
</span>
|
||||
<Separator
|
||||
className="h-4/5"
|
||||
orientation="vertical"
|
||||
></Separator>
|
||||
</>
|
||||
)}
|
||||
{timeAgo(short.updatedAt as Date)}
|
||||
<Switch
|
||||
className="scale-[0.6]"
|
||||
@@ -614,6 +631,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
layout={isMobile ? "right" : "split"}
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
|
||||
@@ -215,8 +215,7 @@ function SetAdminRole({ id, email }: { id: string; email: string }) {
|
||||
ReadyBadge
|
||||
) : (
|
||||
<Button
|
||||
className=""
|
||||
variant={"outline"}
|
||||
variant={"default"}
|
||||
size={"sm"}
|
||||
onClick={handleSetAdmin}
|
||||
disabled={isPending}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { env } from "@/env.mjs";
|
||||
// import { env } from "@/env.mjs";
|
||||
|
||||
export async function GET() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
google: !!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET),
|
||||
github: !!(env.GITHUB_ID && env.GITHUB_SECRET),
|
||||
linuxdo: !!(env.LinuxDo_CLIENT_ID && env.LinuxDo_CLIENT_SECRET),
|
||||
resend: !!(env.RESEND_API_KEY && env.RESEND_FROM_EMAIL),
|
||||
}),
|
||||
);
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
return Response.json({
|
||||
google: true,
|
||||
github: true,
|
||||
linuxdo: true,
|
||||
resend: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("[Error]", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { TeamPlanQuota } from "@/config/team";
|
||||
import { createDNSRecord } from "@/lib/cloudflare";
|
||||
import {
|
||||
@@ -75,10 +76,38 @@ export async function POST(req: Request) {
|
||||
);
|
||||
if (user_record && user_record.length > 0) {
|
||||
return Response.json("Record already exists", {
|
||||
status: 403,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// apply subdomain
|
||||
if (siteConfig.enableSubdomainApply) {
|
||||
const res = await createUserRecord(user.id, {
|
||||
record_id: generateSecret(16),
|
||||
zone_id: matchedZone.cf_zone_id,
|
||||
zone_name: matchedZone.domain_name,
|
||||
name: record.name,
|
||||
type: record.type,
|
||||
content: record.content,
|
||||
proxied: record.proxied,
|
||||
proxiable: false,
|
||||
ttl: record.ttl,
|
||||
comment: record.comment,
|
||||
tags: "",
|
||||
created_on: new Date().toISOString(),
|
||||
modified_on: new Date().toISOString(),
|
||||
active: 2, // pending
|
||||
});
|
||||
|
||||
if (res.status !== "success") {
|
||||
return Response.json(res.status, {
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
// send email to admin
|
||||
return Response.json(res.data?.id);
|
||||
}
|
||||
|
||||
const data = await createDNSRecord(
|
||||
matchedZone.cf_zone_id,
|
||||
matchedZone.cf_api_key,
|
||||
|
||||
1
app/api/record/admin/apply/route.ts
Normal file
1
app/api/record/admin/apply/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export async function POST(req: Request) {}
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "0.6.4",
|
||||
"versionName": "0.6.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -7,6 +7,7 @@ import useSWR from "swr";
|
||||
|
||||
import { DATE_DIMENSION_ENUMS } from "@/lib/enums";
|
||||
import { cn, fetcher, nFormatter } from "@/lib/utils";
|
||||
import { useElementSize } from "@/hooks/use-element-size";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -62,6 +63,7 @@ const chartConfig = {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function InteractiveBarChart() {
|
||||
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
|
||||
const [timeRange, setTimeRange] = useState<string>("7d");
|
||||
const [activeChart, setActiveChart] =
|
||||
React.useState<keyof typeof chartConfig>("users");
|
||||
@@ -134,7 +136,7 @@ export function InteractiveBarChart() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6">
|
||||
{["users", "records", "urls", "emails", "inbox", "sends"].map(
|
||||
(key) => {
|
||||
const chart = key as keyof typeof chartConfig;
|
||||
@@ -144,13 +146,13 @@ export function InteractiveBarChart() {
|
||||
<button
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-l border-t p-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:p-6"
|
||||
className="relative z-30 flex flex-col justify-center gap-1 border-l border-t p-3 text-left transition-colors hover:bg-muted/30 data-[active=true]:bg-muted/50 sm:border-t-0 sm:p-4"
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chartConfig[chart].label}
|
||||
</span>
|
||||
<span className="text-lg font-bold leading-none sm:text-3xl">
|
||||
<span className="text-base font-bold leading-none sm:text-lg">
|
||||
{nFormatter(data.total[key])}
|
||||
</span>
|
||||
<span
|
||||
@@ -170,7 +172,7 @@ export function InteractiveBarChart() {
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">
|
||||
<CardContent ref={wrapperRef} className="px-2 sm:p-6">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[200px] w-full"
|
||||
@@ -178,6 +180,7 @@ export function InteractiveBarChart() {
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={data.list}
|
||||
width={wrapperWidth}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Callout } from "@/components/shared/callout";
|
||||
import { CopyButton } from "@/components/shared/copy-button";
|
||||
|
||||
import { DocsLang } from "../shared/docs-lang";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
const components = {
|
||||
h1: ({ className, ...props }) => (
|
||||
|
||||
@@ -622,7 +622,7 @@ export default function EmailSidebar({
|
||||
))
|
||||
) : (
|
||||
<Button className="w-full" variant="ghost">
|
||||
No domain
|
||||
No domains configured
|
||||
</Button>
|
||||
)}
|
||||
</SelectContent>
|
||||
|
||||
@@ -233,7 +233,7 @@ export function RecordForm({
|
||||
))
|
||||
) : (
|
||||
<Button className="w-full" variant="ghost">
|
||||
No domain
|
||||
No domains configured
|
||||
</Button>
|
||||
)}
|
||||
</SelectContent>
|
||||
|
||||
@@ -224,7 +224,7 @@ export function UrlForm({
|
||||
))
|
||||
) : (
|
||||
<Button className="w-full" variant="ghost">
|
||||
No domain
|
||||
No domains configured
|
||||
</Button>
|
||||
)}
|
||||
</SelectContent>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { NavItem, SidebarNavItem } from "@/types";
|
||||
import { SidebarNavItem } from "@/types";
|
||||
import { Menu, PanelLeftClose, PanelRightClose } from "lucide-react";
|
||||
import { Link } from "next-view-transitions";
|
||||
import { name, version } from "package.json";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -163,25 +164,17 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
</nav>
|
||||
|
||||
{isSidebarExpanded && (
|
||||
<p className="mx-3 mt-auto pb-3 pt-6 font-mono text-xs 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>
|
||||
.{/* <br /> Built with{" "} */}
|
||||
{/* <Link
|
||||
href="https://www.cloudflare.com?ref=wrdo"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium text-primary underline underline-offset-2"
|
||||
>
|
||||
Cloudflare
|
||||
</Link> */}
|
||||
v{version}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -275,6 +268,19 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
),
|
||||
)}
|
||||
|
||||
<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 underline-offset-2 hover:underline"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
v{version}
|
||||
</p>
|
||||
|
||||
{/* <div className="mt-auto">
|
||||
<UpgradeCard />
|
||||
</div> */}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export function DocsLang({ en, zh }: { en: string; zh: string }) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{pathname !== en ? (
|
||||
<Link href={en} className="text-blue-500 hover:underline">
|
||||
English
|
||||
@@ -14,13 +16,13 @@ export function DocsLang({ en, zh }: { en: string; zh: string }) {
|
||||
) : (
|
||||
<p className="text-muted-foreground">English</p>
|
||||
)}
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<div className="h-[20px] w-px shrink-0 border bg-border text-neutral-400"></div>
|
||||
{pathname !== zh ? (
|
||||
<Link href={zh} className="text-blue-500 hover:underline">
|
||||
中文
|
||||
简体中文
|
||||
</Link>
|
||||
) : (
|
||||
<p className="text-muted-foreground">中文</p>
|
||||
<p className="text-muted-foreground">简体中文</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -426,7 +426,7 @@ export default function QRCodeEditor({
|
||||
|
||||
{/* Api Key Mask */}
|
||||
{!user.apiKey && (
|
||||
<div className="absolute left-0 top-0 flex size-full flex-col items-center justify-center gap-2 bg-neutral-100/20 px-4 backdrop-blur">
|
||||
<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{" "}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { User } from "@prisma/client";
|
||||
import { AvatarProps } from "@radix-ui/react-avatar";
|
||||
|
||||
import { generateGradientClasses } from "@/lib/enums";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||
|
||||
interface UserAvatarProps extends AvatarProps {
|
||||
@@ -11,23 +12,14 @@ interface UserAvatarProps extends AvatarProps {
|
||||
export function UserAvatar({ user, ...props }: UserAvatarProps) {
|
||||
return (
|
||||
<Avatar {...props}>
|
||||
{user.image ? (
|
||||
<AvatarImage
|
||||
alt="Picture"
|
||||
src={user.image}
|
||||
src={
|
||||
user.image ??
|
||||
`https://unavatar.io/${user.email}?ttl=1h&fallback=https://wr.do/_static/avatar.png`
|
||||
}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<RandomAvatar text={user.name || user.email || ""} />
|
||||
)}
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
export function RandomAvatar({ text }: { text?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex size-full shrink-0 items-center justify-center rounded-full text-white ${generateGradientClasses(text || "wr.do")}`}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "SCRAPE",
|
||||
title: "OPEN API",
|
||||
items: [
|
||||
{
|
||||
href: "/dashboard/scrape",
|
||||
@@ -85,11 +85,6 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
items: [
|
||||
{ href: "/dashboard/settings", icon: "settings", title: "Settings" },
|
||||
{ href: "/docs", icon: "bookOpen", title: "Documentation" },
|
||||
{
|
||||
href: siteConfig.links.oichat,
|
||||
icon: "botMessageSquare",
|
||||
title: "OiChat",
|
||||
},
|
||||
{
|
||||
href: siteConfig.links.feedback,
|
||||
icon: "messageQuoted",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { env } from "@/env.mjs";
|
||||
const site_url = env.NEXT_PUBLIC_APP_URL || "http://localhost:3030";
|
||||
const open_signup = env.NEXT_PUBLIC_OPEN_SIGNUP;
|
||||
const email_r2_domain = env.NEXT_PUBLIC_EMAIL_R2_DOMAIN || "";
|
||||
const enable_subdomain_apply = env.NEXT_PUBLIC_ENABLE_SUBDOMAIN_APPLY || "0";
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
name: "WR.DO",
|
||||
@@ -21,6 +22,7 @@ export const siteConfig: SiteConfig = {
|
||||
mailSupport: "support@wr.do",
|
||||
openSignup: open_signup === "1" ? true : false,
|
||||
emailR2Domain: email_r2_domain,
|
||||
enableSubdomainApply: enable_subdomain_apply === "1" ? true : false,
|
||||
};
|
||||
|
||||
export const footerLinks: SidebarNavItem[] = [
|
||||
|
||||
@@ -7,9 +7,11 @@ description: 选择你的部署方式
|
||||
|
||||
## 使用 Vercel 部署(推荐)
|
||||
|
||||
[](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)
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
请在部署前先创建你的数据库实例。
|
||||
</Callout>
|
||||
|
||||
记得填写必要的环境变量。
|
||||
[](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)
|
||||
|
||||
## 使用 Docker Compose 部署
|
||||
|
||||
@@ -53,3 +55,15 @@ docker compose up -d
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 官方镜像
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/oiov/wr.do/wrdo:main
|
||||
```
|
||||
|
||||
在 [container/wr.do](https://github.com/oiov/wr.do/pkgs/container/wr.do%2Fwrdo) 可以找到官方镜像。
|
||||
|
||||
## 打包镜像
|
||||
|
||||
Fork 此仓库后,在 Actions 中触发打包镜像。
|
||||
@@ -50,3 +50,15 @@ Fill in the environment variables in the `.env` file, then:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Official Image
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/oiov/wr.do/wrdo:main
|
||||
```
|
||||
|
||||
Find the official image here: [container/wr.do](https://github.com/oiov/wr.do/pkgs/container/wr.do%2Fwrdo)
|
||||
|
||||
## Build Image
|
||||
|
||||
Fork this repository and trigger the build image action in Actions.
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/oiov/wr.do/wrdo:${TAG:-main}
|
||||
image: ghcr.io/oiov/wr.do/wrdo:${TAG:-latest}
|
||||
container_name: wrdo
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/oiov/wr.do/wrdo:${TAG:-main}
|
||||
image: ghcr.io/oiov/wr.do/wrdo:${TAG:-latest}
|
||||
container_name: wrdo
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
5
env.mjs
5
env.mjs
@@ -3,8 +3,6 @@ import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
// This is optional because it's only used in development.
|
||||
// See https://next-auth.js.org/deployment.
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
AUTH_SECRET: z.string().optional(),
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
@@ -23,6 +21,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_APP_URL: z.string().optional(),
|
||||
NEXT_PUBLIC_OPEN_SIGNUP: z.string().min(1).default("1"),
|
||||
NEXT_PUBLIC_EMAIL_R2_DOMAIN: z.string().optional(),
|
||||
NEXT_PUBLIC_ENABLE_SUBDOMAIN_APPLY: z.string().min(1).default("0"),
|
||||
},
|
||||
runtimeEnv: {
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
@@ -37,6 +36,8 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
NEXT_PUBLIC_OPEN_SIGNUP: process.env.NEXT_PUBLIC_OPEN_SIGNUP,
|
||||
NEXT_PUBLIC_EMAIL_R2_DOMAIN: process.env.NEXT_PUBLIC_EMAIL_R2_DOMAIN,
|
||||
NEXT_PUBLIC_ENABLE_SUBDOMAIN_APPLY:
|
||||
process.env.NEXT_PUBLIC_ENABLE_SUBDOMAIN_APPLY,
|
||||
SCREENSHOTONE_BASE_URL: process.env.SCREENSHOTONE_BASE_URL,
|
||||
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
||||
LinuxDo_CLIENT_ID: process.env.LinuxDo_CLIENT_ID,
|
||||
|
||||
@@ -10,7 +10,7 @@ interface UseElementSizeOptions {
|
||||
box?: "content-box" | "border-box" | "device-pixel-content-box";
|
||||
}
|
||||
|
||||
export function useElementSize<T extends HTMLElement>(
|
||||
export function useElementSize<T extends HTMLDivElement>(
|
||||
initialSize: ElementSize = { width: 0, height: 0 },
|
||||
options: UseElementSizeOptions = {},
|
||||
): { ref: React.RefObject<T>; width: number; height: number } {
|
||||
|
||||
327
lib/contries.ts
327
lib/contries.ts
@@ -263,3 +263,330 @@ export const getDeviceVendor = (model: string) => {
|
||||
}
|
||||
return model;
|
||||
};
|
||||
|
||||
export function getRegionName(regionCode: string) {
|
||||
const regionMap = {
|
||||
// Vercel/Cloudflare 地区代码
|
||||
hkg1: "Hong Kong",
|
||||
sin1: "Singapore",
|
||||
iad1: "Washington D.C. (US East)",
|
||||
gru1: "São Paulo (Brazil)",
|
||||
sfo1: "San Francisco (US West)",
|
||||
cdg1: "Paris (France)",
|
||||
cle1: "Cleveland (US Central)",
|
||||
cpt1: "Cape Town (South Africa)",
|
||||
|
||||
// AWS 地区代码
|
||||
"us-east-1": "N. Virginia (US East)",
|
||||
"us-west-1": "N. California (US West)",
|
||||
"us-west-2": "Oregon (US West)",
|
||||
"eu-west-1": "Ireland (Europe)",
|
||||
"eu-central-1": "Frankfurt (Europe)",
|
||||
"ap-southeast-1": "Singapore (Asia Pacific)",
|
||||
"ap-northeast-1": "Tokyo (Asia Pacific)",
|
||||
"ap-south-1": "Mumbai (Asia Pacific)",
|
||||
|
||||
// Cloudflare 地区代码
|
||||
lhr: "London (UK)",
|
||||
fra: "Frankfurt (Germany)",
|
||||
ams: "Amsterdam (Netherlands)",
|
||||
nrt: "Tokyo (Japan)",
|
||||
icn: "Seoul (South Korea)",
|
||||
syd: "Sydney (Australia)",
|
||||
yyz: "Toronto (Canada)",
|
||||
mia: "Miami (US Southeast)",
|
||||
lax: "Los Angeles (US West)",
|
||||
ord: "Chicago (US Central)",
|
||||
atl: "Atlanta (US Southeast)",
|
||||
dfw: "Dallas (US Central)",
|
||||
sea: "Seattle (US West)",
|
||||
bos: "Boston (US Northeast)",
|
||||
ewr: "Newark (US Northeast)",
|
||||
jfk: "New York (US Northeast)",
|
||||
|
||||
// 其他常见地区代码
|
||||
pdx1: "Portland (US West)",
|
||||
bom1: "Mumbai (India)",
|
||||
syd1: "Sydney (Australia)",
|
||||
nrt1: "Tokyo (Japan)",
|
||||
fra1: "Frankfurt (Germany)",
|
||||
lon1: "London (UK)",
|
||||
ams1: "Amsterdam (Netherlands)",
|
||||
tor1: "Toronto (Canada)",
|
||||
nyc1: "New York (US East)",
|
||||
dub1: "Dublin (Ireland)",
|
||||
blr1: "Bangalore (India)",
|
||||
sgp1: "Singapore",
|
||||
hnd1: "Tokyo Haneda (Japan)",
|
||||
kix1: "Osaka (Japan)",
|
||||
icn1: "Seoul (South Korea)",
|
||||
bkk1: "Bangkok (Thailand)",
|
||||
mnl1: "Manila (Philippines)",
|
||||
jkt1: "Jakarta (Indonesia)",
|
||||
mel1: "Melbourne (Australia)",
|
||||
per1: "Perth (Australia)",
|
||||
akl1: "Auckland (New Zealand)",
|
||||
mad1: "Madrid (Spain)",
|
||||
bcn1: "Barcelona (Spain)",
|
||||
mxp1: "Milan (Italy)",
|
||||
vie1: "Vienna (Austria)",
|
||||
zrh1: "Zurich (Switzerland)",
|
||||
sto1: "Stockholm (Sweden)",
|
||||
hel1: "Helsinki (Finland)",
|
||||
cph1: "Copenhagen (Denmark)",
|
||||
osl1: "Oslo (Norway)",
|
||||
waw1: "Warsaw (Poland)",
|
||||
prg1: "Prague (Czech Republic)",
|
||||
bud1: "Budapest (Hungary)",
|
||||
buh1: "Bucharest (Romania)",
|
||||
sof1: "Sofia (Bulgaria)",
|
||||
ath1: "Athens (Greece)",
|
||||
ist1: "Istanbul (Turkey)",
|
||||
tlv1: "Tel Aviv (Israel)",
|
||||
cai1: "Cairo (Egypt)",
|
||||
jnb1: "Johannesburg (South Africa)",
|
||||
lag1: "Lagos (Nigeria)",
|
||||
nbo1: "Nairobi (Kenya)",
|
||||
dxb1: "Dubai (UAE)",
|
||||
bah1: "Bahrain",
|
||||
khi1: "Karachi (Pakistan)",
|
||||
del1: "Delhi (India)",
|
||||
ccj1: "Kolkata (India)",
|
||||
maa1: "Chennai (India)",
|
||||
hyd1: "Hyderabad (India)",
|
||||
pnq1: "Pune (India)",
|
||||
};
|
||||
|
||||
return regionMap[regionCode.toLowerCase()] || regionCode.toUpperCase();
|
||||
}
|
||||
|
||||
export function getLanguageName(langCode: string) {
|
||||
// 统一转换为小写处理大小写不一致
|
||||
const normalizedCode = langCode.toLowerCase();
|
||||
|
||||
const languageMap = {
|
||||
// 英语系列
|
||||
en: "English",
|
||||
"en-us": "English (United States)",
|
||||
"en-gb": "English (United Kingdom)",
|
||||
"en-ca": "English (Canada)",
|
||||
"en-au": "English (Australia)",
|
||||
"en-nz": "English (New Zealand)",
|
||||
"en-ie": "English (Ireland)",
|
||||
"en-za": "English (South Africa)",
|
||||
"en-in": "English (India)",
|
||||
|
||||
// 中文系列
|
||||
zh: "Chinese",
|
||||
"zh-cn": "Chinese (Simplified)",
|
||||
"zh-tw": "Chinese (Traditional)",
|
||||
"zh-hk": "Chinese (Hong Kong)",
|
||||
"zh-sg": "Chinese (Singapore)",
|
||||
|
||||
// 法语系列
|
||||
fr: "French",
|
||||
"fr-fr": "French (France)",
|
||||
"fr-ca": "French (Canada)",
|
||||
"fr-be": "French (Belgium)",
|
||||
"fr-ch": "French (Switzerland)",
|
||||
|
||||
// 德语系列
|
||||
de: "German",
|
||||
"de-de": "German (Germany)",
|
||||
"de-at": "German (Austria)",
|
||||
"de-ch": "German (Switzerland)",
|
||||
|
||||
// 西班牙语系列
|
||||
es: "Spanish",
|
||||
"es-es": "Spanish (Spain)",
|
||||
"es-mx": "Spanish (Mexico)",
|
||||
"es-ar": "Spanish (Argentina)",
|
||||
"es-co": "Spanish (Colombia)",
|
||||
"es-cl": "Spanish (Chile)",
|
||||
|
||||
// 葡萄牙语系列
|
||||
pt: "Portuguese",
|
||||
"pt-pt": "Portuguese (Portugal)",
|
||||
"pt-br": "Portuguese (Brazil)",
|
||||
|
||||
// 意大利语
|
||||
it: "Italian",
|
||||
"it-it": "Italian (Italy)",
|
||||
|
||||
// 俄语
|
||||
ru: "Russian",
|
||||
"ru-ru": "Russian (Russia)",
|
||||
|
||||
// 日语
|
||||
ja: "Japanese",
|
||||
"ja-jp": "Japanese (Japan)",
|
||||
|
||||
// 韩语
|
||||
ko: "Korean",
|
||||
"ko-kr": "Korean (South Korea)",
|
||||
|
||||
// 阿拉伯语
|
||||
ar: "Arabic",
|
||||
"ar-sa": "Arabic (Saudi Arabia)",
|
||||
"ar-ae": "Arabic (UAE)",
|
||||
"ar-eg": "Arabic (Egypt)",
|
||||
|
||||
// 荷兰语
|
||||
nl: "Dutch",
|
||||
"nl-nl": "Dutch (Netherlands)",
|
||||
"nl-be": "Dutch (Belgium)",
|
||||
|
||||
// 北欧语言
|
||||
sv: "Swedish",
|
||||
"sv-se": "Swedish (Sweden)",
|
||||
da: "Danish",
|
||||
"da-dk": "Danish (Denmark)",
|
||||
no: "Norwegian",
|
||||
"no-no": "Norwegian (Norway)",
|
||||
"nb-no": "Norwegian Bokmål",
|
||||
"nn-no": "Norwegian Nynorsk",
|
||||
fi: "Finnish",
|
||||
"fi-fi": "Finnish (Finland)",
|
||||
|
||||
// 其他常见语言
|
||||
hi: "Hindi",
|
||||
"hi-in": "Hindi (India)",
|
||||
th: "Thai",
|
||||
"th-th": "Thai (Thailand)",
|
||||
vi: "Vietnamese",
|
||||
"vi-vn": "Vietnamese (Vietnam)",
|
||||
tr: "Turkish",
|
||||
"tr-tr": "Turkish (Turkey)",
|
||||
pl: "Polish",
|
||||
"pl-pl": "Polish (Poland)",
|
||||
cs: "Czech",
|
||||
"cs-cz": "Czech (Czech Republic)",
|
||||
sk: "Slovak",
|
||||
"sk-sk": "Slovak (Slovakia)",
|
||||
hu: "Hungarian",
|
||||
"hu-hu": "Hungarian (Hungary)",
|
||||
ro: "Romanian",
|
||||
"ro-ro": "Romanian (Romania)",
|
||||
bg: "Bulgarian",
|
||||
"bg-bg": "Bulgarian (Bulgaria)",
|
||||
hr: "Croatian",
|
||||
"hr-hr": "Croatian (Croatia)",
|
||||
sr: "Serbian",
|
||||
"sr-rs": "Serbian (Serbia)",
|
||||
sl: "Slovenian",
|
||||
"sl-si": "Slovenian (Slovenia)",
|
||||
et: "Estonian",
|
||||
"et-ee": "Estonian (Estonia)",
|
||||
lv: "Latvian",
|
||||
"lv-lv": "Latvian (Latvia)",
|
||||
lt: "Lithuanian",
|
||||
"lt-lt": "Lithuanian (Lithuania)",
|
||||
el: "Greek",
|
||||
"el-gr": "Greek (Greece)",
|
||||
he: "Hebrew",
|
||||
"he-il": "Hebrew (Israel)",
|
||||
fa: "Persian",
|
||||
"fa-ir": "Persian (Iran)",
|
||||
ur: "Urdu",
|
||||
"ur-pk": "Urdu (Pakistan)",
|
||||
bn: "Bengali",
|
||||
"bn-bd": "Bengali (Bangladesh)",
|
||||
ta: "Tamil",
|
||||
"ta-in": "Tamil (India)",
|
||||
te: "Telugu",
|
||||
"te-in": "Telugu (India)",
|
||||
ml: "Malayalam",
|
||||
"ml-in": "Malayalam (India)",
|
||||
kn: "Kannada",
|
||||
"kn-in": "Kannada (India)",
|
||||
gu: "Gujarati",
|
||||
"gu-in": "Gujarati (India)",
|
||||
pa: "Punjabi",
|
||||
"pa-in": "Punjabi (India)",
|
||||
mr: "Marathi",
|
||||
"mr-in": "Marathi (India)",
|
||||
ne: "Nepali",
|
||||
"ne-np": "Nepali (Nepal)",
|
||||
si: "Sinhala",
|
||||
"si-lk": "Sinhala (Sri Lanka)",
|
||||
my: "Myanmar",
|
||||
"my-mm": "Myanmar (Myanmar)",
|
||||
km: "Khmer",
|
||||
"km-kh": "Khmer (Cambodia)",
|
||||
lo: "Lao",
|
||||
"lo-la": "Lao (Laos)",
|
||||
ka: "Georgian",
|
||||
"ka-ge": "Georgian (Georgia)",
|
||||
hy: "Armenian",
|
||||
"hy-am": "Armenian (Armenia)",
|
||||
az: "Azerbaijani",
|
||||
"az-az": "Azerbaijani (Azerbaijan)",
|
||||
kk: "Kazakh",
|
||||
"kk-kz": "Kazakh (Kazakhstan)",
|
||||
ky: "Kyrgyz",
|
||||
"ky-kg": "Kyrgyz (Kyrgyzstan)",
|
||||
uz: "Uzbek",
|
||||
"uz-uz": "Uzbek (Uzbekistan)",
|
||||
tg: "Tajik",
|
||||
"tg-tj": "Tajik (Tajikistan)",
|
||||
mn: "Mongolian",
|
||||
"mn-mn": "Mongolian (Mongolia)",
|
||||
bo: "Tibetan",
|
||||
"bo-cn": "Tibetan (China)",
|
||||
ug: "Uyghur",
|
||||
"ug-cn": "Uyghur (China)",
|
||||
id: "Indonesian",
|
||||
"id-id": "Indonesian (Indonesia)",
|
||||
ms: "Malay",
|
||||
"ms-my": "Malay (Malaysia)",
|
||||
tl: "Filipino",
|
||||
"tl-ph": "Filipino (Philippines)",
|
||||
sw: "Swahili",
|
||||
"sw-ke": "Swahili (Kenya)",
|
||||
am: "Amharic",
|
||||
"am-et": "Amharic (Ethiopia)",
|
||||
ha: "Hausa",
|
||||
"ha-ng": "Hausa (Nigeria)",
|
||||
yo: "Yoruba",
|
||||
"yo-ng": "Yoruba (Nigeria)",
|
||||
ig: "Igbo",
|
||||
"ig-ng": "Igbo (Nigeria)",
|
||||
zu: "Zulu",
|
||||
"zu-za": "Zulu (South Africa)",
|
||||
xh: "Xhosa",
|
||||
"xh-za": "Xhosa (South Africa)",
|
||||
af: "Afrikaans",
|
||||
"af-za": "Afrikaans (South Africa)",
|
||||
};
|
||||
|
||||
// 如果找到精确匹配,返回对应值
|
||||
if (languageMap[normalizedCode]) {
|
||||
return languageMap[normalizedCode];
|
||||
}
|
||||
|
||||
// 如果没有精确匹配,尝试匹配语言部分(如 en-xx -> English)
|
||||
const langPart = normalizedCode.split("-")[0];
|
||||
if (languageMap[langPart]) {
|
||||
return languageMap[langPart];
|
||||
}
|
||||
|
||||
// 如果都没有匹配,返回原始值(大写)
|
||||
return langCode.toUpperCase();
|
||||
}
|
||||
|
||||
export function getEngineName(engine: string) {
|
||||
const engineMap = {
|
||||
Blink: "Chrome Engine",
|
||||
WebKit: "Safari Engine",
|
||||
Gecko: "Firefox Engine",
|
||||
Trident: "IE Engine",
|
||||
EdgeHTML: "Edge Engine",
|
||||
Presto: "Opera Engine",
|
||||
};
|
||||
|
||||
return engineMap[engine] || `${engine} Engine`;
|
||||
}
|
||||
|
||||
export function getBotName(bot: boolean) {
|
||||
return bot === true ? "Bot" : "Human";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use server";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { UserRole } from "@prisma/client";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -25,7 +24,7 @@ export type UserRecordFormData = {
|
||||
tags: string;
|
||||
created_on?: string;
|
||||
modified_on?: string;
|
||||
active: number;
|
||||
active: number; // 0: inactive, 1: active, 2: pending
|
||||
};
|
||||
|
||||
export async function createUserRecord(
|
||||
@@ -89,9 +88,15 @@ export async function getUserRecords(
|
||||
role === "USER"
|
||||
? {
|
||||
userId,
|
||||
// active,
|
||||
active: {
|
||||
not: 2,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
: {
|
||||
active: {
|
||||
not: 2,
|
||||
},
|
||||
};
|
||||
const [total, list] = await prisma.$transaction([
|
||||
prisma.userRecord.count({
|
||||
where: option,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wr.do",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"author": {
|
||||
"name": "oiov",
|
||||
"url": "https://github.com/oiov"
|
||||
|
||||
BIN
public/_static/avatar.png
Normal file
BIN
public/_static/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "0.6.4",
|
||||
"versionName": "0.6.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "0.6.4",
|
||||
"versionName": "0.6.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
<?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/robots.txt</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/deploy</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/pricing</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/privacy</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/terms</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/chat</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-05-28T14:53:12.008Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/api/feature</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/chat</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker-zh</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/deploy</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/deploy-zh</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/email-zh</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/installation-zh</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/developer/quick-start-zh</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/pricing</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/privacy</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/terms</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-06-03T07:16:22.972Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
</urlset>
|
||||
File diff suppressed because one or more lines are too long
1
types/index.d.ts
vendored
1
types/index.d.ts
vendored
@@ -18,6 +18,7 @@ export type SiteConfig = {
|
||||
};
|
||||
openSignup: boolean;
|
||||
emailR2Domain: string;
|
||||
enableSubdomainApply: boolean;
|
||||
};
|
||||
|
||||
export type NavItem = {
|
||||
|
||||
Reference in New Issue
Block a user