From 54937e3c5a414a04705f914131179969f0a12e54 Mon Sep 17 00:00:00 2001 From: Baptiste Adrien Date: Sun, 22 Oct 2023 17:54:45 +0200 Subject: [PATCH] feat: switch to app dir (#58) --- package.json | 16 +- src/app/(auth)/dashboard/page.tsx | 10 + src/app/(auth)/layout.tsx | 11 + src/app/(auth)/studio/[id]/page.tsx | 57 + src/app/(public)/faq/page.tsx | 10 + src/app/(public)/gallery/[userId]/page.tsx | 29 + src/app/(public)/how-it-works/page.tsx | 10 + src/app/(public)/login/page.tsx | 15 + .../prompts/dreambooth/[slug]/page.tsx | 45 + src/app/(public)/prompts/page.tsx | 12 + src/app/(public)/terms/page.tsx | 10 + .../api/auth/[...nextauth]/route.ts} | 6 +- .../check/[ppi]/[sessionId]/shot/route.ts} | 45 +- .../check/[ppi]/[sessionId]/studio/route.ts | 37 + .../api/checkout/session/route.ts} | 22 +- .../api/checkout/shots/route.ts} | 27 +- .../predictions/[predictionId]/hd/route.ts | 99 + .../[id]/predictions/[predictionId]/route.ts | 89 + .../api/projects/[id]/predictions/route.ts} | 33 +- .../api/projects/[id]/prompter/route.ts} | 32 +- src/app/api/projects/[id]/route.ts | 76 + .../api/projects/[id]/train/route.ts} | 24 +- src/app/api/projects/route.ts | 72 + src/app/api/projects/shots/route.ts | 44 + src/app/layout.tsx | 62 + src/app/page.tsx | 8 + src/app/sitemap.ts | 17 + .../_app.tsx => components/Providers.tsx} | 33 +- src/components/dashboard/Uploader.tsx | 4 +- src/components/layout/DefaultHead.tsx | 31 - src/components/layout/Header.tsx | 16 +- .../pages/DashboardPage.tsx} | 38 +- src/components/pages/FaqPage.tsx | 91 + .../pages/GalleryPage.tsx} | 39 +- src/components/pages/HomePage.tsx | 21 + src/components/pages/HowItWorksPage.tsx | 59 + .../pages/LoginPage.tsx} | 6 +- src/components/pages/StudioPage.tsx | 41 + src/components/pages/TermsPage.tsx | 326 ++++ .../pages/prompts/PromptDetailPage.tsx} | 42 +- .../pages/prompts/PromptsListPage.tsx} | 11 +- src/components/projects/FormPayment.tsx | 14 +- src/components/projects/ProjectCard.tsx | 11 +- .../projects/ProjectCardSkeleton.tsx | 27 + src/components/projects/PromptImage.tsx | 7 +- src/components/projects/PromptWizardPanel.tsx | 9 +- .../projects/shot/BuyShotButton.tsx | 29 +- src/components/projects/shot/ShotCard.tsx | 19 +- src/components/projects/shot/ShotImage.tsx | 16 - src/contexts/project-context.tsx | 4 +- src/lib/sessions.ts | 34 + src/lib/stripe.ts | 5 + .../check/[ppi]/[sessionId]/studio.ts | 31 - src/pages/api/projects/[id]/index.ts | 66 - .../[id]/predictions/[predictionId]/hd.ts | 75 - .../[id]/predictions/[predictionId]/index.ts | 68 - src/pages/api/projects/[id]/shots.ts | 37 - src/pages/api/projects/index.ts | 70 - src/pages/faq.tsx | 93 - src/pages/how-it-works.tsx | 65 - src/pages/index.tsx | 20 - src/pages/sitemap.xml.ts | 50 - src/pages/studio/[id].tsx | 96 - src/pages/terms.tsx | 335 ---- src/styles/theme.ts | 2 +- yarn.lock | 1732 ++++++++++++++--- 66 files changed, 2994 insertions(+), 1597 deletions(-) create mode 100644 src/app/(auth)/dashboard/page.tsx create mode 100644 src/app/(auth)/layout.tsx create mode 100644 src/app/(auth)/studio/[id]/page.tsx create mode 100644 src/app/(public)/faq/page.tsx create mode 100644 src/app/(public)/gallery/[userId]/page.tsx create mode 100644 src/app/(public)/how-it-works/page.tsx create mode 100644 src/app/(public)/login/page.tsx create mode 100644 src/app/(public)/prompts/dreambooth/[slug]/page.tsx create mode 100644 src/app/(public)/prompts/page.tsx create mode 100644 src/app/(public)/terms/page.tsx rename src/{pages/api/auth/[...nextauth].tsx => app/api/auth/[...nextauth]/route.ts} (88%) rename src/{pages/api/checkout/check/[ppi]/[sessionId]/shot.ts => app/api/checkout/check/[ppi]/[sessionId]/shot/route.ts} (58%) create mode 100644 src/app/api/checkout/check/[ppi]/[sessionId]/studio/route.ts rename src/{pages/api/checkout/session.ts => app/api/checkout/session/route.ts} (60%) rename src/{pages/api/checkout/shots.ts => app/api/checkout/shots/route.ts} (66%) create mode 100644 src/app/api/projects/[id]/predictions/[predictionId]/hd/route.ts create mode 100644 src/app/api/projects/[id]/predictions/[predictionId]/route.ts rename src/{pages/api/projects/[id]/predictions/index.ts => app/api/projects/[id]/predictions/route.ts} (55%) rename src/{pages/api/projects/[id]/prompter.ts => app/api/projects/[id]/prompter/route.ts} (56%) create mode 100644 src/app/api/projects/[id]/route.ts rename src/{pages/api/projects/[id]/train.ts => app/api/projects/[id]/train/route.ts} (77%) create mode 100644 src/app/api/projects/route.ts create mode 100644 src/app/api/projects/shots/route.ts create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/sitemap.ts rename src/{pages/_app.tsx => components/Providers.tsx} (64%) delete mode 100644 src/components/layout/DefaultHead.tsx rename src/{pages/dashboard.tsx => components/pages/DashboardPage.tsx} (67%) create mode 100644 src/components/pages/FaqPage.tsx rename src/{pages/gallery/[userId].tsx => components/pages/GalleryPage.tsx} (58%) create mode 100644 src/components/pages/HomePage.tsx create mode 100644 src/components/pages/HowItWorksPage.tsx rename src/{pages/login.tsx => components/pages/LoginPage.tsx} (72%) create mode 100644 src/components/pages/StudioPage.tsx create mode 100644 src/components/pages/TermsPage.tsx rename src/{pages/prompts/dreambooth/[slug].tsx => components/pages/prompts/PromptDetailPage.tsx} (77%) rename src/{pages/prompts/index.tsx => components/pages/prompts/PromptsListPage.tsx} (94%) create mode 100644 src/components/projects/ProjectCardSkeleton.tsx create mode 100644 src/lib/sessions.ts create mode 100644 src/lib/stripe.ts delete mode 100644 src/pages/api/checkout/check/[ppi]/[sessionId]/studio.ts delete mode 100644 src/pages/api/projects/[id]/index.ts delete mode 100644 src/pages/api/projects/[id]/predictions/[predictionId]/hd.ts delete mode 100644 src/pages/api/projects/[id]/predictions/[predictionId]/index.ts delete mode 100644 src/pages/api/projects/[id]/shots.ts delete mode 100644 src/pages/api/projects/index.ts delete mode 100644 src/pages/faq.tsx delete mode 100644 src/pages/how-it-works.tsx delete mode 100644 src/pages/index.tsx delete mode 100644 src/pages/sitemap.xml.ts delete mode 100644 src/pages/studio/[id].tsx delete mode 100644 src/pages/terms.tsx diff --git a/package.json b/package.json index cc425f2..8cb5de7 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "@chakra-ui/react": "^2.4.2", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@next-auth/prisma-adapter": "^1.0.5", - "@next/font": "^13.0.6", + "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^4.7.1", "@stripe/react-stripe-js": "^1.16.0", "@stripe/stripe-js": "^1.46.0", @@ -26,20 +25,21 @@ "@types/react": "18.0.26", "@types/react-dom": "18.0.9", "@types/uniqid": "^5.3.2", - "@vercel/analytics": "^0.1.5", + "@vercel/analytics": "^1.1.1", + "aws-crt": "^1.18.1", "axios": "^1.2.0", "date-fns": "^2.29.3", "eslint": "8.29.0", "eslint-config-next": "13.0.6", - "framer-motion": "^7.6.19", + "framer-motion": "^10.16.4", "image-blob-reduce": "^4.1.0", "jszip": "^3.10.1", "keen-slider": "^6.8.5", "mjml": "^4.13.0", "mjml-react": "^2.0.8", - "next": "13.0.6", - "next-auth": "^4.18.0", - "next-s3-upload": "^0.2.8", + "next": "13.5.6", + "next-auth": "^4.24.3", + "next-s3-upload": "^0.3.3", "nodemailer": "^6.9.0", "openai": "^3.1.0", "plaiceholder": "^2.5.0", @@ -48,7 +48,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-icons": "^4.7.1", - "react-medium-image-zoom": "^5.1.2", + "react-medium-image-zoom": "^5.1.8", "react-parallax-tilt": "^1.7.77", "react-query": "^3.39.2", "sharp": "^0.31.2", diff --git a/src/app/(auth)/dashboard/page.tsx b/src/app/(auth)/dashboard/page.tsx new file mode 100644 index 0000000..c062052 --- /dev/null +++ b/src/app/(auth)/dashboard/page.tsx @@ -0,0 +1,10 @@ +import DashboardPage from "@/components/pages/DashboardPage"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Dashboard", +}; + +const Dashboard = () => ; + +export default Dashboard; diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..7c76930 --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,11 @@ +import { getCurrentUserOrRedirect } from "@/lib/sessions"; + +type Props = { + children: React.ReactNode; +}; + +export default async function Layout({ children }: Props) { + await getCurrentUserOrRedirect(); + + return <>{children}; +} diff --git a/src/app/(auth)/studio/[id]/page.tsx b/src/app/(auth)/studio/[id]/page.tsx new file mode 100644 index 0000000..7dc310b --- /dev/null +++ b/src/app/(auth)/studio/[id]/page.tsx @@ -0,0 +1,57 @@ +import StudioPage from "@/components/pages/StudioPage"; +import replicateClient from "@/core/clients/replicate"; +import db from "@/core/db"; +import { getCurrentSessionRedirect } from "@/lib/sessions"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; + +const PROJECTS_PER_PAGE = 9; + +export const metadata: Metadata = { + title: "My Studio", +}; + +const Studio = async ({ params }: { params: { id: string } }) => { + const session = await getCurrentSessionRedirect(); + const projectId = params.id; + + const project = await db.project.findFirst({ + where: { + id: projectId, + userId: session.userId, + modelStatus: "succeeded", + }, + include: { + _count: { + select: { shots: true }, + }, + shots: { + orderBy: { createdAt: "desc" }, + take: PROJECTS_PER_PAGE, + skip: 0, + }, + }, + orderBy: { createdAt: "desc" }, + }); + + if (!project) { + notFound(); + } + + const { data: model } = await replicateClient.get( + `https://api.replicate.com/v1/models/${process.env.REPLICATE_USERNAME}/${project.id}/versions/${project.modelVersionId}` + ); + + const hasImageInputAvailable = Boolean( + model.openapi_schema?.components?.schemas?.Input?.properties?.image?.title + ); + + return ( + + ); +}; + +export default Studio; diff --git a/src/app/(public)/faq/page.tsx b/src/app/(public)/faq/page.tsx new file mode 100644 index 0000000..f79e1b3 --- /dev/null +++ b/src/app/(public)/faq/page.tsx @@ -0,0 +1,10 @@ +import FaqPage from "@/components/pages/FaqPage"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "FAQ", +}; + +const Faq = () => ; + +export default Faq; diff --git a/src/app/(public)/gallery/[userId]/page.tsx b/src/app/(public)/gallery/[userId]/page.tsx new file mode 100644 index 0000000..ff7cba7 --- /dev/null +++ b/src/app/(public)/gallery/[userId]/page.tsx @@ -0,0 +1,29 @@ +import GalleryPage from "@/components/pages/GalleryPage"; +import db from "@/core/db"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Gallery", +}; + +const Gallery = async ({ params }: { params: { userId: string } }) => { + const userId = params.userId; + + const shots = await db.shot.findMany({ + select: { outputUrl: true, blurhash: true }, + orderBy: { createdAt: "desc" }, + where: { + outputUrl: { not: { equals: null } }, + bookmarked: true, + Project: { + userId: { + equals: userId, + }, + }, + }, + }); + + return ; +}; + +export default Gallery; diff --git a/src/app/(public)/how-it-works/page.tsx b/src/app/(public)/how-it-works/page.tsx new file mode 100644 index 0000000..0f8daae --- /dev/null +++ b/src/app/(public)/how-it-works/page.tsx @@ -0,0 +1,10 @@ +import HowItWorksPage from "@/components/pages/HowItWorksPage"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "AI Avatar: how it works", +}; + +const HowItWorks = () => ; + +export default HowItWorks; diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx new file mode 100644 index 0000000..a8ad7ec --- /dev/null +++ b/src/app/(public)/login/page.tsx @@ -0,0 +1,15 @@ +import LoginPage from "@/components/pages/LoginPage"; +import { getCurrentUser } from "@/lib/sessions"; +import { redirect } from "next/navigation"; + +const Login = async () => { + const user = await getCurrentUser(); + + if (user) { + redirect("/dashboard"); + } + + return ; +}; + +export default Login; diff --git a/src/app/(public)/prompts/dreambooth/[slug]/page.tsx b/src/app/(public)/prompts/dreambooth/[slug]/page.tsx new file mode 100644 index 0000000..485ca24 --- /dev/null +++ b/src/app/(public)/prompts/dreambooth/[slug]/page.tsx @@ -0,0 +1,45 @@ +import PromptDetailPage, { + TPrompt, +} from "@/components/pages/prompts/PromptDetailPage"; +import { prompts } from "@/core/utils/prompts"; + +export function generateStaticParams() { + return prompts.map((prompt) => ({ + slug: prompt.slug, + })); +} + +export async function generateMetadata({ + params, +}: { + params: { slug: string }; +}) { + const slug = params?.slug as string; + const prompt = prompts.find((prompt) => prompt.slug === slug)!; + + return { + title: `Free prompt ${prompt.label} - Photoshot`, + description: + "Our free AI prompt covers a wide range of themes and topics to help you create a unique avatar. Use theme with our Studio or your Stable Diffusion or Dreambooth models.", + }; +} + +const PromptDetail = async ({ params }: { params: { slug: string } }) => { + const slug = params?.slug as string; + const promptIndex = prompts.findIndex((prompt) => prompt.slug === slug)!; + const prompt = prompts[promptIndex]; + + const morePrompts: TPrompt[] = []; + + for (let i = promptIndex + 1; i < promptIndex + 6; i++) { + if (i > prompts.length - 1) { + morePrompts.push(prompts[Math.abs(i - prompts.length)]); + } else { + morePrompts.push(prompts[i]); + } + } + + return ; +}; + +export default PromptDetail; diff --git a/src/app/(public)/prompts/page.tsx b/src/app/(public)/prompts/page.tsx new file mode 100644 index 0000000..68f8bbf --- /dev/null +++ b/src/app/(public)/prompts/page.tsx @@ -0,0 +1,12 @@ +import PromptsListPage from "@/components/pages/prompts/PromptsListPage"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "AI Prompts Inspiration", + description: + "Our free AI prompt covers a wide range of themes and topics to help you create a unique avatar. Use theme with our Studio or your Stable Diffusion or Dreambooth models.", +}; + +const PromptsList = () => ; + +export default PromptsList; diff --git a/src/app/(public)/terms/page.tsx b/src/app/(public)/terms/page.tsx new file mode 100644 index 0000000..fce6ee6 --- /dev/null +++ b/src/app/(public)/terms/page.tsx @@ -0,0 +1,10 @@ +import TermsPage from "@/components/pages/TermsPage"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Photoshot Privacy Policy", +}; + +const Terms = () => ; + +export default Terms; diff --git a/src/pages/api/auth/[...nextauth].tsx b/src/app/api/auth/[...nextauth]/route.ts similarity index 88% rename from src/pages/api/auth/[...nextauth].tsx rename to src/app/api/auth/[...nextauth]/route.ts index 276ffc3..0327e63 100644 --- a/src/pages/api/auth/[...nextauth].tsx +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,7 +1,7 @@ +import db from "@/core/db"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import NextAuth, { NextAuthOptions } from "next-auth"; import EmailProvider from "next-auth/providers/email"; -import db from "@/core/db"; export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(db), @@ -24,4 +24,6 @@ export const authOptions: NextAuthOptions = { secret: process.env.SECRET, }; -export default NextAuth(authOptions); +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/src/pages/api/checkout/check/[ppi]/[sessionId]/shot.ts b/src/app/api/checkout/check/[ppi]/[sessionId]/shot/route.ts similarity index 58% rename from src/pages/api/checkout/check/[ppi]/[sessionId]/shot.ts rename to src/app/api/checkout/check/[ppi]/[sessionId]/shot/route.ts index c273ae9..539319f 100644 --- a/src/pages/api/checkout/check/[ppi]/[sessionId]/shot.ts +++ b/src/app/api/checkout/check/[ppi]/[sessionId]/shot/route.ts @@ -1,17 +1,13 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import Stripe from "stripe"; import db from "@/core/db"; +import { stripe } from "@/lib/stripe"; +import { NextResponse } from "next/server"; -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2022-11-15", -}); - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse +export async function GET( + req: Request, + { params }: { params: { ppi: string; sessionId: string } } ) { - const sessionId = req.query.sessionId as string; - const ppi = req.query.ppi as string; + const sessionId = params.sessionId; + const ppi = params.ppi; const session = await stripe.checkout.sessions.retrieve(sessionId); @@ -25,9 +21,10 @@ export default async function handler( }); if (payments.length > 0) { - return res - .status(400) - .json({ success: false, error: "payment_already_processed" }); + return NextResponse.json( + { success: false, error: "payment_already_processed" }, + { status: 400 } + ); } if ( @@ -54,12 +51,20 @@ export default async function handler( }, }); - return res.status(200).json({ - success: true, - credits: project.credits, - promptWizardCredits: project.promptWizardCredits, - }); + return NextResponse.json( + { + success: true, + credits: project.credits, + promptWizardCredits: project.promptWizardCredits, + }, + { status: 200 } + ); } - return res.status(400).json({ success: false }); + return NextResponse.json( + { + success: false, + }, + { status: 400 } + ); } diff --git a/src/app/api/checkout/check/[ppi]/[sessionId]/studio/route.ts b/src/app/api/checkout/check/[ppi]/[sessionId]/studio/route.ts new file mode 100644 index 0000000..f8e1a61 --- /dev/null +++ b/src/app/api/checkout/check/[ppi]/[sessionId]/studio/route.ts @@ -0,0 +1,37 @@ +import db from "@/core/db"; +import { stripe } from "@/lib/stripe"; +import { NextResponse } from "next/server"; + +export async function GET( + req: Request, + { params }: { params: { ppi: string; sessionId: string } } +) { + const sessionId = params.sessionId; + const ppi = params.ppi; + + const session = await stripe.checkout.sessions.retrieve(sessionId); + + if ( + session.payment_status === "paid" && + session.metadata?.projectId === ppi + ) { + await db.project.update({ + where: { id: ppi }, + data: { stripePaymentId: session.id }, + }); + + return NextResponse.json( + { + success: true, + }, + { status: 200 } + ); + } + + return NextResponse.json( + { + success: false, + }, + { status: 400 } + ); +} diff --git a/src/pages/api/checkout/session.ts b/src/app/api/checkout/session/route.ts similarity index 60% rename from src/pages/api/checkout/session.ts rename to src/app/api/checkout/session/route.ts index a030fee..efd3c85 100644 --- a/src/pages/api/checkout/session.ts +++ b/src/app/api/checkout/session/route.ts @@ -1,19 +1,15 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import Stripe from "stripe"; +import { stripe } from "@/lib/stripe"; +import { NextResponse } from "next/server"; -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2022-11-15", -}); +export async function GET(req: Request) { + const url = new URL(req.url); + const ppi = url.searchParams.get("ppi"); -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { try { const session = await stripe.checkout.sessions.create({ allow_promotion_codes: true, metadata: { - projectId: req.query.ppi as string, + projectId: ppi as string, }, line_items: [ { @@ -28,12 +24,12 @@ export default async function handler( }, ], mode: "payment", - success_url: `${process.env.NEXTAUTH_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}&ppi=${req.query.ppi}`, + success_url: `${process.env.NEXTAUTH_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}&ppi=${ppi}`, cancel_url: `${process.env.NEXTAUTH_URL}/dashboard`, }); - return res.redirect(303, session.url!); + return NextResponse.redirect(session.url!, 303); } catch (err: any) { - return res.status(400).json(err.message); + return NextResponse.json(err.message, { status: 400 }); } } diff --git a/src/pages/api/checkout/shots.ts b/src/app/api/checkout/shots/route.ts similarity index 66% rename from src/pages/api/checkout/shots.ts rename to src/app/api/checkout/shots/route.ts index 9720485..c4f92db 100644 --- a/src/pages/api/checkout/shots.ts +++ b/src/app/api/checkout/shots/route.ts @@ -1,9 +1,5 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import Stripe from "stripe"; - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2022-11-15", -}); +import { stripe } from "@/lib/stripe"; +import { NextResponse } from "next/server"; const PRICES = { 100: { price: 400, promptWizardQuantity: 20 }, @@ -11,22 +7,21 @@ const PRICES = { 300: { price: 900, promptWizardQuantity: 80 }, }; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const quantity = Number(req.query.quantity); - const ppi = req.query.ppi; +export async function GET(req: Request) { + const url = new URL(req.url); + + const quantity = Number(url.searchParams.get("quantity")); + const ppi = url.searchParams.get("ppi"); if (quantity !== 100 && quantity !== 200 && quantity !== 300) { - return res.status(400).json("invalid_quantity"); + return NextResponse.json("invalid_quantity", { status: 400 }); } try { const session = await stripe.checkout.sessions.create({ allow_promotion_codes: true, metadata: { - projectId: req.query.ppi as string, + projectId: ppi, quantity, promptWizardQuantity: PRICES[quantity].promptWizardQuantity, }, @@ -47,8 +42,8 @@ export default async function handler( cancel_url: `${process.env.NEXTAUTH_URL}/studio/${ppi}`, }); - return res.redirect(303, session.url!); + return NextResponse.redirect(session.url!, 303); } catch (err: any) { - return res.status(400).json(err.message); + return NextResponse.json(err.message, { status: 400 }); } } diff --git a/src/app/api/projects/[id]/predictions/[predictionId]/hd/route.ts b/src/app/api/projects/[id]/predictions/[predictionId]/hd/route.ts new file mode 100644 index 0000000..7345e66 --- /dev/null +++ b/src/app/api/projects/[id]/predictions/[predictionId]/hd/route.ts @@ -0,0 +1,99 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import replicateClient from "@/core/clients/replicate"; +import db from "@/core/db"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; + +export async function GET( + request: Request, + { params }: { params: { id: string; predictionId: string } } +) { + const projectId = params.id; + const predictionId = params.predictionId; + + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const project = await db.project.findFirstOrThrow({ + where: { id: projectId, userId: session.userId }, + }); + + let shot = await db.shot.findFirstOrThrow({ + where: { projectId: project.id, id: predictionId }, + }); + + if (shot.hdStatus !== "PENDING") { + return NextResponse.json( + { message: "4K already applied" }, + { status: 400 } + ); + } + + const { data: prediction } = await replicateClient.get( + `https://api.replicate.com/v1/predictions/${shot.hdPredictionId}` + ); + + if (prediction.output) { + shot = await db.shot.update({ + where: { id: shot.id }, + data: { + hdStatus: "PROCESSED", + hdOutputUrl: prediction.output, + }, + }); + } + + return NextResponse.json({ shot }); +} + +export async function POST( + request: Request, + { params }: { params: { id: string; predictionId: string } } +) { + const projectId = params.id; + const predictionId = params.predictionId; + + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const project = await db.project.findFirstOrThrow({ + where: { id: projectId, userId: session.userId }, + }); + + let shot = await db.shot.findFirstOrThrow({ + where: { projectId: project.id, id: predictionId }, + }); + + if (shot.hdStatus !== "NO") { + return NextResponse.json( + { message: "4K already applied" }, + { status: 400 } + ); + } + + const { data } = await replicateClient.post( + `https://api.replicate.com/v1/predictions`, + { + input: { + image: shot.outputUrl, + upscale: 8, + face_upsample: true, + codeformer_fidelity: 1, + }, + version: process.env.REPLICATE_HD_VERSION_MODEL_ID, + } + ); + + shot = await db.shot.update({ + where: { id: shot.id }, + data: { hdStatus: "PENDING", hdPredictionId: data.id }, + }); + + return NextResponse.json({ shot }); +} diff --git a/src/app/api/projects/[id]/predictions/[predictionId]/route.ts b/src/app/api/projects/[id]/predictions/[predictionId]/route.ts new file mode 100644 index 0000000..ebcf3a6 --- /dev/null +++ b/src/app/api/projects/[id]/predictions/[predictionId]/route.ts @@ -0,0 +1,89 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import replicateClient from "@/core/clients/replicate"; +import db from "@/core/db"; +import { extractSeedFromLogs } from "@/core/utils/predictions"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; +import { getPlaiceholder } from "plaiceholder"; + +export async function GET( + request: Request, + { params }: { params: { id: string; predictionId: string } } +) { + const projectId = params.id as string; + const predictionId = params.predictionId as string; + + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const project = await db.project.findFirstOrThrow({ + where: { id: projectId, userId: session.userId }, + }); + + let shot = await db.shot.findFirstOrThrow({ + where: { projectId: project.id, id: predictionId }, + }); + + const { data: prediction } = await replicateClient.get( + `https://api.replicate.com/v1/predictions/${shot.replicateId}` + ); + + const outputUrl = prediction.output?.[0]; + let blurhash = null; + + if (outputUrl) { + const { base64 } = await getPlaiceholder(outputUrl, { size: 16 }); + blurhash = base64; + } + + const seedNumber = extractSeedFromLogs(prediction.logs); + + shot = await db.shot.update({ + where: { id: shot.id }, + data: { + status: prediction.status, + outputUrl: outputUrl || null, + blurhash, + seed: seedNumber || null, + }, + }); + + return NextResponse.json({ shot }); +} + +export async function PATCH( + request: Request, + { params }: { params: { id: string; predictionId: string } } +) { + const projectId = params.id as string; + const predictionId = params.predictionId as string; + + const body = await request.json(); + const { bookmarked } = body; + + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const project = await db.project.findFirstOrThrow({ + where: { id: projectId, userId: session.userId }, + }); + + let shot = await db.shot.findFirstOrThrow({ + where: { projectId: project.id, id: predictionId }, + }); + + shot = await db.shot.update({ + where: { id: shot.id }, + data: { + bookmarked: bookmarked || false, + }, + }); + + return NextResponse.json({ shot }); +} diff --git a/src/pages/api/projects/[id]/predictions/index.ts b/src/app/api/projects/[id]/predictions/route.ts similarity index 55% rename from src/pages/api/projects/[id]/predictions/index.ts rename to src/app/api/projects/[id]/predictions/route.ts index 7740baa..5a5adeb 100644 --- a/src/pages/api/projects/[id]/predictions/index.ts +++ b/src/app/api/projects/[id]/predictions/route.ts @@ -1,19 +1,22 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import replicateClient from "@/core/clients/replicate"; import db from "@/core/db"; import { replacePromptToken } from "@/core/utils/predictions"; -import { NextApiRequest, NextApiResponse } from "next"; -import { getSession } from "next-auth/react"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const prompt = req.body.prompt as string; - const seed = req.body.seed as number; - const image = req.body.image as string; +export async function POST( + request: Request, + { params }: { params: { id: string } } +) { + const body = await request.json(); + const { prompt, seed, image } = body; - const projectId = req.query.id as string; - const session = await getSession({ req }); + const projectId = params.id; + const session = await getServerSession(authOptions); if (!session?.user) { - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({}, { status: 401 }); } const project = await db.project.findFirstOrThrow({ @@ -21,7 +24,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }); if (project.credits < 1) { - return res.status(400).json({ message: "No credit" }); + return NextResponse.json({ message: "No credit" }, { status: 400 }); } const { data } = await replicateClient.post( @@ -29,7 +32,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { { input: { prompt: replacePromptToken(prompt, project), - negative_prompt: process.env.REPLICATE_NEGATIVE_PROMPT || "cropped face, cover face, cover visage, mutated hands", + negative_prompt: + process.env.REPLICATE_NEGATIVE_PROMPT || + "cropped face, cover face, cover visage, mutated hands", ...(image && { image }), ...(seed && { seed }), }, @@ -53,7 +58,5 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }, }); - return res.json({ shot }); -}; - -export default handler; + return NextResponse.json({ shot }); +} diff --git a/src/pages/api/projects/[id]/prompter.ts b/src/app/api/projects/[id]/prompter/route.ts similarity index 56% rename from src/pages/api/projects/[id]/prompter.ts rename to src/app/api/projects/[id]/prompter/route.ts index c9d62db..9c5ece9 100644 --- a/src/pages/api/projects/[id]/prompter.ts +++ b/src/app/api/projects/[id]/prompter/route.ts @@ -1,24 +1,32 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import openai from "@/core/clients/openai"; -import { NextApiRequest, NextApiResponse } from "next"; -import { getSession } from "next-auth/react"; import db from "@/core/db"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const projectId = req.query.id as string; - const session = await getSession({ req }); +export async function POST( + request: Request, + { params }: { params: { id: string } } +) { + const projectId = params.id; + const session = await getServerSession(authOptions); if (!session?.user) { - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({}, { status: 401 }); } let project = await db.project.findFirstOrThrow({ where: { id: projectId, userId: session.userId }, }); - const keyword = req.body.keyword as string; + const body = await request.json(); + const { keyword } = body; if (project.promptWizardCredits < 1) { - return res.status(400).json({ success: false, message: "no_credit" }); + return NextResponse.json( + { success: false, message: "no_credit" }, + { status: 400 } + ); } try { @@ -43,13 +51,11 @@ ${keyword}:`, }); } - res.status(200).json({ + return NextResponse.json({ prompt, promptWizardCredits: project.promptWizardCredits, }); } catch (e) { - res.status(400).json({ success: false }); + return NextResponse.json({ success: false }, { status: 400 }); } -}; - -export default handler; +} diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts new file mode 100644 index 0000000..fb9319d --- /dev/null +++ b/src/app/api/projects/[id]/route.ts @@ -0,0 +1,76 @@ +import s3Client from "@/core/clients/s3"; +import db from "@/core/db"; +import { DeleteObjectCommand } from "@aws-sdk/client-s3"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +export async function GET( + req: Request, + { params }: { params: { id: string } } +) { + const session = await getServerSession(authOptions); + const projectId = params.id; + + if (!session) { + return NextResponse.json({}, { status: 401 }); + } + + let modelStatus = "not_created"; + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const project = await db.project.findFirstOrThrow({ + where: { id: projectId, userId: session.userId }, + }); + + return NextResponse.json({ project, modelStatus }); +} + +export async function DELETE( + request: Request, + { params }: { params: { id: string } } +) { + const session = await getServerSession(authOptions); + const projectId = params.id; + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const project = await db.project.findFirstOrThrow({ + where: { id: projectId, userId: session.userId }, + }); + + const { imageUrls, id } = project; + + // Delete training image + for (const imageUrl of imageUrls) { + const key = imageUrl.split( + `https://${process.env.S3_UPLOAD_BUCKET}.s3.${process.env.S3_UPLOAD_REGION}.amazonaws.com/` + )[1]; + + await s3Client.send( + new DeleteObjectCommand({ + Bucket: process.env.S3_UPLOAD_BUCKET, + Key: key, + }) + ); + } + + // Delete zip + await s3Client.send( + new DeleteObjectCommand({ + Bucket: process.env.S3_UPLOAD_BUCKET, + Key: `${project.id}.zip`, + }) + ); + + // Delete shots and project + await db.shot.deleteMany({ where: { projectId: id } }); + await db.project.delete({ where: { id } }); + + return NextResponse.json({ success: true }); +} diff --git a/src/pages/api/projects/[id]/train.ts b/src/app/api/projects/[id]/train/route.ts similarity index 77% rename from src/pages/api/projects/[id]/train.ts rename to src/app/api/projects/[id]/train/route.ts index a7f3266..cb3f4f6 100644 --- a/src/pages/api/projects/[id]/train.ts +++ b/src/app/api/projects/[id]/train/route.ts @@ -1,15 +1,19 @@ -import db from "@/core/db"; -import { NextApiRequest, NextApiResponse } from "next"; -import { getSession } from "next-auth/react"; import replicateClient from "@/core/clients/replicate"; +import db from "@/core/db"; import { getRefinedInstanceClass } from "@/core/utils/predictions"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; +import { authOptions } from "../../../auth/[...nextauth]/route"; -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const projectId = req.query.id as string; - const session = await getSession({ req }); +export async function POST( + req: Request, + { params }: { params: { id: string } } +) { + const projectId = params.id; + const session = await getServerSession(authOptions); if (!session?.user) { - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({}, { status: 401 }); } let project = await db.project.findFirstOrThrow({ @@ -52,7 +56,5 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { data: { replicateModelId: replicateModelId, modelStatus: "processing" }, }); - return res.json({ project }); -}; - -export default handler; + return NextResponse.json({ project }); +} diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts new file mode 100644 index 0000000..04d60bf --- /dev/null +++ b/src/app/api/projects/route.ts @@ -0,0 +1,72 @@ +import replicateClient from "@/core/clients/replicate"; +import s3Client from "@/core/clients/s3"; +import db from "@/core/db"; +import { createZipFolder } from "@/core/utils/assets"; +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; +import { authOptions } from "../auth/[...nextauth]/route"; + +export async function GET() { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({}, { status: 401 }); + } + + const projects = await db.project.findMany({ + where: { userId: session.userId }, + include: { shots: { take: 10, orderBy: { createdAt: "desc" } } }, + orderBy: { createdAt: "desc" }, + }); + + for (const project of projects) { + if (project?.replicateModelId && project?.modelStatus !== "succeeded") { + const { data } = await replicateClient.get( + `/v1/trainings/${project.replicateModelId}` + ); + + await db.project.update({ + where: { id: project.id }, + data: { modelVersionId: data.version, modelStatus: data?.status }, + }); + } + } + + return NextResponse.json(projects); +} + +export async function POST(request: Request) { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({}, { status: 401 }); + } + + const body = await request.json(); + const { urls, studioName, instanceClass } = body; + + const project = await db.project.create({ + data: { + imageUrls: urls, + name: studioName, + userId: session.userId, + modelStatus: "not_created", + instanceClass: instanceClass || "person", + instanceName: process.env.NEXT_PUBLIC_REPLICATE_INSTANCE_TOKEN!, + credits: Number(process.env.NEXT_PUBLIC_STUDIO_SHOT_AMOUNT) || 50, + }, + }); + + const buffer = await createZipFolder(urls, project); + + await s3Client.send( + new PutObjectCommand({ + Bucket: process.env.S3_UPLOAD_BUCKET!, + Key: `${project.id}.zip`, + Body: buffer, + }) + ); + + return NextResponse.json(project); +} diff --git a/src/app/api/projects/shots/route.ts b/src/app/api/projects/shots/route.ts new file mode 100644 index 0000000..7413c9a --- /dev/null +++ b/src/app/api/projects/shots/route.ts @@ -0,0 +1,44 @@ +import db from "@/core/db"; +import { getServerSession } from "next-auth"; +import { NextResponse } from "next/server"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +export async function POST( + request: Request, + { params }: { params: { id: string } } +) { + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json({}, { status: 401 }); + } + + const projectId = params.id; + + const body = await request.json(); + const { take, skip } = body; + + const project = await db.project.findFirstOrThrow({ + where: { + id: projectId, + userId: session.userId, + modelStatus: "succeeded", + }, + include: { + _count: { + select: { shots: true }, + }, + shots: { + orderBy: { createdAt: "desc" }, + take: Number(take) || 10, + skip: Number(skip) || 0, + }, + }, + orderBy: { createdAt: "desc" }, + }); + + return NextResponse.json({ + shots: project.shots, + shotsCount: project._count.shots, + }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..293d090 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,62 @@ +import Providers from "@/components/Providers"; +import { getSession } from "@/lib/sessions"; +import { Metadata } from "next"; + +type Props = { + children: React.ReactNode; +}; + +const description = + "Generate AI avatars that perfectly capture your unique style. Write a prompt and let our Dreambooth and Stable diffusion technology do the rest."; +const image = "https://photoshot.app/og-cover.jpg"; + +export const metadata: Metadata = { + title: { + template: "%s | Photoshot", + default: "Generate Custom AI avatar", + }, + description, + twitter: { + card: "summary_large_image", + site: "@shinework", + creator: "@shinework", + title: { template: "%s | Photoshot", default: "Generate Custom AI avatar" }, + description, + images: [ + { + url: image, + width: 1200, + height: 630, + alt: "Photoshot", + }, + ], + }, + openGraph: { + title: { template: "%s | Photoshot", default: "Generate Custom AI avatar" }, + images: [ + description, + { + url: image, + width: 1200, + height: 630, + alt: "Photoshot", + }, + ], + }, +}; + +export default async function RootLayout({ children }: Props) { + const session = await getSession(); + + return ( + + + + + + + {children} + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..58f18d6 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,8 @@ +import HomePage from "@/components/pages/HomePage"; +export const dynamic = "force-dynamic"; + +const Home = async () => { + return ; +}; + +export default Home; diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..e1471f3 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,17 @@ +import { prompts } from "@/core/utils/prompts"; +import { MetadataRoute } from "next"; + +const routes = [ + "https://photoshot.app", + "https://photoshot.app/terms", + "https://photoshot.app/faq", + "https://photoshot.app/prompts", + "https://photoshot.app/how-it-works", + ...prompts.map( + ({ slug }) => `https://photoshot.app/prompts/dreambooth/${slug}` + ), +]; + +export default function sitemap(): MetadataRoute.Sitemap { + return routes.map((route) => ({ url: route })); +} diff --git a/src/pages/_app.tsx b/src/components/Providers.tsx similarity index 64% rename from src/pages/_app.tsx rename to src/components/Providers.tsx index fa424f1..c96e5bf 100644 --- a/src/pages/_app.tsx +++ b/src/components/Providers.tsx @@ -1,32 +1,35 @@ -import Header from "@/components/layout/Header"; +"use client"; + import { ChakraProvider, Flex } from "@chakra-ui/react"; +import { Analytics } from "@vercel/analytics/react"; import { Session } from "next-auth"; import { SessionProvider } from "next-auth/react"; -import { AppProps } from "next/app"; +import { Inter } from "next/font/google"; +import React from "react"; import { QueryClient, QueryClientProvider } from "react-query"; -import Footer from "@/components/layout/Footer"; -import { Inter } from "@next/font/google"; -import theme from "@/styles/theme"; -import { Analytics } from "@vercel/analytics/react"; -import DefaultHead from "@/components/layout/DefaultHead"; +import Footer from "./layout/Footer"; +import Header from "./layout/Header"; +import theme from "@/styles/theme"; import "react-medium-image-zoom/dist/styles.css"; const queryClient = new QueryClient(); export const inter = Inter({ subsets: ["latin"] }); -function App({ - Component, - pageProps: { session, ...pageProps }, -}: AppProps<{ session: Session }>) { +export default function Providers({ + children, + session, +}: { + children: React.ReactNode; + session: Session | null; +}) { return ( - -
- +
+ {children}