Add custom commission types
This commit is contained in:
116
src/components/commissions/customCards/CustomCardImagePicker.tsx
Normal file
116
src/components/commissions/customCards/CustomCardImagePicker.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
deleteCommissionCustomCardImage,
|
||||
uploadCommissionCustomCardImage,
|
||||
} from "@/actions/commissions/customCards/images";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import type { CommissionCustomCardValues } from "@/schemas/commissionCustomCard";
|
||||
import Image from "next/image";
|
||||
import { useMemo, useTransition } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useWatch } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<CommissionCustomCardValues>;
|
||||
initialImages: { key: string; url: string }[];
|
||||
};
|
||||
|
||||
export function CustomCardImagePicker({ form }: Props) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const referenceImageUrl = useWatch({
|
||||
control: form.control,
|
||||
name: "referenceImageUrl",
|
||||
});
|
||||
|
||||
const previewUrl = useMemo(() => {
|
||||
if (!referenceImageUrl) return "";
|
||||
return referenceImageUrl;
|
||||
}, [referenceImageUrl]);
|
||||
|
||||
const handleUpload = (file: File) => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
startTransition(async () => {
|
||||
const item = await uploadCommissionCustomCardImage(fd);
|
||||
form.setValue("referenceImageUrl", item.url, { shouldDirty: true });
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
const url = referenceImageUrl ?? "";
|
||||
const key = url.replace(/^\/api\/image\//, "");
|
||||
const decodedKey = decodeURIComponent(key);
|
||||
if (!decodedKey) return;
|
||||
if (!window.confirm("Delete this image from S3?")) return;
|
||||
|
||||
startTransition(async () => {
|
||||
await deleteCommissionCustomCardImage(decodedKey);
|
||||
form.setValue("referenceImageUrl", null, { shouldDirty: true });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Reference image</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-2">
|
||||
<input type="hidden" {...form.register("referenceImageUrl")} />
|
||||
{previewUrl ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="relative w-full max-w-md overflow-hidden rounded-lg border border-border/60 bg-muted/40">
|
||||
<Image
|
||||
src={previewUrl}
|
||||
alt="Reference preview"
|
||||
width={900}
|
||||
height={600}
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary underline"
|
||||
>
|
||||
Open full size
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No image selected.</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleUpload(file);
|
||||
e.currentTarget.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={!referenceImageUrl || isPending}
|
||||
>
|
||||
Delete image
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Upload and preview a reference image stored in the custom card bucket folder.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
187
src/components/commissions/customCards/EditCustomCardForm.tsx
Normal file
187
src/components/commissions/customCards/EditCustomCardForm.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { updateCommissionCustomCard } from "@/actions/commissions/customCards/updateCard";
|
||||
import type { CommissionCustomCardImageItem } from "@/actions/commissions/customCards/images";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { CommissionExtra, CommissionOption } from "@/generated/prisma/client";
|
||||
import {
|
||||
commissionCustomCardSchema,
|
||||
type CommissionCustomCardValues,
|
||||
} from "@/schemas/commissionCustomCard";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { CommissionExtraField } from "../types/form/CommissionExtraField";
|
||||
import { CommissionOptionField } from "../types/form/CommissionOptionField";
|
||||
import { CustomCardImagePicker } from "./CustomCardImagePicker";
|
||||
|
||||
type CustomCardOption = {
|
||||
optionId: string;
|
||||
price: number | null;
|
||||
pricePercent: number | null;
|
||||
priceRange: string | null;
|
||||
};
|
||||
|
||||
type CustomCardExtra = {
|
||||
extraId: string;
|
||||
price: number | null;
|
||||
pricePercent: number | null;
|
||||
priceRange: string | null;
|
||||
};
|
||||
|
||||
type CustomCardWithItems = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
referenceImageUrl: string | null;
|
||||
isVisible: boolean;
|
||||
isSpecialOffer: boolean;
|
||||
options: CustomCardOption[];
|
||||
extras: CustomCardExtra[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
card: CustomCardWithItems;
|
||||
allOptions: CommissionOption[];
|
||||
allExtras: CommissionExtra[];
|
||||
images: CommissionCustomCardImageItem[];
|
||||
};
|
||||
|
||||
export default function EditCustomCardForm({
|
||||
card,
|
||||
allOptions,
|
||||
allExtras,
|
||||
images,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const form = useForm<CommissionCustomCardValues>({
|
||||
resolver: zodResolver(commissionCustomCardSchema),
|
||||
defaultValues: {
|
||||
name: card.name,
|
||||
description: card.description ?? "",
|
||||
isVisible: card.isVisible,
|
||||
isSpecialOffer: card.isSpecialOffer,
|
||||
referenceImageUrl: card.referenceImageUrl ?? null,
|
||||
options: card.options.map((o) => ({
|
||||
optionId: o.optionId,
|
||||
price: o.price ?? undefined,
|
||||
pricePercent: o.pricePercent ?? undefined,
|
||||
priceRange: o.priceRange ?? undefined,
|
||||
})),
|
||||
extras: card.extras.map((e) => ({
|
||||
extraId: e.extraId,
|
||||
price: e.price ?? undefined,
|
||||
pricePercent: e.pricePercent ?? undefined,
|
||||
priceRange: e.priceRange ?? undefined,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(values: CommissionCustomCardValues) {
|
||||
try {
|
||||
await updateCommissionCustomCard(card.id, values);
|
||||
toast.success("Custom commission card updated.");
|
||||
router.push("/commissions/custom-cards");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast("Failed to update custom commission card.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>The name of the custom commission card.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Optional description.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isVisible"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Visible on app</FormLabel>
|
||||
<FormDescription>Controls whether the card is shown.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSpecialOffer"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Special offer</FormLabel>
|
||||
<FormDescription>Adds a special offer badge on the app.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="referenceImageUrl"
|
||||
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
|
||||
/>
|
||||
|
||||
<CommissionOptionField options={allOptions} />
|
||||
<CommissionExtraField extras={allExtras} />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
219
src/components/commissions/customCards/ListCustomCards.tsx
Normal file
219
src/components/commissions/customCards/ListCustomCards.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
|
||||
import { deleteCommissionCustomCard } from "@/actions/commissions/customCards/deleteCard";
|
||||
import { updateCommissionCustomCardSortOrder } from "@/actions/commissions/customCards/updateSortOrder";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, rectSortingStrategy, SortableContext } from "@dnd-kit/sortable";
|
||||
import { PencilIcon, TrashIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import SortableItemCard from "../types/SortableItemCard";
|
||||
|
||||
type CustomCardOption = {
|
||||
id: string;
|
||||
optionId: string;
|
||||
price: number | null;
|
||||
pricePercent: number | null;
|
||||
priceRange: string | null;
|
||||
option: { name: string } | null;
|
||||
};
|
||||
|
||||
type CustomCardExtra = {
|
||||
id: string;
|
||||
extraId: string;
|
||||
price: number | null;
|
||||
pricePercent: number | null;
|
||||
priceRange: string | null;
|
||||
extra: { name: string } | null;
|
||||
};
|
||||
|
||||
export type CommissionCustomCardWithItems = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
referenceImageUrl: string | null;
|
||||
isVisible: boolean;
|
||||
isSpecialOffer: boolean;
|
||||
options: CustomCardOption[];
|
||||
extras: CustomCardExtra[];
|
||||
};
|
||||
|
||||
export default function ListCustomCards({
|
||||
cards,
|
||||
}: {
|
||||
cards: CommissionCustomCardWithItems[];
|
||||
}) {
|
||||
const [items, setItems] = useState(cards);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const oldIndex = items.findIndex((i) => i.id === active.id);
|
||||
const newIndex = items.findIndex((i) => i.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newItems);
|
||||
|
||||
await updateCommissionCustomCardSortOrder(
|
||||
newItems.map((item, i) => ({ id: item.id, sortIndex: i }))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (!deleteTargetId) return;
|
||||
startTransition(async () => {
|
||||
await deleteCommissionCustomCard(deleteTargetId);
|
||||
setItems((prev) => prev.filter((i) => i.id !== deleteTargetId));
|
||||
setDialogOpen(false);
|
||||
setDeleteTargetId(null);
|
||||
});
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={items.map((i) => i.id)} strategy={rectSortingStrategy}>
|
||||
{items.map((card) => (
|
||||
<SortableItemCard key={card.id} id={card.id}>
|
||||
<Card>
|
||||
<CardHeader className="relative">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<CardTitle className="text-xl truncate">{card.name}</CardTitle>
|
||||
{!card.isVisible ? (
|
||||
<Badge variant="secondary">Hidden</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">Visible</Badge>
|
||||
)}
|
||||
{card.isSpecialOffer ? (
|
||||
<Badge className="bg-amber-500 text-amber-950 hover:bg-amber-500">
|
||||
Special
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<CardDescription>{card.description}</CardDescription>
|
||||
{card.referenceImageUrl ? (
|
||||
<p className="text-xs text-muted-foreground">Has image</p>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col justify-start gap-4">
|
||||
<div>
|
||||
<h4 className="font-semibold">Options</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{card.options.map((opt) => (
|
||||
<li key={opt.id}>
|
||||
{opt.option?.name}:{" "}
|
||||
{opt.price !== null
|
||||
? `${opt.price}€`
|
||||
: opt.pricePercent
|
||||
? `+${opt.pricePercent}%`
|
||||
: opt.priceRange
|
||||
? `${opt.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Extras</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{card.extras.map((ext) => (
|
||||
<li key={ext.id}>
|
||||
{ext.extra?.name}:{" "}
|
||||
{ext.price !== null
|
||||
? `${ext.price}€`
|
||||
: ext.pricePercent
|
||||
? `+${ext.pricePercent}%`
|
||||
: ext.priceRange
|
||||
? `${ext.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Link href={`/commissions/custom-cards/${card.id}`} className="w-full">
|
||||
<Button variant="default" className="w-full flex items-center gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setDeleteTargetId(card.id);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</SortableItemCard>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete this custom card?</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p>This action cannot be undone. Are you sure you want to continue?</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" disabled={isPending} onClick={confirmDelete}>
|
||||
Confirm Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
147
src/components/commissions/customCards/NewCustomCardForm.tsx
Normal file
147
src/components/commissions/customCards/NewCustomCardForm.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { createCommissionCustomCard } from "@/actions/commissions/customCards/newCard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { CommissionExtra, CommissionOption } from "@/generated/prisma/client";
|
||||
import {
|
||||
commissionCustomCardSchema,
|
||||
type CommissionCustomCardValues,
|
||||
} from "@/schemas/commissionCustomCard";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { CommissionExtraField } from "../types/form/CommissionExtraField";
|
||||
import { CommissionOptionField } from "../types/form/CommissionOptionField";
|
||||
import { CustomCardImagePicker } from "./CustomCardImagePicker";
|
||||
import type { CommissionCustomCardImageItem } from "@/actions/commissions/customCards/images";
|
||||
|
||||
type Props = {
|
||||
options: CommissionOption[];
|
||||
extras: CommissionExtra[];
|
||||
images: CommissionCustomCardImageItem[];
|
||||
};
|
||||
|
||||
export default function NewCustomCardForm({ options, extras, images }: Props) {
|
||||
const router = useRouter();
|
||||
const form = useForm<CommissionCustomCardValues>({
|
||||
resolver: zodResolver(commissionCustomCardSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
isVisible: true,
|
||||
isSpecialOffer: false,
|
||||
referenceImageUrl: null,
|
||||
options: [],
|
||||
extras: [],
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(values: CommissionCustomCardValues) {
|
||||
try {
|
||||
const created = await createCommissionCustomCard(values);
|
||||
console.log("Commission custom card created:", created);
|
||||
toast("Custom commission card created.");
|
||||
router.push("/commissions/custom-cards");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast("Failed to create custom commission card.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>The name of the custom commission card.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Optional description.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isVisible"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Visible on app</FormLabel>
|
||||
<FormDescription>Controls whether the card is shown.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSpecialOffer"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Special offer</FormLabel>
|
||||
<FormDescription>Adds a special offer badge on the app.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="referenceImageUrl"
|
||||
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
|
||||
/>
|
||||
|
||||
<CommissionOptionField options={options} />
|
||||
<CommissionExtraField extras={extras} />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -34,6 +34,10 @@ const commissionItems = [
|
||||
title: "Types",
|
||||
href: "/commissions/types",
|
||||
},
|
||||
{
|
||||
title: "Custom Cards",
|
||||
href: "/commissions/custom-cards",
|
||||
},
|
||||
{
|
||||
title: "Guidelines",
|
||||
href: "/commissions/guidelines",
|
||||
@ -203,4 +207,4 @@ export default function TopNav() {
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ export const adminNav: AdminNavGroup[] = [
|
||||
{ title: "Requests", href: "/commissions/requests" },
|
||||
{ title: "Board", href: "/commissions/kanban" },
|
||||
{ title: "Types", href: "/commissions/types" },
|
||||
{ title: "Custom Cards", href: "/commissions/custom-cards" },
|
||||
{ title: "TypeOptions", href: "/commissions/types/options" },
|
||||
{ title: "TypeExtras", href: "/commissions/types/extras" },
|
||||
{ title: "Guidelines", href: "/commissions/guidelines" },
|
||||
|
||||
Reference in New Issue
Block a user