Verwaltung: QR-Typen einzeln freigeben oder ausblenden
Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete Typen verschwinden vollstaendig von der Startseite und lassen sich nicht mehr neu anlegen. - Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit eine frische Installation ohne Vorbelegung laeuft und spaetere Typen automatisch erscheinen. - Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch, ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich die Freigabe aus der Datenbank (src/lib/qr/availability.ts). - Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein gedruckter Code darf durch einen Schalter nicht ungueltig werden. - resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts. - Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung. - Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||
import {
|
||||
getQrTypeAvailability,
|
||||
setQrTypeEnabled,
|
||||
} from "@/lib/qr/availability";
|
||||
import { QR_TYPE_IDS } from "@/lib/qr/type-module";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const updateSchema = z.object({
|
||||
type: z.enum(QR_TYPE_IDS, { error: "Unbekannter QR-Typ." }),
|
||||
enabled: z.boolean({ error: "Der Schalter muss wahr oder falsch sein." }),
|
||||
});
|
||||
|
||||
/** Zustand aller QR-Typen für die Verwaltung. */
|
||||
export async function GET(): Promise<Response> {
|
||||
try {
|
||||
const types = await getQrTypeAvailability();
|
||||
return Response.json({
|
||||
types: types.map(({ qrType, implemented, enabled, usable }) => ({
|
||||
id: qrType.id,
|
||||
title: qrType.title,
|
||||
description: qrType.description,
|
||||
icon: qrType.icon,
|
||||
implemented,
|
||||
enabled,
|
||||
usable,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Einen Typ freigeben oder abschalten. */
|
||||
export async function PATCH(request: NextRequest): Promise<Response> {
|
||||
try {
|
||||
const parsed = updateSchema.safeParse(await request.json());
|
||||
if (!parsed.success) {
|
||||
return jsonError(firstZodMessage(parsed.error));
|
||||
}
|
||||
|
||||
await setQrTypeEnabled(parsed.data.type, parsed.data.enabled);
|
||||
return Response.json({ type: parsed.data.type, enabled: parsed.data.enabled });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import type { Prisma } from "@/generated/prisma/client";
|
||||
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||
import { requireImplementedQrType } from "@/lib/qr/availability";
|
||||
import { designSchema } from "@/lib/qr/design";
|
||||
import { getActiveQrType } from "@/lib/qr/registry";
|
||||
import { QrRequestError } from "@/lib/qr/request";
|
||||
import { QrRequestError } from "@/lib/qr/type-module";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -60,12 +60,10 @@ export async function PATCH(
|
||||
const update: Prisma.QrCodeUpdateInput = {};
|
||||
|
||||
if (parsed.data.data !== undefined) {
|
||||
const qrType = getActiveQrType(existing.type);
|
||||
if (!qrType) {
|
||||
throw new QrRequestError(
|
||||
"Der Typ dieses Codes wird in dieser Version nicht unterstützt.",
|
||||
);
|
||||
}
|
||||
// Bewusst nur „ausprogrammiert“, nicht „freigegeben“: ein in der
|
||||
// Verwaltung abgeschalteter Typ soll bestehende Codes nicht
|
||||
// unbearbeitbar machen — sie sind längst gedruckt.
|
||||
const qrType = requireImplementedQrType(existing.type);
|
||||
const validated = qrType.validate(parsed.data.data);
|
||||
if (!validated.ok) {
|
||||
return jsonError(validated.message);
|
||||
|
||||
@@ -4,10 +4,9 @@ import { z } from "zod";
|
||||
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||
import { requireAvailableQrType } from "@/lib/qr/availability";
|
||||
import { designSchema } from "@/lib/qr/design";
|
||||
import { getActiveQrType } from "@/lib/qr/registry";
|
||||
import { QrRequestError } from "@/lib/qr/request";
|
||||
import { QR_TYPE_IDS } from "@/lib/qr/type-module";
|
||||
import { QR_TYPE_IDS, QrRequestError } from "@/lib/qr/type-module";
|
||||
import { generateSlug } from "@/lib/slug";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -51,13 +50,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
}
|
||||
|
||||
const { type, design, label } = parsed.data;
|
||||
const qrType = getActiveQrType(type);
|
||||
if (!qrType) {
|
||||
throw new QrRequestError(
|
||||
"Dieser QR-Typ ist in dieser Version noch nicht verfügbar.",
|
||||
404,
|
||||
);
|
||||
}
|
||||
const qrType = await requireAvailableQrType(type);
|
||||
if (!qrType.supportsDynamic) {
|
||||
throw new QrRequestError(
|
||||
`Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`,
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
const { qrType, data, content } = resolveQrRequest(parsed.data);
|
||||
const { qrType, data, content } = await resolveQrRequest(parsed.data);
|
||||
|
||||
const body =
|
||||
format === "png"
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
return jsonError(firstZodMessage(parsed.error));
|
||||
}
|
||||
|
||||
const { content } = resolveQrRequest(parsed.data);
|
||||
const { content } = await resolveQrRequest(parsed.data);
|
||||
const svg = await renderSvg(content, parsed.data.design);
|
||||
|
||||
return Response.json({
|
||||
|
||||
@@ -4,14 +4,13 @@ import { notFound } from "next/navigation";
|
||||
|
||||
import { Icon } from "@/components/icons";
|
||||
import { QrGenerator } from "@/components/qr-generator";
|
||||
import { QR_TYPE_IDS, getActiveQrType, getQrType } from "@/lib/qr/registry";
|
||||
import { getQrTypeState } from "@/lib/qr/availability";
|
||||
import { getQrType } from "@/lib/qr/registry";
|
||||
|
||||
type Props = { params: Promise<{ type: string }> };
|
||||
|
||||
/** Alle bekannten Typen vorab erzeugen — auch die noch inaktiven. */
|
||||
export function generateStaticParams() {
|
||||
return QR_TYPE_IDS.map((type) => ({ type }));
|
||||
}
|
||||
// Die Freigabe der Typen kommt aus der Datenbank und ist jederzeit änderbar.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { type } = await params;
|
||||
@@ -19,38 +18,60 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
return { title: qrType ? `${qrType.title}-QR-Code` : "QR-Code erstellen" };
|
||||
}
|
||||
|
||||
/** Erklärseite statt 404 — der Typ existiert, ist nur (noch) nicht nutzbar. */
|
||||
function Hinweis({
|
||||
icon,
|
||||
titel,
|
||||
text,
|
||||
}: {
|
||||
icon: Parameters<typeof Icon>[0]["name"];
|
||||
titel: string;
|
||||
text: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6">
|
||||
<span className="mx-auto flex size-14 items-center justify-center rounded-2xl bg-slate-100 text-slate-400">
|
||||
<Icon name={icon} className="size-7" />
|
||||
</span>
|
||||
<h1 className="mt-5 text-2xl font-bold text-slate-900">{titel}</h1>
|
||||
<p className="mt-2 text-slate-600">{text}</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-6 inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
||||
>
|
||||
Zur Typ-Auswahl
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function CreatePage({ params }: Props) {
|
||||
const { type } = await params;
|
||||
const qrType = getQrType(type);
|
||||
const state = await getQrTypeState(type);
|
||||
|
||||
if (!qrType) {
|
||||
if (!state) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Registrierte, aber noch nicht implementierte Typen bekommen eine
|
||||
// erklärende Seite statt einer 404.
|
||||
if (!getActiveQrType(type)) {
|
||||
if (!state.enabled) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6">
|
||||
<span className="mx-auto flex size-14 items-center justify-center rounded-2xl bg-slate-100 text-slate-400">
|
||||
<Icon name={qrType.icon} className="size-7" />
|
||||
</span>
|
||||
<h1 className="mt-5 text-2xl font-bold text-slate-900">
|
||||
„{qrType.title}“ ist bald verfügbar
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Dieser QR-Typ folgt in Version 2. In Version 1 steht der Typ „Website“
|
||||
zur Verfügung.
|
||||
</p>
|
||||
<Link
|
||||
href="/erstellen/website"
|
||||
className="mt-6 inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
||||
>
|
||||
Website-QR-Code erstellen
|
||||
</Link>
|
||||
</div>
|
||||
<Hinweis
|
||||
icon="lock"
|
||||
titel={`„${state.qrType.title}“ ist abgeschaltet`}
|
||||
text="Dieser QR-Typ wurde in der Verwaltung abgeschaltet. Ein Administrator kann ihn unter Verwaltung → QR-Typen wieder freigeben."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <QrGenerator typeId={qrType.id} />;
|
||||
if (!state.implemented) {
|
||||
return (
|
||||
<Hinweis
|
||||
icon={state.qrType.icon}
|
||||
titel={`„${state.qrType.title}“ ist bald verfügbar`}
|
||||
text="Dieser QR-Typ folgt in einer späteren Version. Bereits verfügbare Typen findest du in der Typ-Auswahl."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <QrGenerator typeId={state.qrType.id} />;
|
||||
}
|
||||
|
||||
+11
-3
@@ -18,7 +18,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
<html lang="de" className="h-full">
|
||||
<body className="flex min-h-full flex-col">
|
||||
<header className="border-b border-slate-200 bg-white">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3.5 sm:px-6">
|
||||
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-3.5 sm:px-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2.5 rounded-lg font-semibold text-slate-900"
|
||||
@@ -26,9 +26,11 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
<span className="flex size-9 items-center justify-center rounded-lg bg-indigo-600 text-white">
|
||||
<Icon name="qr" className="size-5" />
|
||||
</span>
|
||||
QR-Code-Generator
|
||||
{/* Auf schmalen Geräten reicht das Zeichen — sonst passt die
|
||||
Navigation nicht mehr in eine Zeile. */}
|
||||
<span className="hidden sm:inline">QR-Code-Generator</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-1 text-sm font-medium">
|
||||
<nav className="flex items-center gap-0.5 text-sm font-medium sm:gap-1">
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
@@ -41,6 +43,12 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
>
|
||||
Meine Codes
|
||||
</Link>
|
||||
<Link
|
||||
href="/verwaltung/typen"
|
||||
className="rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Verwaltung
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
+99
-66
@@ -1,10 +1,17 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Icon } from "@/components/icons";
|
||||
import { Badge } from "@/components/ui";
|
||||
import { qrTypeList } from "@/lib/qr/registry";
|
||||
import { Badge, Notice } from "@/components/ui";
|
||||
import { getVisibleQrTypes } from "@/lib/qr/availability";
|
||||
|
||||
// Welche Typen sichtbar sind, steht in der Datenbank und ist jederzeit
|
||||
// änderbar — die Startseite darf deshalb nicht vorgerendert werden.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function HomePage() {
|
||||
const types = await getVisibleQrTypes();
|
||||
const usableCount = types.filter((entry) => entry.usable).length;
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-10 sm:px-6 sm:py-14">
|
||||
<div className="max-w-2xl">
|
||||
@@ -18,76 +25,102 @@ export default function HomePage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="mt-9 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{qrTypeList.map((type) => {
|
||||
const tile = (
|
||||
<>
|
||||
<span
|
||||
className={
|
||||
type.active
|
||||
? "flex size-11 shrink-0 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600"
|
||||
: "flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-400"
|
||||
}
|
||||
>
|
||||
<Icon name={type.icon} className="size-6" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={
|
||||
type.active
|
||||
? "font-semibold text-slate-900"
|
||||
: "font-semibold text-slate-500"
|
||||
}
|
||||
>
|
||||
{type.title}
|
||||
</span>
|
||||
{type.active ? (
|
||||
<Badge tone="popular">Beliebt</Badge>
|
||||
) : (
|
||||
<Badge tone="soon">Bald verfügbar</Badge>
|
||||
)}
|
||||
</span>
|
||||
{usableCount === 0 ? (
|
||||
<div className="mt-9 max-w-2xl">
|
||||
<Notice tone="warning">
|
||||
{types.length === 0
|
||||
? "Zurzeit ist kein QR-Typ freigegeben."
|
||||
: "Zurzeit ist kein QR-Typ zum Erstellen freigegeben — die unten gezeigten Typen folgen erst in einer späteren Version."}{" "}
|
||||
Unter{" "}
|
||||
<Link href="/verwaltung/typen" className="font-medium underline">
|
||||
Verwaltung → QR-Typen
|
||||
</Link>{" "}
|
||||
lassen sich Typen wieder freigeben.
|
||||
</Notice>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{types.length === 0 ? null : (
|
||||
<ul className="mt-9 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{types.map(({ qrType, usable }) => {
|
||||
const tile = (
|
||||
<>
|
||||
<span
|
||||
className={
|
||||
type.active
|
||||
? "mt-1 block text-sm text-slate-600"
|
||||
: "mt-1 block text-sm text-slate-400"
|
||||
usable
|
||||
? "flex size-11 shrink-0 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600"
|
||||
: "flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-400"
|
||||
}
|
||||
>
|
||||
{type.description}
|
||||
<Icon name={qrType.icon} className="size-6" />
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={
|
||||
usable
|
||||
? "font-semibold text-slate-900"
|
||||
: "font-semibold text-slate-500"
|
||||
}
|
||||
>
|
||||
{qrType.title}
|
||||
</span>
|
||||
{usable ? (
|
||||
<Badge tone="popular">Beliebt</Badge>
|
||||
) : (
|
||||
<Badge tone="soon">Bald verfügbar</Badge>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
usable
|
||||
? "mt-1 block text-sm text-slate-600"
|
||||
: "mt-1 block text-sm text-slate-400"
|
||||
}
|
||||
>
|
||||
{qrType.description}
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={type.id}>
|
||||
{type.active ? (
|
||||
<Link
|
||||
href={`/erstellen/${type.id}`}
|
||||
className="flex h-full items-start gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
||||
>
|
||||
{tile}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
aria-disabled="true"
|
||||
title={`„${type.title}“ folgt in Version 2`}
|
||||
className="flex h-full cursor-not-allowed items-start gap-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50/60 p-4"
|
||||
>
|
||||
{tile}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
return (
|
||||
<li key={qrType.id}>
|
||||
{usable ? (
|
||||
<Link
|
||||
href={`/erstellen/${qrType.id}`}
|
||||
className="flex h-full items-start gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
||||
>
|
||||
{tile}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
aria-disabled="true"
|
||||
title={`„${qrType.title}“ folgt in einer späteren Version`}
|
||||
className="flex h-full cursor-not-allowed items-start gap-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50/60 p-4"
|
||||
>
|
||||
{tile}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<p className="mt-8 text-sm text-slate-500">
|
||||
In Version 1 ist der Typ „Website“ verfügbar. Die übrigen Typen folgen in
|
||||
Version 2.
|
||||
</p>
|
||||
{usableCount > 0 && usableCount < types.length ? (
|
||||
<p className="mt-8 text-sm text-slate-500">
|
||||
Die übrigen Typen folgen in einer späteren Version. Welche Kacheln
|
||||
hier erscheinen, legst du unter{" "}
|
||||
<Link
|
||||
href="/verwaltung/typen"
|
||||
className="font-medium text-slate-600 underline"
|
||||
>
|
||||
Verwaltung → QR-Typen
|
||||
</Link>{" "}
|
||||
fest.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Icon } from "@/components/icons";
|
||||
import { TypeSettings, type TypeSetting } from "@/components/type-settings";
|
||||
import { getQrTypeAvailability } from "@/lib/qr/availability";
|
||||
|
||||
export const metadata: Metadata = { title: "QR-Typen" };
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function TypeSettingsPage() {
|
||||
const availability = await getQrTypeAvailability();
|
||||
|
||||
const initial: TypeSetting[] = availability.map(
|
||||
({ qrType, implemented, enabled }) => ({
|
||||
id: qrType.id,
|
||||
title: qrType.title,
|
||||
description: qrType.description,
|
||||
icon: qrType.icon,
|
||||
implemented,
|
||||
enabled,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-10">
|
||||
<Link
|
||||
href="/"
|
||||
className="mb-5 inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-slate-600 transition-colors hover:text-slate-900"
|
||||
>
|
||||
<Icon name="arrowLeft" className="size-4" />
|
||||
Zur Typ-Auswahl
|
||||
</Link>
|
||||
|
||||
<div className="mb-7">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
|
||||
QR-Typen
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Lege fest, welche Typen auf der Startseite erscheinen. Abgeschaltete
|
||||
Typen sind komplett ausgeblendet und lassen sich nicht mehr neu
|
||||
anlegen.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Bereits gespeicherte dynamische Codes bleiben davon unberührt: sie
|
||||
leiten weiter und lassen sich weiter bearbeiten. Ein gedruckter Code
|
||||
wird durch das Abschalten seines Typs also nicht ungültig.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TypeSettings initial={initial} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user