Add art types and categories
This commit is contained in:
@ -0,0 +1,22 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[name]` on the table `PortfolioCategory` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[name]` on the table `PortfolioTag` will be added. If there are existing duplicate values, this will fail.
|
||||
- Made the column `name` on table `PortfolioCategory` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `name` on table `PortfolioTag` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PortfolioCategory" ADD COLUMN "description" TEXT,
|
||||
ALTER COLUMN "name" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PortfolioTag" ADD COLUMN "description" TEXT,
|
||||
ALTER COLUMN "name" SET NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PortfolioCategory_name_key" ON "PortfolioCategory"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PortfolioTag_name_key" ON "PortfolioTag"("name");
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "PortfolioImage" ADD COLUMN "setAsHeader" BOOLEAN NOT NULL DEFAULT false;
|
@ -0,0 +1,9 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "PortfolioImage" ADD COLUMN "artType" TEXT,
|
||||
ADD COLUMN "group" TEXT,
|
||||
ADD COLUMN "kind" TEXT,
|
||||
ADD COLUMN "layoutGroup" TEXT,
|
||||
ADD COLUMN "layoutOrder" INTEGER,
|
||||
ADD COLUMN "month" INTEGER,
|
||||
ADD COLUMN "series" TEXT,
|
||||
ADD COLUMN "year" INTEGER;
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `artType` on the `PortfolioImage` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PortfolioImage" DROP COLUMN "artType",
|
||||
ADD COLUMN "artTypeId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PortfolioArtType" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT,
|
||||
"description" TEXT,
|
||||
|
||||
CONSTRAINT "PortfolioArtType_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PortfolioArtType_name_key" ON "PortfolioArtType"("name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PortfolioImage" ADD CONSTRAINT "PortfolioImage_artTypeId_fkey" FOREIGN KEY ("artTypeId") REFERENCES "PortfolioArtType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -160,24 +160,47 @@ model PortfolioImage {
|
||||
originalFile String @unique
|
||||
nsfw Boolean @default(false)
|
||||
published Boolean @default(false)
|
||||
setAsHeader Boolean @default(false)
|
||||
|
||||
altText String?
|
||||
description String?
|
||||
fileType String?
|
||||
group String?
|
||||
kind String?
|
||||
layoutGroup String?
|
||||
name String?
|
||||
series String?
|
||||
slug String?
|
||||
type String?
|
||||
fileSize Int?
|
||||
layoutOrder Int?
|
||||
month Int?
|
||||
year Int?
|
||||
creationDate DateTime?
|
||||
|
||||
metadata ImageMetadata?
|
||||
|
||||
artTypeId String?
|
||||
artType PortfolioArtType? @relation(fields: [artTypeId], references: [id])
|
||||
metadata ImageMetadata?
|
||||
categories PortfolioCategory[]
|
||||
colors ImageColor[]
|
||||
tags PortfolioTag[]
|
||||
variants ImageVariant[]
|
||||
}
|
||||
|
||||
model PortfolioArtType {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
slug String?
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioCategory {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
23
src/actions/portfolio/arttypes/createArtType.ts
Normal file
23
src/actions/portfolio/arttypes/createArtType.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import prisma from '@/lib/prisma';
|
||||
import { artTypeSchema } from '@/schemas/artTypeSchema';
|
||||
|
||||
export async function createArtType(formData: artTypeSchema) {
|
||||
const parsed = artTypeSchema.safeParse(formData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const created = await prisma.portfolioArtType.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
}
|
5
src/app/portfolio/arttypes/edit/page.tsx
Normal file
5
src/app/portfolio/arttypes/edit/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function ArtTypesEditPage() {
|
||||
return (
|
||||
<div>ArtTypesEditPage</div>
|
||||
);
|
||||
}
|
12
src/app/portfolio/arttypes/new/page.tsx
Normal file
12
src/app/portfolio/arttypes/new/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import NewArtTypeForm from "@/components/portfolio/arttypes/NewArtTypeForm";
|
||||
|
||||
export default function ArtTypesNewPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">New Art Type</h1>
|
||||
</div>
|
||||
<NewArtTypeForm />
|
||||
</div>
|
||||
);
|
||||
}
|
27
src/app/portfolio/arttypes/page.tsx
Normal file
27
src/app/portfolio/arttypes/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import ListItems from "@/components/portfolio/arttypes/ListItems";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function ArtTypesPage() {
|
||||
const items = await prisma.portfolioArtType.findMany(
|
||||
{
|
||||
orderBy: [
|
||||
{ sortIndex: 'asc' },
|
||||
{ name: 'asc' }
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Art Types</h1>
|
||||
<Link href="/portfolio/arttypes/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new type
|
||||
</Link>
|
||||
</div>
|
||||
{items.length > 0 ? <ListItems items={items} /> : <p className="text-muted-foreground italic">No items found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
5
src/app/portfolio/categories/page.tsx
Normal file
5
src/app/portfolio/categories/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function CategoriesPage() {
|
||||
return (
|
||||
<div>CategoriesPage</div>
|
||||
);
|
||||
}
|
@ -20,13 +20,14 @@ export default async function PortfolioEditPage({ params }: { params: { id: stri
|
||||
|
||||
const categories = await prisma.portfolioCategory.findMany({ orderBy: { name: "asc" } });
|
||||
const tags = await prisma.portfolioTag.findMany({ orderBy: { name: "asc" } });
|
||||
const artTypes = await prisma.portfolioArtType.findMany({ orderBy: { name: "asc" } });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Edit image</h1>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div>
|
||||
{image ? <EditImageForm image={image} tags={tags} categories={categories} /> : 'Image not found...'}
|
||||
{image ? <EditImageForm image={image} tags={tags} categories={categories} artTypes={artTypes} /> : 'Image not found...'}
|
||||
<div className="mt-6">
|
||||
{image && <DeleteImageButton imageId={image.id} />}
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { NavigationMenu, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||
import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function TopNav() {
|
||||
@ -12,11 +12,43 @@ export default function TopNav() {
|
||||
<Link href="/">Home</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/portfolio">Portfolio</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid gap-2 p-4 w-48">
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
All Portfolio Images
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio/arttypes" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
Art Types
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio/categories" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
Categories
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio/tags" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
Tags
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/items/commissions/types">CommissionTypes</Link>
|
||||
|
384
src/components/portfolio/arttypes/EditArtTypeForm.tsx
Normal file
384
src/components/portfolio/arttypes/EditArtTypeForm.tsx
Normal file
@ -0,0 +1,384 @@
|
||||
"use client"
|
||||
|
||||
import { updateImage } from "@/actions/portfolio/edit/updateImage";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import MultipleSelector from "@/components/ui/multiselect";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioArtType, PortfolioCategory, PortfolioImage, PortfolioTag } from "@/generated/prisma";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { imageSchema } from "@/schemas/imageSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
type ImageWithItems = PortfolioImage & {
|
||||
metadata: ImageMetadata | null,
|
||||
colors: (
|
||||
ImageColor & {
|
||||
color: Color
|
||||
}
|
||||
)[],
|
||||
variants: ImageVariant[],
|
||||
categories: PortfolioCategory[],
|
||||
tags: PortfolioTag[],
|
||||
artTypes: PortfolioArtType[],
|
||||
};
|
||||
|
||||
|
||||
export default function EditImageForm({ image, categories, tags, artTypes }:
|
||||
{
|
||||
image: ImageWithItems,
|
||||
categories: PortfolioCategory[]
|
||||
tags: PortfolioTag[],
|
||||
artTypes: PortfolioArtType[]
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof imageSchema>>({
|
||||
resolver: zodResolver(imageSchema),
|
||||
defaultValues: {
|
||||
fileKey: image.fileKey,
|
||||
originalFile: image.originalFile,
|
||||
nsfw: image.nsfw ?? false,
|
||||
published: image.nsfw ?? false,
|
||||
setAsHeader: image.setAsHeader ?? false,
|
||||
|
||||
altText: image.altText || "",
|
||||
description: image.description || "",
|
||||
fileType: image.fileType || "",
|
||||
group: image.group || "",
|
||||
kind: image.kind || "",
|
||||
layoutGroup: image.layoutGroup || "",
|
||||
name: image.name || "",
|
||||
series: image.series || "",
|
||||
slug: image.slug || "",
|
||||
type: image.type || "",
|
||||
fileSize: image.fileSize || undefined,
|
||||
layoutOrder: image.layoutOrder || undefined,
|
||||
month: image.month || undefined,
|
||||
year: image.year || undefined,
|
||||
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
|
||||
|
||||
tagIds: image.tags?.map(tag => tag.id) ?? [],
|
||||
categoryIds: image.categories?.map(cat => cat.id) ?? [],
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof imageSchema>) {
|
||||
const updatedImage = await updateImage(values, image.id)
|
||||
if (updatedImage) {
|
||||
toast.success("Image updated")
|
||||
router.push(`/portfolio`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* String */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="The public display name" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="altText"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Alt Text</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Alt for this image" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} placeholder="A descriptive text to the image" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="month"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Creation Month</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="year"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Creation Year</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Date */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="creationDate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-1">
|
||||
<FormLabel>Creation Date</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? format(field.value, "PPP") : "Pick a date"}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date)
|
||||
}}
|
||||
initialFocus
|
||||
fromYear={1990}
|
||||
toYear={2030}
|
||||
captionLayout="dropdown"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Select */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="artTypeId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Art Type</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value === "" ? undefined : value)}
|
||||
value={field.value ?? ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an art type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{artTypes.map((type) => (
|
||||
<SelectItem key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tagIds"
|
||||
render={({ field }) => {
|
||||
const selectedOptions = tags
|
||||
.filter(tag => field.value?.includes(tag.id))
|
||||
.map(tag => ({ label: tag.name, value: tag.id }));
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormControl>
|
||||
<MultipleSelector
|
||||
defaultOptions={tags.map(tag => ({
|
||||
label: tag.name,
|
||||
value: tag.id,
|
||||
}))}
|
||||
placeholder="Select tags"
|
||||
hidePlaceholderWhenSelected
|
||||
selectFirstItem
|
||||
value={selectedOptions}
|
||||
onChange={(options) => {
|
||||
const ids = options.map(option => option.value);
|
||||
field.onChange(ids);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="categoryIds"
|
||||
render={({ field }) => {
|
||||
const selectedOptions = categories
|
||||
.filter(cat => field.value?.includes(cat.id))
|
||||
.map(cat => ({ label: cat.name, value: cat.id }));
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Categories</FormLabel>
|
||||
<FormControl>
|
||||
<MultipleSelector
|
||||
defaultOptions={categories.map(cat => ({
|
||||
label: cat.name,
|
||||
value: cat.id,
|
||||
}))}
|
||||
placeholder="Select categories"
|
||||
hidePlaceholderWhenSelected
|
||||
selectFirstItem
|
||||
value={selectedOptions}
|
||||
onChange={(options) => {
|
||||
const ids = options.map(option => option.value);
|
||||
field.onChange(ids);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/* Boolean */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nsfw"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>NSFW</FormLabel>
|
||||
<FormDescription>This image contains sensitive or adult content.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="published"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish</FormLabel>
|
||||
<FormDescription>Will this image be published.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="setAsHeader"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Set as header image</FormLabel>
|
||||
<FormDescription>Will be the main banner image. Choose a fitting one.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Read only */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image Key</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="originalFile"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Original file</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Filetype</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>FileSize</FormLabel>
|
||||
<FormControl><Input type="number" {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<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 >
|
||||
);
|
||||
}
|
55
src/components/portfolio/arttypes/ListItems.tsx
Normal file
55
src/components/portfolio/arttypes/ListItems.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import { updateImageSortOrder } from "@/actions/portfolio/updateImageSortOrder";
|
||||
import { SortableList } from "@/components/sort/SortableList";
|
||||
import { PortfolioArtType } from "@/generated/prisma";
|
||||
import { SortableItem } from "@/types/SortableItem";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SortableCardItem } from "./SortableItem";
|
||||
|
||||
|
||||
export default function ListItems({ items }: { items: PortfolioArtType[] }) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const sortableItems: SortableItem[] = items.map(item => ({
|
||||
id: item.id,
|
||||
sortIndex: item.sortIndex,
|
||||
label: item.name || "",
|
||||
}));
|
||||
|
||||
// const handleSortDefault = async () => {
|
||||
// const sorted = [...sortableItems]
|
||||
// .sort((a, b) => a.label.localeCompare(b.label))
|
||||
// .map((item, index) => ({ ...item, sortIndex: index * 10 }));
|
||||
// await updateImageSortOrder(sorted);
|
||||
// };
|
||||
|
||||
const handleReorder = async (items: SortableItem[]) => {
|
||||
await updateImageSortOrder(items);
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
renderItem={(item) => {
|
||||
const it = items.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableCardItem
|
||||
id={it.id}
|
||||
item={{
|
||||
id: it.id,
|
||||
name: it.name || ""
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
91
src/components/portfolio/arttypes/NewArtTypeForm.tsx
Normal file
91
src/components/portfolio/arttypes/NewArtTypeForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { createArtType } from "@/actions/portfolio/arttypes/createArtType";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { artTypeSchema } from "@/schemas/artTypeSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
|
||||
export default function NewArtTypeForm() {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof artTypeSchema>>({
|
||||
resolver: zodResolver(artTypeSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof artTypeSchema>) {
|
||||
try {
|
||||
const created = await createArtType(values)
|
||||
console.log("CommissionType created:", created)
|
||||
toast("Commission type created.")
|
||||
router.push("/items/commissions/types")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to create commission type.")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* String */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="The public display name" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="The slug sowhn in the navigation" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} placeholder="A descriptive text to the image" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<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 >
|
||||
);
|
||||
}
|
76
src/components/portfolio/arttypes/SortableItem.tsx
Normal file
76
src/components/portfolio/arttypes/SortableItem.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import clsx from 'clsx';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
// type SupportedTypes = 'gallery' | 'album' | 'artist' | 'category' | 'tag';
|
||||
|
||||
// const pluralMap: Record<SupportedTypes, string> = {
|
||||
// gallery: 'galleries',
|
||||
// album: 'albums',
|
||||
// artist: 'artists',
|
||||
// category: 'categories',
|
||||
// tag: 'tags',
|
||||
// };
|
||||
|
||||
type SortableCardItemProps = {
|
||||
id: string;
|
||||
item: {
|
||||
id: string;
|
||||
name: string;
|
||||
count?: number;
|
||||
textLabel?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function SortableCardItem({ id, item }: SortableCardItemProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const href = `/portfolio/arttype/edit/${item.id}`;
|
||||
|
||||
let countLabel = '';
|
||||
if (item.count !== undefined) {
|
||||
countLabel = `${item.count} image${item.count !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
className="relative cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<div
|
||||
{...listeners}
|
||||
className="absolute top-2 left-2 z-20 text-muted-foreground bg-white/70 rounded-full p-1"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<GripVertical className="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
<Link href={href}>
|
||||
<div className="group rounded-lg border overflow-hidden hover:shadow-md transition-shadow bg-background relative">
|
||||
<div className={clsx("p-4 text-center")}>
|
||||
<h2 className={clsx("text-lg font-semibold truncate text-center")}>
|
||||
{item.name}
|
||||
</h2>
|
||||
{countLabel && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{countLabel}</p>
|
||||
)}
|
||||
{item.textLabel && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{item.textLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -7,9 +7,10 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For
|
||||
import { Input } from "@/components/ui/input";
|
||||
import MultipleSelector from "@/components/ui/multiselect";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioCategory, PortfolioImage, PortfolioTag } from "@/generated/prisma";
|
||||
import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioArtType, PortfolioCategory, PortfolioImage, PortfolioTag } from "@/generated/prisma";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { imageSchema } from "@/schemas/imageSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -29,14 +30,16 @@ type ImageWithItems = PortfolioImage & {
|
||||
variants: ImageVariant[],
|
||||
categories: PortfolioCategory[],
|
||||
tags: PortfolioTag[],
|
||||
artTypes: PortfolioArtType[],
|
||||
};
|
||||
|
||||
|
||||
export default function EditImageForm({ image, categories, tags }:
|
||||
export default function EditImageForm({ image, categories, tags, artTypes }:
|
||||
{
|
||||
image: ImageWithItems,
|
||||
categories: PortfolioCategory[]
|
||||
tags: PortfolioTag[],
|
||||
artTypes: PortfolioArtType[]
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof imageSchema>>({
|
||||
@ -46,14 +49,22 @@ export default function EditImageForm({ image, categories, tags }:
|
||||
originalFile: image.originalFile,
|
||||
nsfw: image.nsfw ?? false,
|
||||
published: image.nsfw ?? false,
|
||||
setAsHeader: image.setAsHeader ?? false,
|
||||
|
||||
altText: image.altText || "",
|
||||
description: image.description || "",
|
||||
fileType: image.fileType || "",
|
||||
group: image.group || "",
|
||||
kind: image.kind || "",
|
||||
layoutGroup: image.layoutGroup || "",
|
||||
name: image.name || "",
|
||||
series: image.series || "",
|
||||
slug: image.slug || "",
|
||||
type: image.type || "",
|
||||
fileSize: image.fileSize || undefined,
|
||||
layoutOrder: image.layoutOrder || undefined,
|
||||
month: image.month || undefined,
|
||||
year: image.year || undefined,
|
||||
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
|
||||
|
||||
tagIds: image.tags?.map(tag => tag.id) ?? [],
|
||||
@ -73,66 +84,29 @@ export default function EditImageForm({ image, categories, tags }:
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* String */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileKey"
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image Key</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormLabel>Image name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="The public display name" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="originalFile"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Original file</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nsfw"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>NSFW</FormLabel>
|
||||
<FormDescription>This image contains sensitive or adult content.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="published"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish</FormLabel>
|
||||
<FormDescription>Will this image be published.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="altText"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Alt Text</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Alt for this image" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@ -143,66 +117,41 @@ export default function EditImageForm({ image, categories, tags }:
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl><Textarea {...field} /></FormControl>
|
||||
<FormControl>
|
||||
<Textarea {...field} placeholder="A descriptive text to the image" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="month"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Creation Month</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
name="year"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image name</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image slug</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image type</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Filetype</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image fileSize</FormLabel>
|
||||
<FormControl><Input type="number" {...field} disabled /></FormControl>
|
||||
<FormLabel>Creation Year</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Date */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="creationDate"
|
||||
@ -243,6 +192,34 @@ export default function EditImageForm({ image, categories, tags }:
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Select */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="artTypeId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Art Type</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value === "" ? undefined : value)}
|
||||
value={field.value ?? ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an art type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{artTypes.map((type) => (
|
||||
<SelectItem key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tagIds"
|
||||
@ -305,6 +282,97 @@ export default function EditImageForm({ image, categories, tags }:
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/* Boolean */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nsfw"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>NSFW</FormLabel>
|
||||
<FormDescription>This image contains sensitive or adult content.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="published"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish</FormLabel>
|
||||
<FormDescription>Will this image be published.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="setAsHeader"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Set as header image</FormLabel>
|
||||
<FormDescription>Will be the main banner image. Choose a fitting one.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Read only */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image Key</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="originalFile"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Original file</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Filetype</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>FileSize</FormLabel>
|
||||
<FormControl><Input type="number" {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||
|
5
src/components/portfolio/tags/page.tsx
Normal file
5
src/components/portfolio/tags/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function TagsPage() {
|
||||
return (
|
||||
<div>TagsPage</div>
|
||||
);
|
||||
}
|
9
src/schemas/artTypeSchema.ts
Normal file
9
src/schemas/artTypeSchema.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const artTypeSchema = z.object({
|
||||
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type artTypeSchema = z.infer<typeof artTypeSchema>
|
@ -13,16 +13,25 @@ export const imageSchema = z.object({
|
||||
originalFile: z.string().min(1, "Original file is required"),
|
||||
nsfw: z.boolean(),
|
||||
published: z.boolean(),
|
||||
setAsHeader: z.boolean(),
|
||||
|
||||
altText: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
fileType: z.string().optional(),
|
||||
group: z.string().optional(),
|
||||
kind: z.string().optional(),
|
||||
layoutGroup: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
series: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
layoutOrder: z.number().optional(),
|
||||
month: z.number().optional(),
|
||||
year: z.number().optional(),
|
||||
creationDate: z.date().optional(),
|
||||
|
||||
categoryIds: z.array(z.string()).optional(),
|
||||
tagIds: z.array(z.string()).optional(),
|
||||
artTypeId: z.string().optional(),
|
||||
})
|
Reference in New Issue
Block a user