refact: mult levels dashboard sidebar

This commit is contained in:
oiov
2025-10-30 17:11:49 +08:00
parent d56ddaf105
commit 7ee88c8026
27 changed files with 655 additions and 215 deletions

View File

@@ -92,7 +92,7 @@ export default function AppConfigs({}: {}) {
return (
<Card>
<Collapsible className="group">
<Collapsible className="group" defaultOpen>
<CollapsibleTrigger className="flex w-full items-center justify-between bg-neutral-50 px-4 py-5 dark:bg-neutral-900">
<div className="text-lg font-bold">{t("App Configs")}</div>
<Icons.chevronDown className="ml-auto size-4" />

View File

@@ -0,0 +1,11 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function SystemSettingsLoading() {
return (
<>
<Skeleton className="h-48 w-full rounded-lg" />
<Skeleton className="h-56 w-full rounded-lg" />
<Skeleton className="h-[400px] w-full rounded-lg" />
</>
);
}

View File

@@ -0,0 +1,38 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import DomainList from "../domain-list";
export const metadata = constructMetadata({
title: "System Settings",
description: "",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<DomainList
user={{
id: user.id,
name: user.name || "",
apiKey: user.apiKey || "",
email: user.email || "",
role: user.role,
team: user.team,
}}
action="/api/admin/domain"
/>
<p className="rounded-md border border-dashed bg-muted px-3 py-2 text-xs text-muted-foreground">
<strong>Note</strong>: Once the domain is bound to the project, it will
be used as a business domain to provide services, and direct access to
the business domain will redirect to the main site you deploy.
</p>
</>
);
}

View File

@@ -24,28 +24,6 @@ export default async function DashboardPage() {
<DashboardHeader heading="System Settings" text="" />
<AppConfigs />
<S3Configs />
<DomainList
user={{
id: user.id,
name: user.name || "",
apiKey: user.apiKey || "",
email: user.email || "",
role: user.role,
team: user.team,
}}
action="/api/admin/domain"
/>
<PlanList
user={{
id: user.id,
name: user.name || "",
apiKey: user.apiKey || "",
email: user.email || "",
role: user.role,
team: user.team,
}}
action="/api/admin/plan"
/>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function SystemSettingsLoading() {
return (
<>
<Skeleton className="h-48 w-full rounded-lg" />
<Skeleton className="h-56 w-full rounded-lg" />
<Skeleton className="h-[400px] w-full rounded-lg" />
</>
);
}

View File

@@ -0,0 +1,33 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import PlanList from "../plan-list";
export const metadata = constructMetadata({
title: "System Settings",
description: "",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<PlanList
user={{
id: user.id,
name: user.name || "",
apiKey: user.apiKey || "",
email: user.email || "",
role: user.role,
team: user.team,
}}
action="/api/admin/plan"
/>
</>
);
}

View File

@@ -579,21 +579,7 @@ export const CodeLight = ({ content }: { content: string }) => {
{i + 1}
</span>
{/* Code content */}
<span className="text-blue-400">
{line
.replace(
/function/,
(match) => `<span class="text-purple-400">${match}</span>`,
)
.replace(
/"[^"]*"/,
(match) => `<span class="text-green-400">${match}</span>`,
)
.replace(
/console/,
(match) => `<span class="text-yellow-400">${match}</span>`,
)}
</span>
<span className="text-blue-400">{line}</span>
</div>
))}
</code>

View File

@@ -0,0 +1,9 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function DashboardUrlsLoading() {
return (
<>
<Skeleton className="h-[400px] w-full rounded-lg" />
</>
);
}

View File

@@ -0,0 +1,24 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import Globe from "../globe";
export const metadata = constructMetadata({
title: "Globe Analytics",
description: "Display link's globe analytics.",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<Globe />
</>
);
}

View File

@@ -0,0 +1,10 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function DashboardUrlsLoading() {
return (
<>
<Skeleton className="h-[120px] w-full rounded-lg" />
<Skeleton className="h-[400px] w-full rounded-lg" />
</>
);
}

View File

