mirror of
https://github.com/SrIzan10/nextbooru.git
synced 2026-06-06 00:57:02 +00:00
feat: initial upload and image show stuff
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,3 +35,7 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
public/uploads
|
||||
dev/
|
||||
!dev/docker-compose.yml
|
||||
@@ -1,5 +1,6 @@
|
||||
services:
|
||||
psql:
|
||||
user: 1000:1000
|
||||
image: postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "docker compose --file dev/docker-compose.yml up -d && next dev",
|
||||
"dev": "docker compose --file dev/docker-compose.yml up -d && next dev --turbo",
|
||||
"setup": "docker compose --file dev/docker-compose.yml up -d && prisma migrate deploy",
|
||||
"build": "prisma generate && next build",
|
||||
"start": "next start",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.0",
|
||||
|
||||
14
prisma/migrations/20241226183003_add_post/migration.sql
Normal file
14
prisma/migrations/20241226183003_add_post/migration.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Post" (
|
||||
"id" TEXT NOT NULL,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"caption" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"tags" TEXT[],
|
||||
"authorId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -14,10 +14,11 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
hashed_password String
|
||||
sessions Session[]
|
||||
sessions Session[]
|
||||
posts Post[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
@@ -25,4 +26,14 @@ model Session {
|
||||
userId String
|
||||
expiresAt DateTime
|
||||
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||
}
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
imageUrl String
|
||||
caption String
|
||||
createdAt DateTime @default(now())
|
||||
tags String[]
|
||||
authorId String
|
||||
author User @relation(references: [id], fields: [authorId], onDelete: Cascade)
|
||||
}
|
||||
|
||||
46
src/app/(protected)/upload/page.tsx
Normal file
46
src/app/(protected)/upload/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { UniversalForm } from '@/components/app/UniversalForm/UniversalForm';
|
||||
import UploadFile from '@/components/app/UploadFile/UploadFile';
|
||||
import { create } from '@/lib/form/actions';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center">
|
||||
<div className="max-w-md w-full p-4">
|
||||
<UniversalForm
|
||||
fields={[
|
||||
{
|
||||
name: 'asdf',
|
||||
label: 'asdf',
|
||||
type: 'hidden',
|
||||
placeholder: 'asdf',
|
||||
customComponent: <UploadFile id="file" />,
|
||||
},
|
||||
{
|
||||
name: 'caption',
|
||||
label: 'Caption',
|
||||
type: 'text',
|
||||
placeholder: 'Caption',
|
||||
textArea: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: 'text',
|
||||
placeholder: 'Tags',
|
||||
description: 'Separate tags with commas',
|
||||
},
|
||||
]}
|
||||
schemaName="upload"
|
||||
action={create}
|
||||
submitText="Upload"
|
||||
submitClassname="w-full"
|
||||
onActionComplete={(res) => res.success && router.push(`/post/${res.id}`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +1,23 @@
|
||||
// https://v0.dev/r/DxCSk58T8pM
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import prisma from '@/lib/db';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Home() {
|
||||
export default async function Home() {
|
||||
const posts = await prisma.post.findMany({ take: 30 });
|
||||
return (
|
||||
<>
|
||||
<main className="flex-1">
|
||||
<section className="w-full py-12 md:py-24 lg:py-32 xl:py-48">
|
||||
<div className="container px-4 md:px-6">
|
||||
<div className="flex flex-col items-center space-y-4 text-center">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl/none">
|
||||
The modern tech stack for your next(.js) project
|
||||
</h1>
|
||||
<p className="mx-auto max-w-[700px] text-gray-500 md:text-xl dark:text-gray-400">
|
||||
stack is a comprehensive tech stack that includes everything you need to build and deploy your next
|
||||
web application.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-4">
|
||||
<Link href="https://github.com/SrIzan10/stack">
|
||||
<Button>Start right NOW!</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="w-full py-12 md:py-24 lg:py-32 bg-mantle" id="features">
|
||||
<div className="container px-4 md:px-6">
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-block rounded-lg bg-gray-100 px-3 py-1 text-sm dark:bg-gray-800">
|
||||
Key Features
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">
|
||||
Everything you need to build and deploy
|
||||
</h2>
|
||||
<p className="max-w-[900px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
|
||||
Stack is a comprehensive tech stack that includes everything you need to build and deploy your next
|
||||
web application.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<div className="flex flex-col justify-center items-center text-center space-y-4">
|
||||
<ul className="grid gap-6">
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Next.js</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Build server-rendered React applications with Next.js.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Prisma</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Access your database with Prisma, the best way to work with databases in Ethan's opinion
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Tailwind CSS</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Style your application with Tailwind CSS, the utility-first CSS framework.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">shadcn/ui</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
The customizability of the components makes it one of the best UI libraries out there.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Vercel</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Deploy your application to the cloud with Vercel, the serverless platform.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Lucia</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Manage authentication with Lucia auth, the best selfhosted authentication library.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
<div className='text-center pt-2'>
|
||||
<h1>This is nextbooru</h1>
|
||||
<p>The simplest and most modern booru software.</p>
|
||||
<p className='text-sm text-muted-foreground italic'>(very unstable and not feature complete!)</p>
|
||||
<div className="flex gap-4 p-4">
|
||||
{posts.map((post) => (
|
||||
<div key={post.id}>
|
||||
<Link href={`/post/${post.id}`}>
|
||||
<Image width={176} height={176} src={post.imageUrl} alt={''} className="aspect-square object-contain border-2 rounded-md border-dashed" />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
80
src/app/(public)/post/[id]/page.tsx
Normal file
80
src/app/(public)/post/[id]/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import prisma from '@/lib/db';
|
||||
import { Heart, Download, Flag, MessageSquare, Link, User, Calendar } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const post = await prisma.post.findUnique({ where: { id }, include: { author: true } });
|
||||
if (!post) {
|
||||
return <div>Post not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="mb-8">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative aspect-square w-full">
|
||||
<Image
|
||||
src={post.imageUrl}
|
||||
alt="Post image"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
className="rounded-t-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 flex justify-between items-center">
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="icon">
|
||||
<Heart className="h-4 w-4" />
|
||||
</Button>
|
||||
<a href={post.imageUrl} download>
|
||||
<Button variant="outline" size="icon">
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</a>
|
||||
<Button variant="outline" size="icon">
|
||||
<Flag className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Comments
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p>comments here</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-xl font-bold">Image information</h3>
|
||||
<div className="flex gap-4">
|
||||
<User className="h-6 w-6" />
|
||||
<span>Uploaded by {post.author.username}</span>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Calendar className="h-6 w-6" />
|
||||
<span>Uploaded on: {post.createdAt.toLocaleString()}</span>
|
||||
</div>
|
||||
<Separator className='my-6' />
|
||||
<div className="grid gap-4">
|
||||
<h3 className="text-xl font-bold">Tags</h3>
|
||||
<div className="flex gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<Badge key={tag} variant={'secondary'} className="select-none cursor-pointer">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* related images missing */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Path, PathValue, useForm } from 'react-hook-form';
|
||||
import { Path, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -19,11 +19,12 @@ import React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { accountSchema } from '@/lib/form/zod';
|
||||
import { accountSchema, uploadSchema } from '@/lib/form/zod';
|
||||
|
||||
export const schemaDb = [
|
||||
{ name: 'login', zod: accountSchema },
|
||||
{ name: 'register', zod: accountSchema },
|
||||
{ name: 'upload', zod: uploadSchema },
|
||||
] as const;
|
||||
|
||||
export function UniversalForm<T extends z.ZodType>({
|
||||
@@ -78,23 +79,28 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
name={field.name as Path<z.infer<T>>}
|
||||
render={({ field: formField }) => (
|
||||
<FormItem>
|
||||
{field.type !== 'hidden' && <FormLabel>{field.label}</FormLabel>}
|
||||
{field.type !== 'hidden' && !field.hiddenShowLabel && (
|
||||
<FormLabel>{field.label}</FormLabel>
|
||||
)}
|
||||
<FormControl>
|
||||
{field.textArea ? (
|
||||
<Textarea
|
||||
placeholder={field.placeholder}
|
||||
{...formField}
|
||||
value={formField.value ?? ''}
|
||||
rows={field.textAreaRows ?? 5}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type={field.type || 'text'}
|
||||
placeholder={field.placeholder}
|
||||
{...formField}
|
||||
value={formField.value ?? ''}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{field.textArea ? (
|
||||
<Textarea
|
||||
placeholder={field.placeholder}
|
||||
{...formField}
|
||||
value={formField.value ?? ''}
|
||||
rows={field.textAreaRows ?? 5}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type={field.type || 'text'}
|
||||
placeholder={field.placeholder}
|
||||
{...formField}
|
||||
value={formField.value ?? ''}
|
||||
/>
|
||||
)}
|
||||
{field.customComponent && field.customComponent}
|
||||
</div>
|
||||
</FormControl>
|
||||
{field.description && <FormDescription>{field.description}</FormDescription>}
|
||||
<FormMessage />
|
||||
@@ -102,11 +108,11 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div className={cn("flex gap-2 py-2", submitButtonDivClassname)}>
|
||||
<div className={cn('flex gap-2 py-2', submitButtonDivClassname)}>
|
||||
{otherSubmitButton}
|
||||
<SubmitButton buttonText={submitText} className={submitClassname} />
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export type FormFieldConfig = {
|
||||
name: string;
|
||||
label: string;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
customComponent?: React.ReactNode;
|
||||
hiddenShowLabel?: boolean;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
value?: string;
|
||||
@@ -17,7 +19,7 @@ export type UniversalFormProps<T extends z.ZodType> = {
|
||||
fields: FormFieldConfig[];
|
||||
schemaName: (typeof schemaDb)[number]['name'];
|
||||
action: (prev: any, formData: FormData) => void;
|
||||
onActionComplete?: (result: unknown) => void;
|
||||
onActionComplete?: (result: any) => void;
|
||||
defaultValues?: Partial<z.infer<T>>;
|
||||
submitText?: string;
|
||||
submitClassname?: string;
|
||||
|
||||
11
src/components/app/UploadFile/UploadFile.tsx
Normal file
11
src/components/app/UploadFile/UploadFile.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
export default function UploadFile({ id }: { id: string }) {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor={id}>Image</Label>
|
||||
<Input id={id} type="file" name={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
31
src/components/ui/separator.tsx
Normal file
31
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
41
src/lib/form/actions.ts
Normal file
41
src/lib/form/actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
'use server';
|
||||
|
||||
import { writeFile } from 'fs/promises';
|
||||
import zodVerify from '../zodVerify';
|
||||
import { uploadSchema } from './zod';
|
||||
import prisma from '../db';
|
||||
import { validateRequest } from '../auth';
|
||||
|
||||
export async function create(prev: any, formData: FormData) {
|
||||
const { user } = await validateRequest();
|
||||
if (!user) {
|
||||
return { success: false, message: 'not logged in' };
|
||||
}
|
||||
const zod = await zodVerify(uploadSchema, formData);
|
||||
if (!zod.success) {
|
||||
return zod;
|
||||
}
|
||||
|
||||
const file = zod.data.file as File;
|
||||
|
||||
const buffer = new Uint8Array(await file.arrayBuffer());
|
||||
const filename = `${Date.now()}-${file.name.replaceAll(' ', '_')}`;
|
||||
await writeFile(`./public/uploads/${filename}`, buffer).catch((e) => {
|
||||
console.log(e);
|
||||
return { success: false, message: 'writing file' };
|
||||
});
|
||||
|
||||
const dbCreate = await prisma.post.create({
|
||||
data: {
|
||||
caption: zod.data.caption,
|
||||
tags: zod.data.tags.split(',').map((tag: string) => tag.trim()),
|
||||
imageUrl: `/uploads/${filename}`,
|
||||
author: {
|
||||
connect: {
|
||||
id: user.id
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return { success: true, id: dbCreate.id };
|
||||
}
|
||||
@@ -1,6 +1,22 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
export const accountSchema = z.object({
|
||||
username: z.string().min(3, { message: 'Mininum 3 characters' }).max(31, { message: 'Maximum 31 characters' }).regex(/^[a-z0-9_-]+$/, { message: 'Only characters from a-z, 0-9, underscores and dashes' }),
|
||||
password: z.string().min(6, { message: 'Minimum 6 characters' }).max(255, { message: 'Maximum 255 characters' }),
|
||||
})
|
||||
username: z
|
||||
.string()
|
||||
.min(3, { message: 'Mininum 3 characters' })
|
||||
.max(31, { message: 'Maximum 31 characters' })
|
||||
.regex(/^[a-z0-9_-]+$/, { message: 'Only characters from a-z, 0-9, underscores and dashes' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, { message: 'Minimum 6 characters' })
|
||||
.max(255, { message: 'Maximum 255 characters' }),
|
||||
});
|
||||
|
||||
export const uploadSchema = z.object({
|
||||
file: z
|
||||
.any()
|
||||
.refine((file: File) => file?.name !== '', 'No file name found.')
|
||||
.refine((file) => file.size < 5000000, 'Max size is 5MB.'),
|
||||
caption: z.string().min(1),
|
||||
tags: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -1158,6 +1158,13 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-separator@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.1.tgz#dd60621553c858238d876be9b0702287424866d2"
|
||||
integrity sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
|
||||
"@radix-ui/react-slot@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
|
||||
|
||||
Reference in New Issue
Block a user