Spaces:
Runtime error
Runtime error
add admin rule
Browse files- app/api/collections/[id]/route.ts +7 -1
- app/api/collections/[id]/visibility/route.ts +55 -0
- app/api/collections/route.ts +12 -0
- app/api/login/route.ts +1 -1
- app/api/me/route.ts +7 -1
- app/api/route.ts +1 -1
- components/button/index.tsx +15 -2
- components/main/collections/collection.tsx +15 -3
- components/main/hooks/useCollections.ts +9 -3
- components/modal/modal.tsx +27 -9
- components/modal/useCollection.ts +63 -5
- prisma/schema.prisma +7 -6
- utils/type.ts +1 -0
app/api/collections/[id]/route.ts
CHANGED
|
@@ -7,7 +7,13 @@ const prisma = new PrismaClient()
|
|
| 7 |
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
|
| 8 |
const { headers } = request
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
return Response.json(
|
| 12 |
{
|
| 13 |
status: 401,
|
|
|
|
| 7 |
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
|
| 8 |
const { headers } = request
|
| 9 |
|
| 10 |
+
const staff_flag_id = headers.get('x-staff-flag-id') ?? undefined
|
| 11 |
+
|
| 12 |
+
// @ts-ignore
|
| 13 |
+
const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
|
| 14 |
+
const is_admin = staff_flag_id ? HF_ADMIN.includes(staff_flag_id) : false
|
| 15 |
+
|
| 16 |
+
if (!is_admin) {
|
| 17 |
return Response.json(
|
| 18 |
{
|
| 19 |
status: 401,
|
app/api/collections/[id]/visibility/route.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PrismaClient } from '@prisma/client'
|
| 2 |
+
|
| 3 |
+
const prisma = new PrismaClient()
|
| 4 |
+
|
| 5 |
+
export async function PUT(request: Request, { params }: { params: { id: string } }) {
|
| 6 |
+
const { headers } = request
|
| 7 |
+
|
| 8 |
+
const staff_flag_id = headers.get('x-staff-flag-id') ?? undefined
|
| 9 |
+
|
| 10 |
+
// @ts-ignore
|
| 11 |
+
const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
|
| 12 |
+
const is_admin = staff_flag_id ? HF_ADMIN.includes(staff_flag_id) : false
|
| 13 |
+
|
| 14 |
+
if (!is_admin) {
|
| 15 |
+
return Response.json(
|
| 16 |
+
{
|
| 17 |
+
status: 401,
|
| 18 |
+
ok: false,
|
| 19 |
+
message: 'Wrong castle fam :/'
|
| 20 |
+
}
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const collection = await prisma.collection.findUnique({
|
| 25 |
+
where: {
|
| 26 |
+
id: parseInt(params.id)
|
| 27 |
+
}
|
| 28 |
+
})
|
| 29 |
+
if (!collection) {
|
| 30 |
+
return Response.json(
|
| 31 |
+
{
|
| 32 |
+
status: 404,
|
| 33 |
+
ok: false
|
| 34 |
+
}
|
| 35 |
+
)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const new_image = await prisma.collection.update({
|
| 39 |
+
where: {
|
| 40 |
+
id: parseInt(params.id)
|
| 41 |
+
},
|
| 42 |
+
data: {
|
| 43 |
+
is_visible: true
|
| 44 |
+
}
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
return Response.json(
|
| 48 |
+
{
|
| 49 |
+
image: new_image,
|
| 50 |
+
message: `Image ${params.id} has been updated`,
|
| 51 |
+
status: 200,
|
| 52 |
+
ok: true
|
| 53 |
+
}
|
| 54 |
+
)
|
| 55 |
+
}
|
app/api/collections/route.ts
CHANGED
|
@@ -3,10 +3,19 @@ import { PrismaClient } from '@prisma/client'
|
|
| 3 |
const prisma = new PrismaClient()
|
| 4 |
|
| 5 |
export async function GET(request: Request) {
|
|
|
|
| 6 |
const { searchParams } = new URL(request.url)
|
| 7 |
const userId = searchParams.get('id') ?? undefined
|
| 8 |
const page = searchParams.get('page') ? parseInt(searchParams.get('page') as string) : 0
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
const collections = await prisma.collection.findMany({
|
| 11 |
orderBy: {
|
| 12 |
id: 'desc'
|
|
@@ -14,6 +23,9 @@ export async function GET(request: Request) {
|
|
| 14 |
where: {
|
| 15 |
userId: {
|
| 16 |
equals: userId
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
},
|
| 19 |
take: 15,
|
|
|
|
| 3 |
const prisma = new PrismaClient()
|
| 4 |
|
| 5 |
export async function GET(request: Request) {
|
| 6 |
+
const { headers } = request
|
| 7 |
const { searchParams } = new URL(request.url)
|
| 8 |
const userId = searchParams.get('id') ?? undefined
|
| 9 |
const page = searchParams.get('page') ? parseInt(searchParams.get('page') as string) : 0
|
| 10 |
|
| 11 |
+
const staff_flag_id = headers.get('x-staff-flag-id') ?? undefined
|
| 12 |
+
|
| 13 |
+
// @ts-ignore
|
| 14 |
+
const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
|
| 15 |
+
const is_admin = staff_flag_id ? HF_ADMIN.includes(staff_flag_id) : false
|
| 16 |
+
|
| 17 |
+
console.log('is_admin', HF_ADMIN, is_admin, staff_flag_id)
|
| 18 |
+
|
| 19 |
const collections = await prisma.collection.findMany({
|
| 20 |
orderBy: {
|
| 21 |
id: 'desc'
|
|
|
|
| 23 |
where: {
|
| 24 |
userId: {
|
| 25 |
equals: userId
|
| 26 |
+
},
|
| 27 |
+
is_visible: {
|
| 28 |
+
equals: is_admin ? undefined : true
|
| 29 |
}
|
| 30 |
},
|
| 31 |
take: 15,
|
app/api/login/route.ts
CHANGED
|
@@ -2,7 +2,7 @@ export async function GET() {
|
|
| 2 |
const REDIRECT_URI = `https://${process.env.SPACE_HOST}/login/callback`
|
| 3 |
return Response.json(
|
| 4 |
{
|
| 5 |
-
redirect: `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=openid%20profile&state=STATE&response_type=code`,
|
| 6 |
status: 200,
|
| 7 |
ok: true
|
| 8 |
}
|
|
|
|
| 2 |
const REDIRECT_URI = `https://${process.env.SPACE_HOST}/login/callback`
|
| 3 |
return Response.json(
|
| 4 |
{
|
| 5 |
+
redirect: `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=openid%20profile%20email&state=STATE&response_type=code`,
|
| 6 |
status: 200,
|
| 7 |
ok: true
|
| 8 |
}
|
app/api/me/route.ts
CHANGED
|
@@ -13,10 +13,16 @@ export async function GET() {
|
|
| 13 |
})
|
| 14 |
|
| 15 |
const res = await request.clone().json().catch(() => ({}));
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
return Response.json(
|
| 18 |
{
|
| 19 |
-
user:
|
|
|
|
|
|
|
|
|
|
| 20 |
status: 200,
|
| 21 |
ok: true
|
| 22 |
}
|
|
|
|
| 13 |
})
|
| 14 |
|
| 15 |
const res = await request.clone().json().catch(() => ({}));
|
| 16 |
+
// @ts-ignore
|
| 17 |
+
const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
|
| 18 |
+
const is_admin = res?.sub ? HF_ADMIN.includes(res?.sub) : false
|
| 19 |
|
| 20 |
return Response.json(
|
| 21 |
{
|
| 22 |
+
user: {
|
| 23 |
+
...res,
|
| 24 |
+
is_admin,
|
| 25 |
+
},
|
| 26 |
status: 200,
|
| 27 |
ok: true
|
| 28 |
}
|
app/api/route.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function POST(
|
|
| 41 |
const imageIsNSFW = await isImageNSFW(blob, global_headers)
|
| 42 |
if (imageIsNSFW) return Response.json({ status: 401, ok: false, message: "Image is not safe for work." });
|
| 43 |
|
| 44 |
-
const name = Date.now() + `-${inputs.replace(/[^a-zA-Z0-9]/g, '-').
|
| 45 |
const { ok, message } = await UploaderDataset(blob, name)
|
| 46 |
|
| 47 |
if (!ok) return Response.json({ status: 500, ok: false, message });
|
|
|
|
| 41 |
const imageIsNSFW = await isImageNSFW(blob, global_headers)
|
| 42 |
if (imageIsNSFW) return Response.json({ status: 401, ok: false, message: "Image is not safe for work." });
|
| 43 |
|
| 44 |
+
const name = Date.now() + `-${inputs.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`
|
| 45 |
const { ok, message } = await UploaderDataset(blob, name)
|
| 46 |
|
| 47 |
if (!ok) return Response.json({ status: 500, ok: false, message });
|
components/button/index.tsx
CHANGED
|
@@ -1,15 +1,17 @@
|
|
| 1 |
import classNames from "classnames";
|
|
|
|
| 2 |
|
| 3 |
interface Props {
|
| 4 |
children: React.ReactNode;
|
| 5 |
disabled?: boolean;
|
| 6 |
-
theme?: "primary" | "secondary" | "white";
|
| 7 |
-
onClick?: () => void;
|
| 8 |
}
|
| 9 |
export const Button: React.FC<Props> = ({
|
| 10 |
children,
|
| 11 |
disabled,
|
| 12 |
theme = "primary",
|
|
|
|
| 13 |
...props
|
| 14 |
}) => {
|
| 15 |
return (
|
|
@@ -19,11 +21,22 @@ export const Button: React.FC<Props> = ({
|
|
| 19 |
{
|
| 20 |
"bg-primary text-white border-primary": theme === "primary",
|
| 21 |
"bg-white text-gray-900 border-white": theme === "white",
|
|
|
|
|
|
|
| 22 |
"!bg-gray-400 !text-gray-600 !cursor-not-allowed !border-gray-400":
|
| 23 |
disabled,
|
| 24 |
}
|
| 25 |
)}
|
| 26 |
{...props}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
>
|
| 28 |
{children}
|
| 29 |
</button>
|
|
|
|
| 1 |
import classNames from "classnames";
|
| 2 |
+
import { MouseEvent } from "react";
|
| 3 |
|
| 4 |
interface Props {
|
| 5 |
children: React.ReactNode;
|
| 6 |
disabled?: boolean;
|
| 7 |
+
theme?: "primary" | "secondary" | "white" | "danger" | "success";
|
| 8 |
+
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
|
| 9 |
}
|
| 10 |
export const Button: React.FC<Props> = ({
|
| 11 |
children,
|
| 12 |
disabled,
|
| 13 |
theme = "primary",
|
| 14 |
+
onClick,
|
| 15 |
...props
|
| 16 |
}) => {
|
| 17 |
return (
|
|
|
|
| 21 |
{
|
| 22 |
"bg-primary text-white border-primary": theme === "primary",
|
| 23 |
"bg-white text-gray-900 border-white": theme === "white",
|
| 24 |
+
"bg-red-500 text-white border-red-500": theme === "danger",
|
| 25 |
+
"bg-green-500 text-white border-green-500": theme === "success",
|
| 26 |
"!bg-gray-400 !text-gray-600 !cursor-not-allowed !border-gray-400":
|
| 27 |
disabled,
|
| 28 |
}
|
| 29 |
)}
|
| 30 |
{...props}
|
| 31 |
+
onClick={
|
| 32 |
+
onClick
|
| 33 |
+
? (e) => {
|
| 34 |
+
e.stopPropagation();
|
| 35 |
+
e.preventDefault();
|
| 36 |
+
onClick(e);
|
| 37 |
+
}
|
| 38 |
+
: undefined
|
| 39 |
+
}
|
| 40 |
>
|
| 41 |
{children}
|
| 42 |
</button>
|
components/main/collections/collection.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
import { useMemo } from "react";
|
| 2 |
import { motion } from "framer-motion";
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
import {
|
| 5 |
import { useInputGeneration } from "@/components/main/hooks/useInputGeneration";
|
| 6 |
-
import {
|
| 7 |
|
| 8 |
interface Props {
|
| 9 |
index: number;
|
|
@@ -63,8 +65,18 @@ export const Collection: React.FC<Props> = ({
|
|
| 63 |
style={{
|
| 64 |
backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection.file_name}.png)`,
|
| 65 |
}}
|
| 66 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
</motion.div>
|
| 69 |
</div>
|
| 70 |
);
|
|
|
|
| 1 |
import { useMemo } from "react";
|
| 2 |
import { motion } from "framer-motion";
|
| 3 |
+
import classNames from "classnames";
|
| 4 |
+
import { AiFillEyeInvisible } from "react-icons/ai";
|
| 5 |
|
| 6 |
+
import { Image } from "@/utils/type";
|
| 7 |
import { useInputGeneration } from "@/components/main/hooks/useInputGeneration";
|
| 8 |
+
import { Button } from "@/components/button";
|
| 9 |
|
| 10 |
interface Props {
|
| 11 |
index: number;
|
|
|
|
| 65 |
style={{
|
| 66 |
backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection.file_name}.png)`,
|
| 67 |
}}
|
| 68 |
+
className={classNames(
|
| 69 |
+
"rounded-[33px] bg-gray-950 bg-cover absolute top-0 left-0 w-full h-full z-[-1] transition-all duration-200 group-hover:scale-110 bg-center",
|
| 70 |
+
{
|
| 71 |
+
"opacity-40": !collection.is_visible,
|
| 72 |
+
}
|
| 73 |
+
)}
|
| 74 |
/>
|
| 75 |
+
{!collection.is_visible && (
|
| 76 |
+
<div className="flex items-center justify-end px-5 py-6">
|
| 77 |
+
<AiFillEyeInvisible className="text-white text-2xl" />
|
| 78 |
+
</div>
|
| 79 |
+
)}
|
| 80 |
</motion.div>
|
| 81 |
</div>
|
| 82 |
);
|
components/main/hooks/useCollections.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
import { useState } from "react";
|
| 2 |
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
| 3 |
-
import {
|
| 4 |
import _ from "lodash";
|
| 5 |
|
| 6 |
import { useUser } from "@/utils/useUser";
|
| 7 |
|
| 8 |
export const useCollections = (category: string) => {
|
| 9 |
const [loading, setLoading] = useState(false);
|
| 10 |
-
const { user } = useUser();
|
| 11 |
-
const [myGenerationsId] = useLocalStorage<any>('my-own-generations', []);
|
| 12 |
|
| 13 |
const client = useQueryClient();
|
| 14 |
|
|
@@ -24,6 +23,9 @@ export const useCollections = (category: string) => {
|
|
| 24 |
queryParams.append('page', '0');
|
| 25 |
|
| 26 |
const response = await fetch(`/api/collections?${queryParams.toString()}`, {
|
|
|
|
|
|
|
|
|
|
| 27 |
method: "GET",
|
| 28 |
})
|
| 29 |
|
|
@@ -38,6 +40,7 @@ export const useCollections = (category: string) => {
|
|
| 38 |
};
|
| 39 |
},
|
| 40 |
{
|
|
|
|
| 41 |
refetchOnMount: false,
|
| 42 |
refetchOnWindowFocus: false,
|
| 43 |
refetchOnReconnect: false,
|
|
@@ -51,6 +54,9 @@ export const useCollections = (category: string) => {
|
|
| 51 |
queryParams.append('page', data?.pagination?.page,);
|
| 52 |
|
| 53 |
const response = await fetch(`/api/collections?${queryParams.toString()}`, {
|
|
|
|
|
|
|
|
|
|
| 54 |
method: "GET",
|
| 55 |
})
|
| 56 |
|
|
|
|
| 1 |
import { useState } from "react";
|
| 2 |
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
| 3 |
+
import { useUpdateEffect } from "react-use";
|
| 4 |
import _ from "lodash";
|
| 5 |
|
| 6 |
import { useUser } from "@/utils/useUser";
|
| 7 |
|
| 8 |
export const useCollections = (category: string) => {
|
| 9 |
const [loading, setLoading] = useState(false);
|
| 10 |
+
const { user, loading: userLoading } = useUser();
|
|
|
|
| 11 |
|
| 12 |
const client = useQueryClient();
|
| 13 |
|
|
|
|
| 23 |
queryParams.append('page', '0');
|
| 24 |
|
| 25 |
const response = await fetch(`/api/collections?${queryParams.toString()}`, {
|
| 26 |
+
headers: {
|
| 27 |
+
'x-staff-flag-id': user?.sub
|
| 28 |
+
},
|
| 29 |
method: "GET",
|
| 30 |
})
|
| 31 |
|
|
|
|
| 40 |
};
|
| 41 |
},
|
| 42 |
{
|
| 43 |
+
enabled: !userLoading,
|
| 44 |
refetchOnMount: false,
|
| 45 |
refetchOnWindowFocus: false,
|
| 46 |
refetchOnReconnect: false,
|
|
|
|
| 54 |
queryParams.append('page', data?.pagination?.page,);
|
| 55 |
|
| 56 |
const response = await fetch(`/api/collections?${queryParams.toString()}`, {
|
| 57 |
+
headers: {
|
| 58 |
+
'x-staff-flag-id': user?.sub
|
| 59 |
+
},
|
| 60 |
method: "GET",
|
| 61 |
})
|
| 62 |
|
components/modal/modal.tsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
| 1 |
import { useMemo } from "react";
|
| 2 |
import { motion } from "framer-motion";
|
| 3 |
import Image from "next/image";
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
import { arrayBufferToBase64 } from "@/utils";
|
| 6 |
import { useCollection } from "./useCollection";
|
|
|
|
|
|
|
| 7 |
|
| 8 |
interface Props {
|
| 9 |
id: string;
|
|
@@ -30,16 +33,17 @@ const dropIn = {
|
|
| 30 |
};
|
| 31 |
|
| 32 |
export const Modal: React.FC<Props> = ({ id, onClose }) => {
|
| 33 |
-
const { collection } = useCollection(id);
|
| 34 |
-
|
| 35 |
-
if (!collection) return null;
|
| 36 |
|
| 37 |
const formatDate = useMemo(() => {
|
| 38 |
if (!collection) return;
|
| 39 |
-
const date = new Date(collection
|
| 40 |
return date.toLocaleDateString();
|
| 41 |
}, [collection?.createdAt]);
|
| 42 |
|
|
|
|
|
|
|
| 43 |
return (
|
| 44 |
<motion.div
|
| 45 |
onClick={onClose}
|
|
@@ -50,14 +54,28 @@ export const Modal: React.FC<Props> = ({ id, onClose }) => {
|
|
| 50 |
>
|
| 51 |
<motion.div
|
| 52 |
onClick={(e) => e.stopPropagation()}
|
| 53 |
-
className="max-w-2xl h-auto w-full z-[1] rounded-3xl overflow-hidden flex items-center justify-center flex-col gap-4 bg-white/30 backdrop-blur-sm px-2 pb-2 pt-2"
|
| 54 |
variants={dropIn}
|
| 55 |
initial="hidden"
|
| 56 |
animate="visible"
|
| 57 |
exit="exit"
|
| 58 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
<Image
|
| 60 |
-
src={`https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection
|
| 61 |
alt="Generated image"
|
| 62 |
className="object-center object-contain w-full h-full rounded-2xl"
|
| 63 |
width={1024}
|
|
@@ -66,13 +84,13 @@ export const Modal: React.FC<Props> = ({ id, onClose }) => {
|
|
| 66 |
<div
|
| 67 |
className="bg-cover bg-center w-full h-full rounded-2xl bg-no-repeat z-[-1] absolute top-0 left-0 opacity-90 blur-xl"
|
| 68 |
style={{
|
| 69 |
-
backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection
|
| 70 |
}}
|
| 71 |
/>
|
| 72 |
<div className="text-left w-full px-4 pb-3 pt-2">
|
| 73 |
<p className="text-sm font-medium text-white/60 mb-1">{formatDate}</p>
|
| 74 |
<p className="text-xl font-semibold text-white lowercase leading-snug">
|
| 75 |
-
{collection
|
| 76 |
</p>
|
| 77 |
</div>
|
| 78 |
</motion.div>
|
|
|
|
| 1 |
import { useMemo } from "react";
|
| 2 |
import { motion } from "framer-motion";
|
| 3 |
import Image from "next/image";
|
| 4 |
+
import { BsFillTrashFill } from "react-icons/bs";
|
| 5 |
+
import { AiFillCheckCircle } from "react-icons/ai";
|
| 6 |
|
|
|
|
| 7 |
import { useCollection } from "./useCollection";
|
| 8 |
+
import { Button } from "../button";
|
| 9 |
+
import { useUser } from "@/utils/useUser";
|
| 10 |
|
| 11 |
interface Props {
|
| 12 |
id: string;
|
|
|
|
| 33 |
};
|
| 34 |
|
| 35 |
export const Modal: React.FC<Props> = ({ id, onClose }) => {
|
| 36 |
+
const { collection, updateVisibility, remove } = useCollection(id);
|
| 37 |
+
const { user } = useUser();
|
|
|
|
| 38 |
|
| 39 |
const formatDate = useMemo(() => {
|
| 40 |
if (!collection) return;
|
| 41 |
+
const date = new Date(collection?.createdAt);
|
| 42 |
return date.toLocaleDateString();
|
| 43 |
}, [collection?.createdAt]);
|
| 44 |
|
| 45 |
+
console.log(user);
|
| 46 |
+
|
| 47 |
return (
|
| 48 |
<motion.div
|
| 49 |
onClick={onClose}
|
|
|
|
| 54 |
>
|
| 55 |
<motion.div
|
| 56 |
onClick={(e) => e.stopPropagation()}
|
| 57 |
+
className="max-w-2xl h-auto w-full z-[1] rounded-3xl overflow-hidden relative flex items-center justify-center flex-col gap-4 bg-white/30 backdrop-blur-sm px-2 pb-2 pt-2"
|
| 58 |
variants={dropIn}
|
| 59 |
initial="hidden"
|
| 60 |
animate="visible"
|
| 61 |
exit="exit"
|
| 62 |
>
|
| 63 |
+
{user?.is_admin && (
|
| 64 |
+
<div className="absolute p-2 rounded-full top-4 left-4 flex items-center justify-start gap-2 bg-black/20 backdrop-blur">
|
| 65 |
+
{!collection?.is_visible && (
|
| 66 |
+
<Button theme="white" onClick={updateVisibility}>
|
| 67 |
+
<AiFillCheckCircle />
|
| 68 |
+
Validate
|
| 69 |
+
</Button>
|
| 70 |
+
)}
|
| 71 |
+
<Button theme="danger" onClick={remove}>
|
| 72 |
+
<BsFillTrashFill />
|
| 73 |
+
Delete
|
| 74 |
+
</Button>
|
| 75 |
+
</div>
|
| 76 |
+
)}
|
| 77 |
<Image
|
| 78 |
+
src={`https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection?.file_name}.png?raw=true`}
|
| 79 |
alt="Generated image"
|
| 80 |
className="object-center object-contain w-full h-full rounded-2xl"
|
| 81 |
width={1024}
|
|
|
|
| 84 |
<div
|
| 85 |
className="bg-cover bg-center w-full h-full rounded-2xl bg-no-repeat z-[-1] absolute top-0 left-0 opacity-90 blur-xl"
|
| 86 |
style={{
|
| 87 |
+
backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection?.file_name}.png?raw=true)`,
|
| 88 |
}}
|
| 89 |
/>
|
| 90 |
<div className="text-left w-full px-4 pb-3 pt-2">
|
| 91 |
<p className="text-sm font-medium text-white/60 mb-1">{formatDate}</p>
|
| 92 |
<p className="text-xl font-semibold text-white lowercase leading-snug">
|
| 93 |
+
{collection?.prompt}
|
| 94 |
</p>
|
| 95 |
</div>
|
| 96 |
</motion.div>
|
components/modal/useCollection.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
| 1 |
import { useMemo, useState } from "react"
|
| 2 |
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
| 3 |
|
| 4 |
-
import { Collection } from "@/utils/type"
|
|
|
|
|
|
|
| 5 |
|
| 6 |
export const useCollection = (id?: string) => {
|
|
|
|
|
|
|
|
|
|
| 7 |
const { data: open } = useQuery(["modal"], () => {
|
| 8 |
return null
|
| 9 |
}, {
|
|
@@ -18,14 +23,67 @@ export const useCollection = (id?: string) => {
|
|
| 18 |
|
| 19 |
const collection = useMemo(() => {
|
| 20 |
const collections = client.getQueryData<Collection>(["collections"])
|
| 21 |
-
if (!collections?.images)
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
return collections?.images?.find((collection) => collection.id === id)
|
| 24 |
-
}, [id])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
return {
|
| 27 |
collection,
|
| 28 |
open,
|
| 29 |
-
setOpen
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
}
|
|
|
|
| 1 |
import { useMemo, useState } from "react"
|
| 2 |
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
| 3 |
|
| 4 |
+
import { Collection, Image } from "@/utils/type"
|
| 5 |
+
import { useUser } from "@/utils/useUser"
|
| 6 |
+
import { set } from "lodash"
|
| 7 |
|
| 8 |
export const useCollection = (id?: string) => {
|
| 9 |
+
const { user } = useUser()
|
| 10 |
+
const [loading, setLoading] = useState(false)
|
| 11 |
+
|
| 12 |
const { data: open } = useQuery(["modal"], () => {
|
| 13 |
return null
|
| 14 |
}, {
|
|
|
|
| 23 |
|
| 24 |
const collection = useMemo(() => {
|
| 25 |
const collections = client.getQueryData<Collection>(["collections"])
|
| 26 |
+
if (!collections?.images) {
|
| 27 |
+
setOpen(null)
|
| 28 |
+
return null
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
return collections?.images?.find((collection) => collection.id === id)
|
| 32 |
+
}, [id, loading])
|
| 33 |
+
|
| 34 |
+
const updateVisibility = async () => {
|
| 35 |
+
setLoading(true)
|
| 36 |
+
const response = await fetch(`/api/collections/${collection?.id}/visibility`, {
|
| 37 |
+
method: "PUT",
|
| 38 |
+
headers: {
|
| 39 |
+
'x-staff-flag-id': user?.sub
|
| 40 |
+
}
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
const data = await response.json()
|
| 44 |
+
if (data.ok) {
|
| 45 |
+
client.setQueryData(["collections"], (old: any) => {
|
| 46 |
+
return {
|
| 47 |
+
...old,
|
| 48 |
+
images: old.images.map((collection: Image) => {
|
| 49 |
+
if (collection.id === data.image.id) {
|
| 50 |
+
return data.image
|
| 51 |
+
}
|
| 52 |
+
return collection
|
| 53 |
+
})
|
| 54 |
+
}
|
| 55 |
+
})
|
| 56 |
+
}
|
| 57 |
+
setLoading(false)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const remove = async () => {
|
| 61 |
+
setLoading(true)
|
| 62 |
+
const response = await fetch(`/api/collections/${collection?.id}`, {
|
| 63 |
+
method: "DELETE",
|
| 64 |
+
headers: {
|
| 65 |
+
'x-staff-flag-id': user?.sub
|
| 66 |
+
}
|
| 67 |
+
})
|
| 68 |
+
|
| 69 |
+
const data = await response.json()
|
| 70 |
+
if (data.ok) {
|
| 71 |
+
client.setQueryData(["collections"], (old: any) => {
|
| 72 |
+
return {
|
| 73 |
+
...old,
|
| 74 |
+
images: old.images.filter((col: Image) => col.id !== collection?.id)
|
| 75 |
+
}
|
| 76 |
+
})
|
| 77 |
+
setOpen(null)
|
| 78 |
+
}
|
| 79 |
+
setLoading(false)
|
| 80 |
+
}
|
| 81 |
|
| 82 |
return {
|
| 83 |
collection,
|
| 84 |
open,
|
| 85 |
+
setOpen,
|
| 86 |
+
updateVisibility,
|
| 87 |
+
remove
|
| 88 |
}
|
| 89 |
}
|
prisma/schema.prisma
CHANGED
|
@@ -4,13 +4,14 @@ generator client {
|
|
| 4 |
|
| 5 |
datasource db {
|
| 6 |
provider = "sqlite"
|
| 7 |
-
url = "file
|
| 8 |
}
|
| 9 |
|
| 10 |
model Collection {
|
| 11 |
-
id
|
| 12 |
-
prompt
|
| 13 |
-
file_name
|
| 14 |
-
|
| 15 |
-
|
|
|
|
| 16 |
}
|
|
|
|
| 4 |
|
| 5 |
datasource db {
|
| 6 |
provider = "sqlite"
|
| 7 |
+
url = "file:../data/dev.db"
|
| 8 |
}
|
| 9 |
|
| 10 |
model Collection {
|
| 11 |
+
id Int @id @default(autoincrement())
|
| 12 |
+
prompt String
|
| 13 |
+
file_name String
|
| 14 |
+
is_visible Boolean @default(false)
|
| 15 |
+
createdAt DateTime @default(now())
|
| 16 |
+
userId String? @default("")
|
| 17 |
}
|
utils/type.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface Image {
|
|
| 12 |
file_name: string;
|
| 13 |
prompt: string;
|
| 14 |
createdAt: string;
|
|
|
|
| 15 |
error?: string;
|
| 16 |
loading?: boolean;
|
| 17 |
}
|
|
|
|
| 12 |
file_name: string;
|
| 13 |
prompt: string;
|
| 14 |
createdAt: string;
|
| 15 |
+
is_visible: boolean;
|
| 16 |
error?: string;
|
| 17 |
loading?: boolean;
|
| 18 |
}
|