Refactor and add NSFW boolean
This commit is contained in:
30
package-lock.json
generated
30
package-lock.json
generated
@ -20,6 +20,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@ -2792,6 +2793,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
|
||||
"integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
|
@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "nsfw" BOOLEAN NOT NULL DEFAULT false;
|
@ -112,6 +112,7 @@ model Image {
|
||||
imageName String
|
||||
originalFile String
|
||||
uploadDate DateTime @default(now())
|
||||
nsfw Boolean @default(false)
|
||||
|
||||
altText String?
|
||||
description String?
|
||||
|
@ -17,10 +17,11 @@ export async function updateImage(
|
||||
imageName,
|
||||
originalFile,
|
||||
uploadDate,
|
||||
nsfw,
|
||||
altText,
|
||||
description,
|
||||
fileType,
|
||||
imageData,
|
||||
source,
|
||||
creationMonth,
|
||||
creationYear,
|
||||
fileSize,
|
||||
@ -37,10 +38,11 @@ export async function updateImage(
|
||||
imageName,
|
||||
originalFile,
|
||||
uploadDate,
|
||||
nsfw,
|
||||
altText,
|
||||
description,
|
||||
fileType,
|
||||
imageData,
|
||||
source,
|
||||
creationMonth,
|
||||
creationYear,
|
||||
fileSize,
|
||||
|
30
src/app/colors/[id]/page.tsx
Normal file
30
src/app/colors/[id]/page.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import DisplayColor from "@/components/colors/single/DisplayColor";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function ColorsPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
|
||||
const colorData = await prisma.color.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
images: {
|
||||
include: {
|
||||
image: {
|
||||
include: {
|
||||
variants: { where: { type: "thumbnail" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Show color</h1>
|
||||
{colorData ? <DisplayColor color={colorData} /> : 'Color not found...'}
|
||||
</div>
|
||||
);
|
||||
}
|
22
src/app/colors/page.tsx
Normal file
22
src/app/colors/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import ListColors from "@/components/colors/list/ListColors";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function ColorsPage() {
|
||||
const colors = await prisma.color.findMany(
|
||||
{
|
||||
orderBy: { name: "asc" },
|
||||
include: {
|
||||
images: { select: { id: true } }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h1 className="text-2xl font-bold mb-4">Colors</h1>
|
||||
</div>
|
||||
{colors.length > 0 ? <ListColors colors={colors} /> : <p className="text-muted-foreground italic">No colors found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
30
src/app/extract/[id]/page.tsx
Normal file
30
src/app/extract/[id]/page.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import DisplayExtractColor from "@/components/extract/single/DisplayExtractColor";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function ColorsPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
|
||||
const colorData = await prisma.extractColor.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
images: {
|
||||
include: {
|
||||
image: {
|
||||
include: {
|
||||
variants: { where: { type: "thumbnail" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Show color</h1>
|
||||
{colorData ? <DisplayExtractColor color={colorData} /> : 'Color not found...'}
|
||||
</div>
|
||||
);
|
||||
}
|
22
src/app/extract/page.tsx
Normal file
22
src/app/extract/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import ListExtractColors from "@/components/extract/list/ListExtractColors";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function ExtractColorsPage() {
|
||||
const extractColors = await prisma.extractColor.findMany(
|
||||
{
|
||||
orderBy: { name: "asc" },
|
||||
include: {
|
||||
images: { select: { id: true } }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h1 className="text-2xl font-bold mb-4">Extract colors</h1>
|
||||
</div>
|
||||
{extractColors.length > 0 ? <ListExtractColors colors={extractColors} /> : <p className="text-muted-foreground italic">No colors found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,11 +4,11 @@ import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function ImagesPage() {
|
||||
const images = await prisma.image.findMany({ orderBy: { createdAt: "asc" } });
|
||||
const images = await prisma.image.findMany({ orderBy: { imageName: "asc" } });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Images</h1>
|
||||
<Link href="/images/upload" 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
|
||||
|
35
src/components/colors/list/ListColors.tsx
Normal file
35
src/components/colors/list/ListColors.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Color } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
type ColorsWithItems = Color & {
|
||||
images: { id: string }[]
|
||||
}
|
||||
|
||||
export default function ListColors({ colors }: { colors: ColorsWithItems[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{colors.map((col) => (
|
||||
<Link href={`/colors/${col.id}`} key={col.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{col.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full border"
|
||||
style={{ backgroundColor: col.hex ?? "#ccc" }}
|
||||
title={col.hex ?? "n/a"}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Used by {col.images.length} image{col.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
41
src/components/colors/single/DisplayColor.tsx
Normal file
41
src/components/colors/single/DisplayColor.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
import ImageColorCard from "@/components/images/ImageColorCard";
|
||||
import { Color, Image, ImageColor, ImageVariant } from "@/generated/prisma";
|
||||
|
||||
type ColorWithItems = Color & {
|
||||
images: (ImageColor & {
|
||||
image: Image & {
|
||||
variants: ImageVariant[]
|
||||
}
|
||||
})[]
|
||||
}
|
||||
|
||||
export default function DisplayColor({ color }: { color: ColorWithItems }) {
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{color.name}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div
|
||||
key={color.id}
|
||||
className="w-10 h-10 rounded-full border"
|
||||
style={{ backgroundColor: color.hex ?? "#ccc" }}
|
||||
title={color.hex ?? "n/a"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Used by {color.images.length} image(s)</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||
{color.images.map((image) => (
|
||||
<ImageColorCard key={image.id} image={image} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
35
src/components/extract/list/ListExtractColors.tsx
Normal file
35
src/components/extract/list/ListExtractColors.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ExtractColor } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
type ColorsWithItems = ExtractColor & {
|
||||
images: { id: string }[]
|
||||
}
|
||||
|
||||
export default function ListExtractColors({ colors }: { colors: ColorsWithItems[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{colors.map((col) => (
|
||||
<Link href={`/extract/${col.id}`} key={col.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{col.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full border"
|
||||
style={{ backgroundColor: col.hex ?? "#ccc" }}
|
||||
title={col.hex ?? "n/a"}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Used by {col.images.length} image{col.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
41
src/components/extract/single/DisplayExtractColor.tsx
Normal file
41
src/components/extract/single/DisplayExtractColor.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
import ImageExtractColorCard from "@/components/images/ImageExtractColorCard";
|
||||
import { ExtractColor, Image, ImageExtractColor, ImageVariant } from "@/generated/prisma";
|
||||
|
||||
type ColorWithItems = ExtractColor & {
|
||||
images: (ImageExtractColor & {
|
||||
image: Image & {
|
||||
variants: ImageVariant[]
|
||||
}
|
||||
})[]
|
||||
}
|
||||
|
||||
export default function DisplayExtractColor({ color }: { color: ColorWithItems }) {
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{color.name}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div
|
||||
key={color.id}
|
||||
className="w-10 h-10 rounded-full border"
|
||||
style={{ backgroundColor: color.hex ?? "#ccc" }}
|
||||
title={color.hex ?? "n/a"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Used by {color.images.length} image(s)</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||
{color.images.map((image) => (
|
||||
<ImageExtractColorCard key={image.id} image={image} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -47,6 +47,11 @@ export default function TopNav() {
|
||||
<Link href="/colors">Colors</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/extract">Extract-Colors</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/images">Images</Link>
|
||||
|
39
src/components/images/ImageColorCard.tsx
Normal file
39
src/components/images/ImageColorCard.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { Image, ImageColor, ImageVariant } from "@/generated/prisma";
|
||||
import ImageComponent from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
type ImagePaletteWithItems = {
|
||||
image: ImageColor & {
|
||||
image: Image & {
|
||||
variants: ImageVariant[];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default function ImageColorCard({ image }: ImagePaletteWithItems) {
|
||||
const thumbnail = image.image.variants.find((v) => v.type === "thumbnail");
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/images/${image.image.id}`}
|
||||
className="block overflow-hidden rounded-md border shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
{thumbnail?.s3Key ? (
|
||||
<ImageComponent
|
||||
src={`/api/image/${thumbnail.s3Key}`}
|
||||
alt={image.image.altText || image.image.imageName}
|
||||
width={thumbnail.width}
|
||||
height={thumbnail.height}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-48 bg-gray-100 flex items-center justify-center text-muted-foreground text-sm">
|
||||
No Thumbnail
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 text-sm truncate">{image.image.imageName} ({image.type})</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
39
src/components/images/ImageExtractColorCard.tsx
Normal file
39
src/components/images/ImageExtractColorCard.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { Image, ImageExtractColor, ImageVariant } from "@/generated/prisma";
|
||||
import ImageComponent from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
type ImagePaletteWithItems = {
|
||||
image: ImageExtractColor & {
|
||||
image: Image & {
|
||||
variants: ImageVariant[];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default function ImageExtractColorCard({ image }: ImagePaletteWithItems) {
|
||||
const thumbnail = image.image.variants.find((v) => v.type === "thumbnail");
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/images/${image.image.id}`}
|
||||
className="block overflow-hidden rounded-md border shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
{thumbnail?.s3Key ? (
|
||||
<ImageComponent
|
||||
src={`/api/image/${thumbnail.s3Key}`}
|
||||
alt={image.image.altText || image.image.imageName}
|
||||
width={thumbnail.width}
|
||||
height={thumbnail.height}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-48 bg-gray-100 flex items-center justify-center text-muted-foreground text-sm">
|
||||
No Thumbnail
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 text-sm truncate">{image.image.imageName} ({image.type})</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
@ -3,11 +3,12 @@
|
||||
import { updateImage } from "@/actions/images/updateImage";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
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 { Album, Artist, Category, Color, ColorPalette, ColorPaletteItem, ExtractColor, Image, ImageColor, ImageExtractColor, ImageMetadata, ImagePalette, ImageStats, ImageVariant, Tag } from "@/generated/prisma";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -68,6 +69,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
imageName: image.imageName,
|
||||
originalFile: image.originalFile,
|
||||
uploadDate: image.uploadDate,
|
||||
nsfw: image.nsfw ?? false,
|
||||
|
||||
altText: image.altText || "",
|
||||
description: image.description || "",
|
||||
@ -82,7 +84,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
albumId: image.album?.id || undefined,
|
||||
tagIds: image.tags?.map(tag => tag.id) ?? [],
|
||||
categoryIds: image.categories?.map(cat => cat.id) ?? [],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// const watchCreationDate = form.watch("creationDate");
|
||||
@ -167,6 +169,22 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
)}
|
||||
/>
|
||||
|
||||
<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="altText"
|
||||
|
@ -7,18 +7,20 @@ import Link from "next/link";
|
||||
|
||||
export default function ListImages({ images }: { images: Image[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
<div className="columns-1 sm:columns-2 md:columns-3 xl:columns-4 gap-4 space-y-4">
|
||||
{images.map((image) => (
|
||||
<Link href={`/images/edit/${image.id}`} key={image.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{image.imageName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<NetImage src={`/api/image/thumbnails/${image.fileKey}.webp`} alt={image.altText ? image.altText : "Image"} width={200} height={200} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<div key={image.id} className="break-inside-avoid">
|
||||
<Link href={`/images/edit/${image.id}`} key={image.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{image.imageName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center items-center">
|
||||
<NetImage src={`/api/image/thumbnails/${image.fileKey}.webp`} alt={image.altText ? image.altText : "Image"} width={200} height={200} className="rounded max-w-full h-auto object-contain" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
31
src/components/ui/switch.tsx
Normal file
31
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
@ -14,6 +14,7 @@ export const imageSchema = z.object({
|
||||
imageName: z.string().min(1, "Image name is required"),
|
||||
originalFile: z.string().min(1, "Original file is required"),
|
||||
uploadDate: z.date(),
|
||||
nsfw: z.boolean(),
|
||||
|
||||
altText: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
|
Reference in New Issue
Block a user