@@ -0,0 +1,47 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import ApiReference from "@/components/shared/api-reference";
import { CodeLight } from "../../scrape/scrapes";
import LiveLog from "../live-logs";
export const metadata = constructMetadata({
title: "Live Logs",
description: "Display link's real-time live logs.",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<ApiReference
badge="POST /api/v1/short"
target="creating short urls"
link="/docs/short-urls#api-reference"
/>
<CodeLight
content={`
curl -X POST \\
-H "Content-Type: application/json" \\
-H "wrdo-api-key: YOUR_API_KEY" \\
-d '{
"target": "https://www.oiov.dev",
"url": "abc123",
"expiration": "-1",
"prefix": "wr.do",
"visible": 1,
"active": 1,
"password": ""
}' \\
https://wr.do/api/v1/short
`}
/>
</>
);
}

View File

@@ -47,10 +47,16 @@ export interface LogEntry {
isNew?: boolean; // New property to track newly added logs
}
export default function LiveLog({ admin = false }: { admin?: boolean }) {
export default function LiveLog({
admin = false,
live = false,
}: {
admin?: boolean;
live?: boolean;
}) {
const { theme } = useTheme();
const { mutate } = useSWRConfig();
const [isLive, setIsLive] = useState(false);
const [isLive, setIsLive] = useState(live);
const [logs, setLogs] = useState<LogEntry[]>([]);
const [limitDiplay, setLimitDisplay] = useState(100);
const newLogsRef = useRef<Set<string>>(new Set()); // Track new log keys

View File

@@ -0,0 +1,11 @@
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function DashboardUrlsLoading() {
return (
<>
<DashboardHeader heading="Live Logs" text="" />
<Skeleton className="h-[400px] w-full rounded-lg" />
</>
);
}

View File

@@ -0,0 +1,25 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import LiveLog from "../live-logs";
export const metadata = constructMetadata({
title: "Live Logs",
description: "Display link's real-time live logs.",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<DashboardHeader heading="Live Logs" text="" />
<LiveLog live={true} />
</>
);
}

View File

@@ -647,17 +647,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</>
);
const rendLogs = () => (
<div className="mt-6 space-y-3">
{action.indexOf("admin") > -1 ? <LiveLog admin={true} /> : <LiveLog />}
<ApiReference
badge="POST /api/v1/short"
target="creating short urls"
link="/docs/short-urls#api-reference"
/>
</div>
);
return (
<>
<Tabs
@@ -678,13 +667,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
<Icons.layoutGrid className="size-4" />
{/* Grid */}
</TabsTrigger>
<TabsTrigger
onClick={() => setCurrentView("Realtime")}
value="Realtime"
>
<Icons.globe className="size-4 text-blue-500" />
{/* Realtime */}
</TabsTrigger>
{selectedUrl?.id && (
<TabsTrigger
className="flex items-center gap-1 text-muted-foreground"
@@ -732,16 +714,11 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
{pathname !== "/dashboard" && <UrlStatus action={action} />}
{rendeSeachInputs()}
{rendeList()}
{rendLogs()}
</TabsContent>
<TabsContent className="space-y-3" value="Grid">
{pathname !== "/dashboard" && <UrlStatus action={action} />}
{rendeSeachInputs()}
{rendeGrid()}
{rendLogs()}
</TabsContent>
<TabsContent value="Realtime">
{action.indexOf("admin") > -1 ? <Globe isAdmin={true} /> : <Globe />}
</TabsContent>
{selectedUrl?.id && (
<TabsContent value={selectedUrl.id}>

View File

@@ -3,7 +3,7 @@
"short_name": "WR.DO",
"description": "All-in-one domain platform with short links, temp email, subdomain management, file storage, and open APIs.",
"appid": "com.wr.do",
"versionName": "1.1.6",
"versionName": "1.1.7",
"versionCode": "1",
"start_url": "/",
"orientation": "portrait",

View File

@@ -3,7 +3,7 @@
import { Fragment, useEffect, useState } from "react";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { SidebarNavItem } from "@/types";
import { NavItem, SidebarNavItem } from "@/types";
import { Menu, PanelLeftClose, PanelRightClose } from "lucide-react";
import { useTranslations } from "next-intl";
import { Link } from "next-view-transitions";
@@ -24,6 +24,12 @@ import {
} from "@/components/ui/tooltip";
import { Icons } from "@/components/shared/icons";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
interface DashboardSidebarProps {
links: SidebarNavItem[];
}
@@ -34,18 +40,183 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
const { isTablet } = useMediaQuery();
const [isSidebarExpanded, setIsSidebarExpanded] = useState(!isTablet);
const [openCollapsibles, setOpenCollapsibles] = useState<Set<string>>(
new Set(),
);
const toggleSidebar = () => {
setIsSidebarExpanded(!isSidebarExpanded);
};
const toggleCollapsible = (itemTitle: string) => {
setOpenCollapsibles((prev) => {
const newSet = new Set(prev);
if (newSet.has(itemTitle)) {
newSet.delete(itemTitle);
} else {
newSet.add(itemTitle);
}
return newSet;
});
};
useEffect(() => {
setIsSidebarExpanded(!isTablet);
}, [isTablet]);
// Auto-open collapsibles that contain the current path
useEffect(() => {
links.forEach((section) => {
section.items.forEach((item) => {
if (item.items) {
const hasActivePath = item.items.some(
(subItem) => subItem.href === path,
);
if (hasActivePath) {
setOpenCollapsibles((prev) => new Set(prev).add(item.title));
}
}
});
});
}, [path, links]);
const renderNavItem = (item: NavItem, isNested = false) => {
const Icon = item.icon ? Icons[item.icon] : () => null;
const hasSubItems = item.items && item.items.length > 0;
const isOpen = openCollapsibles.has(item.title);
// Item with sub-items (collapsible)
if (hasSubItems) {
return (
<Fragment key={`nav-item-${item.title}`}>
{isSidebarExpanded ? (
<Collapsible
open={isOpen}
onOpenChange={() => toggleCollapsible(item.title)}
>
<CollapsibleTrigger
className={cn(
"flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
"text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<Icon className="size-5" />
{t(item.title)}
<Icons.chevronDown
className={cn(
"ml-auto size-4 transition-transform",
isOpen && "rotate-180",
)}
/>
</CollapsibleTrigger>
<CollapsibleContent className="pl-4 pt-1">
<div className="flex flex-col gap-0.5">
{item.items!.map((subItem) => renderNavItem(subItem, true))}
</div>
</CollapsibleContent>
</Collapsible>
) : (
<Tooltip key={`tooltip-${item.title}`}>
<TooltipTrigger asChild>
<div
className={cn(
"flex items-center gap-3 rounded-md py-2 text-sm font-medium hover:bg-muted",
"text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<span className="flex size-full items-center justify-center">
<Icon className="size-5" />
</span>
</div>
</TooltipTrigger>
<TooltipContent side="right">
<div className="flex flex-col gap-2">
{item.items!.map((subItem) =>
subItem.disabled ? (
<span className="cursor-pointer text-muted-foreground">
{t(subItem.title)}
</span>
) : (
<Link
key={subItem.title}
href={subItem.href || "#"}
className="hover:underline"
>
{t(subItem.title)}
</Link>
),
)}
</div>
</TooltipContent>
</Tooltip>
)}
</Fragment>
);
}
// Regular link item
if (item.href) {
return (
<Fragment key={`link-fragment-${item.title}`}>
{isSidebarExpanded ? (
<Link
key={`link-${item.title}`}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
path === item.href
? "bg-muted"
: "text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
isNested && "pl-6",
)}
>
<Icon className="size-5" />
{t(item.title)}
{item.badge && (
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
{item.badge}
</Badge>
)}
</Link>
) : (
<Tooltip key={`tooltip-${item.title}`}>
<TooltipTrigger asChild>
<Link
key={`link-tooltip-${item.title}`}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center gap-3 rounded-md py-2 text-sm font-medium hover:bg-muted",
path === item.href
? "bg-muted"
: "text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<span className="flex size-full items-center justify-center">
<Icon className="size-5" />
</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">{t(item.title)}</TooltipContent>
</Tooltip>
)}
</Fragment>
);
}
return null;
};
return (
<TooltipProvider delayDuration={0}>
<div className="sticky top-0 h-full">
<div className="sticky top-0 z-[40] h-full">
<ScrollArea className="h-full overflow-y-auto border-r">
<aside
className={cn(
@@ -55,8 +226,6 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
>
<div className="flex h-full max-h-screen flex-1 flex-col gap-2">
<div className="flex h-14 items-center gap-2 p-4 lg:h-[60px]">
{/* {isSidebarExpanded ? <ProjectSwitcher /> : null} */}
{isSidebarExpanded && (
<>
<Icons.logo />
@@ -105,61 +274,7 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
) : (
<div className="h-4" />
)}
{section.items.map((item) => {
const Icon = Icons[item.icon || "arrowRight"];
return (
item.href && (
<Fragment key={`link-fragment-${item.title}`}>
{isSidebarExpanded ? (
<Link
key={`link-${item.title}`}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
path === item.href
? "bg-muted"
: "text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<Icon className="size-5" />
{t(item.title)}
{item.badge && (
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
{item.badge}
</Badge>
)}
</Link>
) : (
<Tooltip key={`tooltip-${item.title}`}>
<TooltipTrigger asChild>
<Link
key={`link-tooltip-${item.title}`}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center gap-3 rounded-md py-2 text-sm font-medium hover:bg-muted",
path === item.href
? "bg-muted"
: "text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<span className="flex size-full items-center justify-center">
<Icon className="size-5" />
</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">
{t(item.title)}
</TooltipContent>
</Tooltip>
)}
</Fragment>
)
);
})}
{section.items.map((item) => renderNavItem(item))}
</section>
),
)}
@@ -200,9 +315,114 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
const path = usePathname();
const [open, setOpen] = useState(false);
const [openCollapsibles, setOpenCollapsibles] = useState<Set<string>>(
new Set(),
);
const { isSm, isMobile } = useMediaQuery();
const t = useTranslations("System");
const toggleCollapsible = (itemTitle: string) => {
setOpenCollapsibles((prev) => {
const newSet = new Set(prev);
if (newSet.has(itemTitle)) {
newSet.delete(itemTitle);
} else {
newSet.add(itemTitle);
}
return newSet;
});
};
// Auto-open collapsibles that contain the current path
useEffect(() => {
links.forEach((section) => {
section.items.forEach((item) => {
if (item.items) {
const hasActivePath = item.items.some(
(subItem) => subItem.href === path,
);
if (hasActivePath) {
setOpenCollapsibles((prev) => new Set(prev).add(item.title));
}
}
});
});
}, [path, links]);
const renderMobileNavItem = (item: NavItem, isNested = false) => {
const Icon = item.icon ? Icons[item.icon] : () => null;
const hasSubItems = item.items && item.items.length > 0;
const isOpen = openCollapsibles.has(item.title);
// Item with sub-items (collapsible)
if (hasSubItems) {
return (
<Collapsible
key={`nav-item-${item.title}`}
open={isOpen}
onOpenChange={() => toggleCollapsible(item.title)}
>
<CollapsibleTrigger
className={cn(
"flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
"text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<Icon className="size-5" />
{t(item.title)}
<Icons.chevronDown
className={cn(
"ml-auto size-4 transition-transform",
isOpen && "rotate-180",
)}
/>
</CollapsibleTrigger>
<CollapsibleContent className="pl-4 pt-1">
<div className="flex flex-col gap-0.5">
{item.items!.map((subItem) => renderMobileNavItem(subItem, true))}
</div>
</CollapsibleContent>
</Collapsible>
);
}
// Regular link item
if (item.href) {
return (
<Fragment key={`link-fragment-${item.title}`}>
<Link
key={`link-${item.title}`}
onClick={() => {
if (!item.disabled) setOpen(false);
}}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
path === item.href
? "bg-muted"
: "text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
isNested && "pl-6",
)}
>
<Icon className="size-5" />
{t(item.title)}
{item.badge && (
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
{item.badge}
</Badge>
)}
</Link>
</Fragment>
);
}
return null;
};
if (isSm || isMobile) {
return (
<Sheet open={open} onOpenChange={setOpen}>
@@ -224,7 +444,6 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
href="/"
className="flex items-center gap-2 text-lg font-semibold"
>
{/* <Icons.logo /> */}
<Image src="/favicon.ico" alt="logo" width={20} height={20} />
<span
style={{ fontFamily: "Bahamas Bold" }}
@@ -245,38 +464,7 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
{t(section.title)}
</p>
{section.items.map((item) => {
const Icon = Icons[item.icon || "arrowRight"];
return (
item.href && (
<Fragment key={`link-fragment-${item.title}`}>
<Link
key={`link-${item.title}`}
onClick={() => {
if (!item.disabled) setOpen(false);
}}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
path === item.href
? "bg-muted"
: "text-muted-foreground hover:text-accent-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<Icon className="size-5" />
{t(item.title)}
{item.badge && (
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
{item.badge}
</Badge>
)}
</Link>
</Fragment>
)
);
})}
{section.items.map((item) => renderMobileNavItem(item))}
</section>
),
)}
@@ -303,10 +491,6 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
v{pkg.version}
</Link>
</div>
{/* <div className="mt-auto">
<UpgradeCard />
</div> */}
</nav>
</div>
</ScrollArea>

View File

@@ -7,6 +7,7 @@ import {
ArrowUpRight,
BookOpen,
BotMessageSquare,
Boxes,
Braces,
Bug,
Calendar,
@@ -39,6 +40,7 @@ import {
Inbox,
Info,
Laptop,
Layers,
LayoutGrid,
LayoutPanelLeft,
Link,
@@ -115,7 +117,9 @@ export const Icons = {
scanQrCode: ScanQrCode,
monitorDown: MonitorDown,
shieldCheck: ShieldCheck,
layers: Layers,
databaseZap: DatabaseZap,
boxes: Boxes,
cloudUpload: ({ ...props }: LucideProps) => (
<svg
role="presentation"

View File

@@ -61,7 +61,7 @@ export function Modal({
}
}}
>
<Drawer.Overlay className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" />
<Drawer.Overlay className="fixed inset-0 z-[40] bg-background/80 backdrop-blur-sm" />
<Drawer.Portal>
<Drawer.Content
className={cn(

View File

@@ -9,13 +9,42 @@ export const sidebarLinks: SidebarNavItem[] = [
title: "MENU",
items: [
{ href: "/dashboard", icon: "dashboard", title: "Dashboard" },
{ href: "/dashboard/urls", icon: "link", title: "Short Urls" },
{ href: "/dashboard/records", icon: "globe", title: "DNS Records" },
{ href: "/emails", icon: "mail", title: "Emails" },
{
href: "/dashboard/storage",
href: "",
icon: "link",
title: "Short Urls",
items: [
{ href: "/dashboard/urls", title: "Links" },
{ href: "/dashboard/urls/analytics", title: "Analytics" },
{ href: "/dashboard/urls/logs", title: "Ip Logs" },
{ href: "/dashboard/urls/api", title: "API" },
],
},
{ href: "/dashboard/records", icon: "globe", title: "DNS Records" },
{
href: "",
icon: "mail",
title: "Emails",
items: [
{ href: "/emails", title: "Inbox" },
{ href: "/emails/sent", title: "Sent", disabled: true },
{ href: "/emails/trash", title: "Trash", disabled: true },
{ href: "/emails/api", title: "API", disabled: true },
],
},
{
href: "",
icon: "storage",
title: "Cloud Storage",
items: [
{ href: "/dashboard/storage", title: "Storage" },
{
href: "/dashboard/storage/analytics",
title: "File Analytics",
disabled: true,
},
{ href: "/dashboard/storage/api", title: "API", disabled: true },
],
},
],
},
@@ -24,28 +53,31 @@ export const sidebarLinks: SidebarNavItem[] = [
items: [
{
href: "/dashboard/scrape",
icon: "bug",
icon: "layers",
title: "Overview",
},
{
href: "/dashboard/scrape/screenshot",
icon: "camera",
title: "Screenshot",
},
{
href: "/dashboard/scrape/qrcode",
icon: "qrcode",
title: "QR Code",
},
{
href: "/dashboard/scrape/meta-info",
icon: "globe",
title: "Meta Info",
},
{
href: "/dashboard/scrape/markdown",
icon: "fileText",
title: "Markdown",
href: "",
icon: "bug",
title: "APIs",
items: [
{
href: "/dashboard/scrape/screenshot",
title: "Screenshot",
},
{
href: "/dashboard/scrape/qrcode",
title: "QR Code",
},
{
href: "/dashboard/scrape/meta-info",
title: "Meta Info",
},
{
href: "/dashboard/scrape/markdown",
title: "Markdown",
},
],
},
],
},
@@ -58,35 +90,60 @@ export const sidebarLinks: SidebarNavItem[] = [
title: "Admin Panel",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/users",
icon: "users",
title: "Users",
href: "/admin/resources",
icon: "boxes",
title: "Resources",
authorizeOnly: UserRole.ADMIN,
items: [
{
href: "/admin/users",
title: "Users",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/urls",
// icon: "link",
title: "URLs",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/records",
// icon: "globe",
title: "Records",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/storage",
// icon: "storage",
title: "Cloud Storage Manage",
authorizeOnly: UserRole.ADMIN,
},
],
},
{
href: "/admin/urls",
icon: "link",
title: "URLs",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/records",
icon: "globe",
title: "Records",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/storage",
icon: "storage",
title: "Cloud Storage Manage",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/system",
href: "",
icon: "settings",
title: "System Settings",
authorizeOnly: UserRole.ADMIN,
items: [
{
href: "/admin/system",
title: "App Configs",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/system/domains",
title: "Domains",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/system/plans",
title: "Plans",
authorizeOnly: UserRole.ADMIN,
},
],
},
],
},

View File

@@ -591,7 +591,21 @@
"Log out": "Log out",
"System Settings": "System Settings",
"Cloud Storage": "Cloud Storage",
"Cloud Storage Manage": "Cloud Storage"
"Cloud Storage Manage": "Cloud Storage",
"Inbox": "Inbox",
"Sent": "Sent",
"Spam": "Spam",
"Trash": "Trash",
"API": "API",
"Links": "Links",
"Analytics": "Analytics",
"Ip Logs": "Ip Logs",
"APIs": "APIs",
"File Analytics": "File Analytics",
"Storage": "Storage",
"Resources": "Resources",
"App Configs": "App Settings",
"Plans": "Plan Settings"
},
"Email": {
"Search emails": "Search emails",

View File

@@ -565,9 +565,9 @@
"WRoom": "聊天室",
"OPEN API": "开放API",
"Overview": "概览面板",
"Screenshot": "截图API",
"QR Code": "二维码API",
"Meta Info": "元数据API",
"Screenshot": "网页截图",
"QR Code": "网页二维码",
"Meta Info": "元数据",
"Markdown": "Markdown",
"ADMIN": "管理员",
"Admin Panel": "概览面板",
@@ -590,7 +590,21 @@
"Log out": "退出登录",
"System Settings": "系统设置",
"Cloud Storage": "云存储",
"Cloud Storage Manage": "云存储管理"
"Cloud Storage Manage": "云存储管理",
"Inbox": "收件箱",
"Sent": "已发送",
"Spam": "垃圾邮件",
"Trash": "废纸篓",
"API": "API",
"Links": "我的链接",
"Analytics": "访客统计",
"Ip Logs": "实时日志",
"APIs": "APIs",
"File Analytics": "文件统计",
"Storage": "存储桶",
"Resources": "资源管理",
"App Configs": "全局配置",
"Plans": "配额设置"
},
"Email": {
"Search emails": "搜索邮箱...",

View File

@@ -1,6 +1,6 @@
{
"name": "wr.do",
"version": "1.1.6",
"version": "1.1.7",
"author": {
"name": "oiov",
"url": "https://github.com/oiov"

View File

@@ -3,7 +3,7 @@
"short_name": "WR.DO",
"description": "All-in-one domain platform with short links, temp email, subdomain management, file storage, and open APIs.",
"appid": "com.wr.do",
"versionName": "1.1.6",
"versionName": "1.1.7",
"versionCode": "1",
"start_url": "/",
"orientation": "portrait",

View File

@@ -3,7 +3,7 @@
"short_name": "WR.DO",
"description": "All-in-one domain platform with short links, temp email, subdomain management, file storage, and open APIs.",
"appid": "com.wr.do",
"versionName": "1.1.6",
"versionName": "1.1.7",
"versionCode": "1",
"start_url": "/",
"orientation": "portrait",

File diff suppressed because one or more lines are too long

1
types/index.d.ts vendored
View File

@@ -27,6 +27,7 @@ export type NavItem = {
external?: boolean;
authorizeOnly?: UserRole;
icon?: keyof typeof Icons;
items?: NavItem[];
};
export type MainNavItem = NavItem;