Refactor navigation structure and API routes
- Centralize user menu, sidebar items, and breadcrumb logic. - Map consistent API endpoints in `customerRoutes`. - Replace inline route definitions with reusable constants. - Refactor auth configuration file location. - Improve `<Link>` usage to replace static `<a>` elements. - Adjust sidebar and dropdown components to use dynamic navigation configurations.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import {authOptions} from "@/lib/auth/authOptions";
|
import {authOptions} from "@/lib/api/auth/authOptions";
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions);
|
||||||
export {handler as GET, handler as POST};
|
export {handler as GET, handler as POST};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {NextRequest, NextResponse} from "next/server";
|
import {NextRequest, NextResponse} from "next/server";
|
||||||
import {serverCall} from "@/lib/api/serverCall";
|
import {serverCall} from "@/lib/api/serverCall";
|
||||||
|
import {customerRoutes} from "@/app/api/customers/customerRoutes";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const id = request.url.split('/').pop();
|
const id = request.url.split('/').pop();
|
||||||
const response = await serverCall(`/customers/${id}`, "GET");
|
const response = await serverCall(customerRoutes.getById(id!), "GET");
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return NextResponse.json({error: "Customer not found"}, {status: 404});
|
return NextResponse.json({error: "Customer not found"}, {status: 404});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const customerRoutes = {
|
export const customerRoutes = {
|
||||||
create: "/customers",
|
create: "/customers",
|
||||||
validate: "/customers/validate",
|
validate: "/customers/validate",
|
||||||
|
getById: (id: string) => `/customers/${id}`,
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import {NextRequest, NextResponse} from "next/server";
|
import {NextRequest, NextResponse} from "next/server";
|
||||||
import {serverCall} from "@/lib/api/serverCall";
|
import {serverCall} from "@/lib/api/serverCall";
|
||||||
|
import {customerRoutes} from "@/app/api/customers/customerRoutes";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const data = await serverCall("/customers", "GET");
|
const data = await serverCall(customerRoutes.create, "GET");
|
||||||
const customers = await data.json();
|
const customers = await data.json();
|
||||||
return NextResponse.json(customers);
|
return NextResponse.json(customers);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const result = await serverCall("/customers", "POST", body);
|
const result = await serverCall(customerRoutes.create, "POST", body);
|
||||||
return NextResponse.json(result.json());
|
return NextResponse.json(result.json());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useState, useEffect, useMemo} from "react";
|
import {useState, useEffect, useMemo} from "react";
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Card, CardContent} from "@/components/ui/card";
|
import {Card, CardContent} from "@/components/ui/card";
|
||||||
@@ -25,9 +24,9 @@ import {ArrowRight} from "lucide-react";
|
|||||||
import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal";
|
import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {Customer} from "@/services/customers/entities/customer";
|
import {Customer} from "@/services/customers/entities/customer";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function CustomersPage() {
|
export default function CustomersPage() {
|
||||||
const router = useRouter();
|
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -50,7 +49,7 @@ export default function CustomersPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if(customers.length === 0) return [];
|
if (customers.length === 0) return [];
|
||||||
|
|
||||||
return customers.filter(
|
return customers.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -143,13 +142,11 @@ export default function CustomersPage() {
|
|||||||
<TableCell>{customer.city}</TableCell>
|
<TableCell>{customer.city}</TableCell>
|
||||||
<TableCell>{new Date(customer.createdAt).toLocaleString()}</TableCell>
|
<TableCell>{new Date(customer.createdAt).toLocaleString()}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Link href={`/customers/${customer.id}`}>
|
||||||
variant="ghost"
|
<Button variant="ghost" size="icon">
|
||||||
size="icon"
|
<ArrowRight className="w-4 h-4"/>
|
||||||
onClick={() => router.push(`/customers/${customer.id}`)}
|
</Button>
|
||||||
>
|
</Link>
|
||||||
<ArrowRight className="w-4 h-4"/>
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {Separator} from "@/components/ui/separator";
|
|||||||
import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb";
|
import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb";
|
||||||
import {getServerSession} from "next-auth";
|
import {getServerSession} from "next-auth";
|
||||||
import LoginScreen from "@/components/login-screen";
|
import LoginScreen from "@/components/login-screen";
|
||||||
import {authOptions} from "@/lib/auth/authOptions";
|
import {authOptions} from "@/lib/api/auth/authOptions";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Internal | Rhein Software",
|
title: "Internal | Rhein Software",
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
AppWindowIcon,
|
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Home,
|
|
||||||
Scale,
|
Scale,
|
||||||
User2,
|
User2,
|
||||||
Settings, LayoutDashboard
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -23,40 +21,20 @@ import {
|
|||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
|
import {rheinItems, customerItems, kanzleiItems} from "@/lib/navigation/sidebar-items";
|
||||||
|
import {userMenuItems} from "@/lib/navigation/user-menu-items";
|
||||||
|
|
||||||
const rheinItems = [
|
|
||||||
{
|
|
||||||
title: "Dashboard",
|
|
||||||
url: "/",
|
|
||||||
icon: Home,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Apps",
|
|
||||||
url: "/apps",
|
|
||||||
icon: AppWindowIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const customerItems = [
|
|
||||||
{
|
|
||||||
title: "Kundenübersicht",
|
|
||||||
url: "/customers",
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
return (
|
return (
|
||||||
@@ -74,10 +52,10 @@ export function AppSidebar() {
|
|||||||
asChild
|
asChild
|
||||||
className="hover:bg-accent hover:text-accent-foreground"
|
className="hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<a href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon/>
|
<item.icon/>
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</a>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
@@ -95,10 +73,10 @@ export function AppSidebar() {
|
|||||||
asChild
|
asChild
|
||||||
className="hover:bg-accent hover:text-accent-foreground"
|
className="hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<a href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon/>
|
<item.icon/>
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</a>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
@@ -117,11 +95,10 @@ export function AppSidebar() {
|
|||||||
asChild
|
asChild
|
||||||
className="hover:bg-accent hover:text-accent-foreground"
|
className="hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
|
<Link href="/demo/settings">
|
||||||
<a href="/demo/settings">
|
|
||||||
<Settings/>
|
<Settings/>
|
||||||
<span>Demo Settings</span>
|
<span>Demo Settings</span>
|
||||||
</a>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -144,21 +121,15 @@ export function AppSidebar() {
|
|||||||
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub className="ml-4 border-l border-border pl-4 flex flex-col gap-y-1">
|
<SidebarMenuSub className="ml-4 border-l border-border pl-4 flex flex-col gap-y-1">
|
||||||
<SidebarMenuSubItem>
|
{kanzleiItems.map((item) => (
|
||||||
<SidebarMenuSubButton href="/demo/kanzlei/steuer">
|
<SidebarMenuSubItem key={item.title}>
|
||||||
Steuer
|
<SidebarMenuSubButton asChild>
|
||||||
</SidebarMenuSubButton>
|
<Link href={item.url}>
|
||||||
</SidebarMenuSubItem>
|
{item.title}
|
||||||
<SidebarMenuSubItem>
|
</Link>
|
||||||
<SidebarMenuSubButton href="/demo/kanzlei/rechtsanwalt">
|
</SidebarMenuSubButton>
|
||||||
Rechtsanwalt
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSubButton>
|
))}
|
||||||
</SidebarMenuSubItem>
|
|
||||||
<SidebarMenuSubItem>
|
|
||||||
<SidebarMenuSubButton href="/demo/kanzlei/bilanzbuchhalter">
|
|
||||||
Bilanzbuchhalter
|
|
||||||
</SidebarMenuSubButton>
|
|
||||||
</SidebarMenuSubItem>
|
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@@ -183,15 +154,14 @@ export function AppSidebar() {
|
|||||||
side="top"
|
side="top"
|
||||||
className="w-[--radix-popper-anchor-width]"
|
className="w-[--radix-popper-anchor-width]"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem>
|
{userMenuItems.map((item) => (
|
||||||
<span>Account</span>
|
<DropdownMenuItem key={item.title} asChild>
|
||||||
</DropdownMenuItem>
|
<Link href={item.url}>
|
||||||
<DropdownMenuItem>
|
{item.icon && <item.icon className="mr-2 h-4 w-4"/>}
|
||||||
<span>Billing</span>
|
<span>{item.title}</span>
|
||||||
</DropdownMenuItem>
|
</Link>
|
||||||
<DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<span>Sign out</span>
|
))}
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -199,4 +169,4 @@ export function AppSidebar() {
|
|||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from '@/components/ui/breadcrumb';
|
} from '@/components/ui/breadcrumb';
|
||||||
import {Skeleton} from "@/components/ui/skeleton";
|
import {Skeleton} from "@/components/ui/skeleton";
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {getBreadcrumbs} from '@/utils/BreadcrumbUtils';
|
import {getBreadcrumbs} from '@/services/navigation/breadcrumb-utils';
|
||||||
import {breadcrumbResolvers} from "@/lib/breadcrumb-map";
|
import {breadcrumbResolvers} from "@/lib/navigation/breadcrumb-map";
|
||||||
|
|
||||||
interface ResolvedBreadcrumb {
|
interface ResolvedBreadcrumb {
|
||||||
href: string;
|
href: string;
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ const {
|
|||||||
NEXTAUTH_SECRET,
|
NEXTAUTH_SECRET,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
// if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID");
|
if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID");
|
||||||
// if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET");
|
if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET");
|
||||||
// if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER");
|
if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER");
|
||||||
// if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET");
|
if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET");
|
||||||
|
|
||||||
console.log("[auth] Using Keycloak provider:");
|
console.log("[auth] Using Keycloak provider:");
|
||||||
console.log(" - Client ID:", KEYCLOAK_CLIENT_ID);
|
console.log(" - Client ID:", KEYCLOAK_CLIENT_ID);
|
||||||
@@ -42,8 +42,8 @@ async function isTokenValid(token: string): Promise<boolean> {
|
|||||||
export const authOptions: NextAuthOptions = {
|
export const authOptions: NextAuthOptions = {
|
||||||
providers: [
|
providers: [
|
||||||
KeycloakProvider({
|
KeycloakProvider({
|
||||||
clientId: KEYCLOAK_CLIENT_ID as string,
|
clientId: KEYCLOAK_CLIENT_ID,
|
||||||
clientSecret: KEYCLOAK_CLIENT_SECRET as string,
|
clientSecret: KEYCLOAK_CLIENT_SECRET,
|
||||||
issuer: KEYCLOAK_ISSUER,
|
issuer: KEYCLOAK_ISSUER,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// lib/callBackendApi.ts
|
// lib/callBackendApi.ts
|
||||||
import {getServerSession} from "next-auth";
|
import {getServerSession} from "next-auth";
|
||||||
import {authOptions} from "@/lib/auth/authOptions";
|
import {authOptions} from "@/lib/api/auth/authOptions";
|
||||||
|
|
||||||
export async function serverCall(
|
export async function serverCall(
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
// lib/breadcrumb-map.ts
|
import {customerRoutes} from "@/app/api/customers/customerRoutes";
|
||||||
|
|
||||||
export const breadcrumbMap: Record<string, string> = {
|
export const breadcrumbMap: Record<string, string> = {
|
||||||
'dashboard': 'Dashboard',
|
'dashboard': 'Dashboard',
|
||||||
'settings': 'Settings',
|
'settings': 'Settings',
|
||||||
'demo': 'Demo',
|
'demo': 'Demo',
|
||||||
'users': 'User Management',
|
'users': 'User Management',
|
||||||
'customers': 'Kundenübersicht',
|
'customers': 'Kundenübersicht',
|
||||||
// Add more mappings as needed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const breadcrumbResolvers: Record<string, (id: string) => Promise<string>> = {
|
export const breadcrumbResolvers: Record<string, (id: string) => Promise<string>> = {
|
||||||
"customers": async (id: string) => {
|
"customers": async (id: string) => {
|
||||||
const res = await fetch(`/api/customers/${id}`, {cache: "no-store"});
|
const res = await fetch(`/api${customerRoutes.getById(id)}`, {cache: "no-store"});
|
||||||
const customer = await res .json();
|
const customer = await res.json();
|
||||||
if (customer.companyName) return `Firma: ${customer.companyName}`;
|
if (customer.companyName) return `Firma: ${customer.companyName}`;
|
||||||
if (customer.name) return `Name: ${customer.name}`;
|
if (customer.name) return `Name: ${customer.name}`;
|
||||||
return `ID: ${id}`;
|
return `ID: ${id}`;
|
||||||
},
|
},
|
||||||
// Add more mappings as needed
|
|
||||||
};
|
};
|
||||||
38
internal_frontend/lib/navigation/sidebar-items.ts
Normal file
38
internal_frontend/lib/navigation/sidebar-items.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {AppWindowIcon, Home, LayoutDashboard} from "lucide-react";
|
||||||
|
import {MenuItem, SubMenuItem} from "@/types/navigation/sidebar";
|
||||||
|
|
||||||
|
export const rheinItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
title: "Dashboard",
|
||||||
|
url: "/",
|
||||||
|
icon: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Apps",
|
||||||
|
url: "/apps",
|
||||||
|
icon: AppWindowIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const customerItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
title: "Kundenübersicht",
|
||||||
|
url: "/customers",
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const kanzleiItems: SubMenuItem[] = [
|
||||||
|
{
|
||||||
|
title: "Steuer",
|
||||||
|
url: "/demo/kanzlei/steuer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Rechtsanwalt",
|
||||||
|
url: "/demo/kanzlei/rechtsanwalt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bilanzbuchhalter",
|
||||||
|
url: "/demo/kanzlei/bilanzbuchhalter",
|
||||||
|
},
|
||||||
|
];
|
||||||
20
internal_frontend/lib/navigation/user-menu-items.ts
Normal file
20
internal_frontend/lib/navigation/user-menu-items.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {LogOut, Settings} from "lucide-react";
|
||||||
|
|
||||||
|
export interface UserMenuItem {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
icon?: typeof Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userMenuItems: UserMenuItem[] = [
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
url: "/settings",
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ausloggen",
|
||||||
|
url: "/api/auth/signout",
|
||||||
|
icon: LogOut,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {breadcrumbMap} from "@/lib/breadcrumb-map";
|
import {breadcrumbMap} from "@/lib/navigation/breadcrumb-map";
|
||||||
|
|
||||||
export interface Breadcrumb {
|
export interface Breadcrumb {
|
||||||
href: string;
|
href: string;
|
||||||
12
internal_frontend/types/navigation/sidebar.ts
Normal file
12
internal_frontend/types/navigation/sidebar.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {LucideIcon} from "lucide-react";
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubMenuItem {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user