Refactor portfolio
This commit is contained in:
25
src/actions/portfolio/categories/createCategory.ts
Normal file
25
src/actions/portfolio/categories/createCategory.ts
Normal file
@ -0,0 +1,25 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { categorySchema } from '@/schemas/portfolio/categorySchema';
|
||||
|
||||
export async function createCategory(formData: categorySchema) {
|
||||
const parsed = categorySchema.safeParse(formData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const created = await prisma.portfolioCategory.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
}
|
27
src/actions/portfolio/categories/updateCategory.ts
Normal file
27
src/actions/portfolio/categories/updateCategory.ts
Normal file
@ -0,0 +1,27 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { categorySchema } from '@/schemas/portfolio/categorySchema';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export async function updateCategory(id: string, rawData: z.infer<typeof categorySchema>) {
|
||||
const parsed = categorySchema.safeParse(rawData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const updated = await prisma.portfolioCategory.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
}
|
20
src/actions/portfolio/deleteItem.ts
Normal file
20
src/actions/portfolio/deleteItem.ts
Normal file
@ -0,0 +1,20 @@
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function deleteItems(itemId: string, type: string) {
|
||||
|
||||
switch (type) {
|
||||
case "categories":
|
||||
await prisma.portfolioCategory.delete({ where: { id: itemId } });
|
||||
break;
|
||||
case "tags":
|
||||
await prisma.portfolioTag.delete({ where: { id: itemId } });
|
||||
break;
|
||||
case "types":
|
||||
await prisma.portfolioType.delete({ where: { id: itemId } });
|
||||
break;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
@ -24,6 +24,7 @@ export async function updateImage(
|
||||
name,
|
||||
fileSize,
|
||||
creationDate,
|
||||
typeId,
|
||||
tagIds,
|
||||
categoryIds
|
||||
} = validated.data;
|
||||
@ -41,6 +42,7 @@ export async function updateImage(
|
||||
name,
|
||||
fileSize,
|
||||
creationDate,
|
||||
typeId
|
||||
}
|
||||
});
|
||||
|
||||
|
40
src/actions/portfolio/sortItems.ts
Normal file
40
src/actions/portfolio/sortItems.ts
Normal file
@ -0,0 +1,40 @@
|
||||
'use server';
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { SortableItem } from "@/types/SortableItem";
|
||||
|
||||
export async function sortItems(items: SortableItem[], type: string) {
|
||||
|
||||
switch(type) {
|
||||
case "categories":
|
||||
await Promise.all(
|
||||
items.map(item =>
|
||||
prisma.portfolioCategory.update({
|
||||
where: { id: item.id },
|
||||
data: { sortIndex: item.sortIndex },
|
||||
})
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "tags":
|
||||
await Promise.all(
|
||||
items.map(item =>
|
||||
prisma.portfolioTag.update({
|
||||
where: { id: item.id },
|
||||
data: { sortIndex: item.sortIndex },
|
||||
})
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "types":
|
||||
await Promise.all(
|
||||
items.map(item =>
|
||||
prisma.portfolioType.update({
|
||||
where: { id: item.id },
|
||||
data: { sortIndex: item.sortIndex },
|
||||
})
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
25
src/actions/portfolio/tags/createTag.ts
Normal file
25
src/actions/portfolio/tags/createTag.ts
Normal file
@ -0,0 +1,25 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { tagSchema } from '@/schemas/portfolio/tagSchema';
|
||||
|
||||
export async function createTag(formData: tagSchema) {
|
||||
const parsed = tagSchema.safeParse(formData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const created = await prisma.portfolioTag.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
}
|
27
src/actions/portfolio/tags/updateTag.ts
Normal file
27
src/actions/portfolio/tags/updateTag.ts
Normal file
@ -0,0 +1,27 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { tagSchema } from '@/schemas/portfolio/tagSchema';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export async function updateTag(id: string, rawData: z.infer<typeof tagSchema>) {
|
||||
const parsed = tagSchema.safeParse(rawData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const updated = await prisma.portfolioTag.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
}
|
25
src/actions/portfolio/types/createType.ts
Normal file
25
src/actions/portfolio/types/createType.ts
Normal file
@ -0,0 +1,25 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { typeSchema } from '@/schemas/portfolio/typeSchema';
|
||||
|
||||
export async function createType(formData: typeSchema) {
|
||||
const parsed = typeSchema.safeParse(formData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const created = await prisma.portfolioType.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
}
|
27
src/actions/portfolio/types/updateType.ts
Normal file
27
src/actions/portfolio/types/updateType.ts
Normal file
@ -0,0 +1,27 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { typeSchema } from '@/schemas/portfolio/typeSchema';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export async function updateType(id: string, rawData: z.infer<typeof typeSchema>) {
|
||||
const parsed = typeSchema.safeParse(rawData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const updated = await prisma.portfolioType.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
export default function PortfolioCategoriesEditPage() {
|
||||
import EditCategoryForm from "@/components/portfolio/categories/EditCategoryForm";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function PortfolioCategoriesEditPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
const category = await prisma.portfolioCategory.findUnique({
|
||||
where: {
|
||||
id,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>PortfolioCategoriesEditPage</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Edit Category</h1>
|
||||
{category && <EditCategoryForm category={category} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import NewCategoryForm from "@/components/portfolio/categories/NewCategoryForm";
|
||||
|
||||
export default function PortfolioCategoriesNewPage() {
|
||||
return (
|
||||
<div>PortfolioCategoriesNewPage</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">New Category</h1>
|
||||
<NewCategoryForm />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,20 @@
|
||||
export default function PortfolioCategoriesPage() {
|
||||
import ItemList from "@/components/portfolio/ItemList";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function PortfolioCategoriesPage() {
|
||||
const items = await prisma.portfolioCategory.findMany({})
|
||||
|
||||
return (
|
||||
<div>PortfolioCategoriesPage</div>
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Art Categories</h1>
|
||||
<Link href="/portfolio/categories/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 category
|
||||
</Link>
|
||||
</div>
|
||||
{items && items.length > 0 ? <ItemList items={items} type="categories" /> : <p>There are no categories yet. Consider adding some!</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,24 +1,55 @@
|
||||
import FilterBar from "@/components/portfolio/images/FilterBar";
|
||||
import ImageList from "@/components/portfolio/images/ImageList";
|
||||
import { Prisma } from "@/generated/prisma";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function PortfolioImagesPage() {
|
||||
export default async function PortfolioImagesPage(
|
||||
{ searchParams }:
|
||||
{ searchParams: { type: string, published: string } }
|
||||
) {
|
||||
const { type, published } = await searchParams;
|
||||
|
||||
const types = await prisma.portfolioType.findMany({
|
||||
orderBy: { sortIndex: "asc" },
|
||||
});
|
||||
|
||||
const typeFilter = type ?? "all";
|
||||
const publishedFilter = published ?? "all";
|
||||
|
||||
const where: Prisma.PortfolioImageWhereInput = {};
|
||||
|
||||
if (typeFilter !== "all") {
|
||||
where.typeId = typeFilter === "none" ? null : typeFilter;
|
||||
}
|
||||
|
||||
if (publishedFilter === "published") {
|
||||
where.published = true;
|
||||
} else if (publishedFilter === "unpublished") {
|
||||
where.published = false;
|
||||
}
|
||||
|
||||
const images = await prisma.portfolioImage.findMany(
|
||||
{
|
||||
orderBy: [{ sortIndex: 'asc' }]
|
||||
where,
|
||||
orderBy: [{ sortIndex: 'asc' }],
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<div className="flex justify-between pb-4 items-end">
|
||||
<h1 className="text-2xl font-bold mb-4">Images</h1>
|
||||
<Link href="/portfolio/images/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" /> Upload new image
|
||||
</Link>
|
||||
</div>
|
||||
{images && images.length > 0 ? <ImageList images={images} /> : <p>There are no images yet. Consider adding some!</p>}
|
||||
|
||||
<FilterBar types={types} currentType={typeFilter} currentPublished={publishedFilter} />
|
||||
<div className="mt-6">
|
||||
{images && images.length > 0 ? <ImageList images={images} /> : <p>There are no images yet. Consider adding some!</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
export default function PortfolioTagsEditPage() {
|
||||
import EditTagForm from "@/components/portfolio/tags/EditTagForm";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
const tag = await prisma.portfolioTag.findUnique({
|
||||
where: {
|
||||
id,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>PortfolioTagsEditPage</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Edit Tag</h1>
|
||||
{tag && <EditTagForm tag={tag} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import NewTagForm from "@/components/portfolio/tags/NewTagForm";
|
||||
|
||||
export default function PortfolioTagsNewPage() {
|
||||
return (
|
||||
<div>PortfolioTagsNewPage</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">New Tag</h1>
|
||||
<NewTagForm />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,20 @@
|
||||
export default function PortfolioTagsPage() {
|
||||
import ItemList from "@/components/portfolio/ItemList";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function PortfolioTagsPage() {
|
||||
const items = await prisma.portfolioTag.findMany({})
|
||||
|
||||
return (
|
||||
<div>PortfolioTagsPage</div>
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Art Tags</h1>
|
||||
<Link href="/portfolio/tags/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 tag
|
||||
</Link>
|
||||
</div>
|
||||
{items && items.length > 0 ? <ItemList items={items} type="tags" /> : <p>There are no tags yet. Consider adding some!</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
export default function PortfolioTypesEditPage() {
|
||||
import EditTypeForm from "@/components/portfolio/types/EditTypeForm";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function PortfolioTypesEditPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
const type = await prisma.portfolioType.findUnique({
|
||||
where: {
|
||||
id,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>PortfolioTypesEditPage</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Edit Type</h1>
|
||||
{type && <EditTypeForm type={type} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import NewTypeForm from "@/components/portfolio/types/NewTypeForm";
|
||||
|
||||
export default function PortfolioTypesNewPage() {
|
||||
return (
|
||||
<div>PortfolioTypesNewPage</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">New Type</h1>
|
||||
<NewTypeForm />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,20 @@
|
||||
export default function PortfolioTypesPage() {
|
||||
import ItemList from "@/components/portfolio/ItemList";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function PortfolioTypesPage() {
|
||||
const items = await prisma.portfolioType.findMany({})
|
||||
|
||||
return (
|
||||
<div>PortfolioTypesPage</div>
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Art Types</h1>
|
||||
<Link href="/portfolio/types/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 && items.length > 0 ? <ItemList items={items} type="types" /> : <p>There are no types yet. Consider adding some!</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
67
src/components/portfolio/ItemList.tsx
Normal file
67
src/components/portfolio/ItemList.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { deleteItems } from "@/actions/portfolio/deleteItem";
|
||||
import { sortItems } from "@/actions/portfolio/sortItems";
|
||||
import { SortableItem as ItemType } from "@/types/SortableItem";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SortableItem } from "../sort/items/SortableItem";
|
||||
import SortableList from "../sort/lists/SortableList";
|
||||
|
||||
type ItemProps = {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
description: string | null
|
||||
sortIndex: number
|
||||
}
|
||||
|
||||
export default function ItemList({ items, type }: { items: ItemProps[], type: string }) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const sortableItems: ItemType[] = items.map(item => ({
|
||||
id: item.id,
|
||||
sortIndex: item.sortIndex,
|
||||
label: item.name || "",
|
||||
}));
|
||||
|
||||
const handleReorder = async (items: ItemType[]) => {
|
||||
await sortItems(items, type);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteItems(id, type);
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
renderItem={(item) => {
|
||||
const it = items.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableItem
|
||||
key={it.id}
|
||||
id={it.id}
|
||||
item={
|
||||
{
|
||||
id: it.id,
|
||||
name: it.name,
|
||||
href: `/portfolio/${type}/${it.id}`,
|
||||
type: 'items'
|
||||
}
|
||||
}
|
||||
onDelete={() => handleDelete(it.id)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
91
src/components/portfolio/categories/EditCategoryForm.tsx
Normal file
91
src/components/portfolio/categories/EditCategoryForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { updateCategory } from "@/actions/portfolio/categories/updateCategory";
|
||||
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 { PortfolioCategory } from "@/generated/prisma";
|
||||
import { categorySchema } from "@/schemas/portfolio/categorySchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export default function EditCategoryForm({ category }: { category: PortfolioCategory }) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof categorySchema>>({
|
||||
resolver: zodResolver(categorySchema),
|
||||
defaultValues: {
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
description: category.description || "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof categorySchema>) {
|
||||
try {
|
||||
const updated = await updateCategory(category.id, values)
|
||||
console.log("Art category updated:", updated)
|
||||
toast("Art category updated.")
|
||||
router.push("/portfolio/categories")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to update art category.")
|
||||
}
|
||||
}
|
||||
|
||||
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 shown 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 (optional)" />
|
||||
</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 >
|
||||
);
|
||||
}
|
91
src/components/portfolio/categories/NewCategoryForm.tsx
Normal file
91
src/components/portfolio/categories/NewCategoryForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { createCategory } from "@/actions/portfolio/categories/createCategory";
|
||||
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 { categorySchema } from "@/schemas/portfolio/categorySchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
|
||||
export default function NewCategoryForm() {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof categorySchema>>({
|
||||
resolver: zodResolver(categorySchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof categorySchema>) {
|
||||
try {
|
||||
const created = await createCategory(values)
|
||||
console.log("Art category created:", created)
|
||||
toast("Art category created.")
|
||||
router.push("/portfolio/categories")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to create art category.")
|
||||
}
|
||||
}
|
||||
|
||||
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 shown 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 (optional)" />
|
||||
</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 >
|
||||
);
|
||||
}
|
@ -47,10 +47,10 @@ export default function EditImageForm({ image, categories, tags, types }:
|
||||
defaultValues: {
|
||||
fileKey: image.fileKey,
|
||||
originalFile: image.originalFile,
|
||||
nsfw: image.nsfw ?? false,
|
||||
published: image.nsfw ?? false,
|
||||
setAsHeader: image.setAsHeader ?? false,
|
||||
name: image.name,
|
||||
nsfw: image.nsfw ?? false,
|
||||
published: image.published ?? false,
|
||||
setAsHeader: image.setAsHeader ?? false,
|
||||
|
||||
altText: image.altText || "",
|
||||
description: image.description || "",
|
||||
@ -72,7 +72,7 @@ export default function EditImageForm({ image, categories, tags, types }:
|
||||
const updatedImage = await updateImage(values, image.id)
|
||||
if (updatedImage) {
|
||||
toast.success("Image updated")
|
||||
router.push(`/portfolio`)
|
||||
router.push(`/portfolio/images`)
|
||||
}
|
||||
}
|
||||
|
||||
|
97
src/components/portfolio/images/FilterBar.tsx
Normal file
97
src/components/portfolio/images/FilterBar.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import { PortfolioType } from "@/generated/prisma";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
export default function FilterBar({
|
||||
types,
|
||||
currentType,
|
||||
currentPublished,
|
||||
}: {
|
||||
types: PortfolioType[];
|
||||
currentType: string;
|
||||
currentPublished: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
const setFilter = (key: string, value: string) => {
|
||||
if (value !== "all") {
|
||||
searchParams.set(key, value);
|
||||
} else {
|
||||
searchParams.delete(key);
|
||||
}
|
||||
|
||||
router.push(`${pathname}?${searchParams.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-4 border-b pb-4">
|
||||
{/* Type Filter */}
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<span className="text-sm font-medium text-muted-foreground">Type:</span>
|
||||
<FilterButton
|
||||
active={currentType === "all"}
|
||||
label="All"
|
||||
onClick={() => setFilter("type", "all")}
|
||||
/>
|
||||
{types.map((type) => (
|
||||
<FilterButton
|
||||
key={type.id}
|
||||
active={currentType === type.id}
|
||||
label={type.name}
|
||||
onClick={() => setFilter("type", type.id)}
|
||||
/>
|
||||
))}
|
||||
<FilterButton
|
||||
active={currentType === "none"}
|
||||
label="No Type"
|
||||
onClick={() => setFilter("type", "none")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Published Filter */}
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<span className="text-sm font-medium text-muted-foreground">Status:</span>
|
||||
<FilterButton
|
||||
active={currentPublished === "all"}
|
||||
label="All"
|
||||
onClick={() => setFilter("published", "all")}
|
||||
/>
|
||||
<FilterButton
|
||||
active={currentPublished === "published"}
|
||||
label="Published"
|
||||
onClick={() => setFilter("published", "published")}
|
||||
/>
|
||||
<FilterButton
|
||||
active={currentPublished === "unpublished"}
|
||||
label="Unpublished"
|
||||
onClick={() => setFilter("published", "unpublished")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterButton({
|
||||
active,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-3 py-1 rounded text-sm border ${active
|
||||
? "bg-primary text-white border-primary"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/70 border-muted"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { deleteImage } from "@/actions/_portfolio/edit/deleteImage";
|
||||
import { sortImages } from "@/actions/portfolio/images/sortImages";
|
||||
import { SortableItem } from "@/components/sort/items/SortableItem";
|
||||
import SortableList from "@/components/sort/lists/SortableList";
|
||||
@ -24,6 +25,10 @@ export default function ImageList({ images }: { images: PortfolioImage[] }) {
|
||||
await sortImages(items);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteImage(id);
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
@ -48,6 +53,7 @@ export default function ImageList({ images }: { images: PortfolioImage[] }) {
|
||||
type: 'image'
|
||||
}
|
||||
}
|
||||
onDelete={() => handleDelete(image.id)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
91
src/components/portfolio/tags/EditTagForm.tsx
Normal file
91
src/components/portfolio/tags/EditTagForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { updateTag } from "@/actions/portfolio/tags/updateTag";
|
||||
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 { PortfolioTag } from "@/generated/prisma";
|
||||
import { tagSchema } from "@/schemas/portfolio/tagSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export default function EditTagForm({ tag }: { tag: PortfolioTag }) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof tagSchema>>({
|
||||
resolver: zodResolver(tagSchema),
|
||||
defaultValues: {
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
description: tag.description || "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof tagSchema>) {
|
||||
try {
|
||||
const updated = await updateTag(tag.id, values)
|
||||
console.log("Art tag updated:", updated)
|
||||
toast("Art tag updated.")
|
||||
router.push("/portfolio/tags")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to update art tag.")
|
||||
}
|
||||
}
|
||||
|
||||
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 shown 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 (optional)" />
|
||||
</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 >
|
||||
);
|
||||
}
|
91
src/components/portfolio/tags/NewTagForm.tsx
Normal file
91
src/components/portfolio/tags/NewTagForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { createTag } from "@/actions/portfolio/tags/createTag";
|
||||
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 { tagSchema } from "@/schemas/portfolio/tagSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
|
||||
export default function NewTagForm() {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof tagSchema>>({
|
||||
resolver: zodResolver(tagSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof tagSchema>) {
|
||||
try {
|
||||
const created = await createTag(values)
|
||||
console.log("Art tag created:", created)
|
||||
toast("Art tag created.")
|
||||
router.push("/portfolio/tags")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to create art tag.")
|
||||
}
|
||||
}
|
||||
|
||||
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 shown 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 (optional)" />
|
||||
</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 >
|
||||
);
|
||||
}
|
91
src/components/portfolio/types/EditTypeForm.tsx
Normal file
91
src/components/portfolio/types/EditTypeForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { updateType } from "@/actions/portfolio/types/updateType";
|
||||
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 { PortfolioType } from "@/generated/prisma";
|
||||
import { typeSchema } from "@/schemas/portfolio/typeSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export default function EditTypeForm({ type }: { type: PortfolioType }) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof typeSchema>>({
|
||||
resolver: zodResolver(typeSchema),
|
||||
defaultValues: {
|
||||
name: type.name,
|
||||
slug: type.slug,
|
||||
description: type.description || "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof typeSchema>) {
|
||||
try {
|
||||
const updated = await updateType(type.id, values)
|
||||
console.log("Art type updated:", updated)
|
||||
toast("Art type updated.")
|
||||
router.push("/portfolio/types")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to update art 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 shown 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 (optional)" />
|
||||
</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 >
|
||||
);
|
||||
}
|
91
src/components/portfolio/types/NewTypeForm.tsx
Normal file
91
src/components/portfolio/types/NewTypeForm.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { createType } from "@/actions/portfolio/types/createType";
|
||||
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 { typeSchema } from "@/schemas/portfolio/typeSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
|
||||
export default function NewTypeForm() {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof typeSchema>>({
|
||||
resolver: zodResolver(typeSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
}
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof typeSchema>) {
|
||||
try {
|
||||
const created = await createType(values)
|
||||
console.log("Art type created:", created)
|
||||
toast("Art type created.")
|
||||
router.push("/portfolio/types")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast("Failed to create art 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 shown 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 (optional)" />
|
||||
</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 >
|
||||
);
|
||||
}
|
@ -5,9 +5,10 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical, PencilIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
type SupportedTypes = 'image' | 'type' | 'category' | 'tag';
|
||||
type SupportedTypes = 'image' | 'items';
|
||||
|
||||
type SortableItemProps = {
|
||||
id: string;
|
||||
@ -22,9 +23,10 @@ type SortableItemProps = {
|
||||
count?: number;
|
||||
textLabel?: string;
|
||||
};
|
||||
onDelete: (itemId: string) => void;
|
||||
};
|
||||
|
||||
export function SortableItem({ id, item }: SortableItemProps) {
|
||||
export function SortableItem({ id, item, onDelete }: SortableItemProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
@ -51,11 +53,11 @@ export function SortableItem({ id, item }: SortableItemProps) {
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
className="relative cursor-grab active:cursor-grabbing"
|
||||
className="relative active:cursor-grabbing"
|
||||
>
|
||||
<div
|
||||
{...listeners}
|
||||
className="absolute top-2 left-2 z-20 text-muted-foreground bg-white/70 rounded-full p-1"
|
||||
className="absolute cursor-grab 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" />
|
||||
@ -66,7 +68,15 @@ export function SortableItem({ id, item }: SortableItemProps) {
|
||||
<CardDescription></CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col justify-start gap-4">
|
||||
|
||||
{item.type === 'image' && (
|
||||
<Image
|
||||
src={`/api/image/thumbnail/${item.fileKey}.webp`}
|
||||
alt={item.altText || item.name}
|
||||
width={200}
|
||||
height={200}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Link
|
||||
@ -78,6 +88,13 @@ export function SortableItem({ id, item }: SortableItemProps) {
|
||||
Edit
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
onClick={() => onDelete(item.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -3,11 +3,11 @@ import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { Readable } from "stream";
|
||||
|
||||
export async function getImageBufferFromS3(fileKey: string, fileType?: string): Promise<Buffer> {
|
||||
const type = fileType ? fileType.split("/")[1] : "webp";
|
||||
// const type = fileType ? fileType.split("/")[1] : "webp";
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: `original/${fileKey}.${type}`,
|
||||
Key: `original/${fileKey}.${fileType}`,
|
||||
});
|
||||
|
||||
const response = await s3.send(command);
|
||||
|
Reference in New Issue
Block a user