From 73f8acae859126d4d2ab5ba2db2859406299f66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20Akg=C3=BCl?= <s86116@bht-berlin.de> Date: Thu, 8 Jun 2023 01:14:11 +0200 Subject: [PATCH] ui fixes --- .../(gaming)/games/[gameid]/page.tsx | 7 +- app/(content)/(home)/home/page.tsx | 42 ++--- app/api/messages/route.ts | 51 +---- components/post-item.tsx | 20 +- components/post-messages.tsx | 107 ++++++----- components/ui/form.tsx | 177 ++++++++++++++++++ components/ui/textarea.tsx | 24 +++ lib/utils.ts | 9 + types/prisma-item.d.ts | 9 + 9 files changed, 314 insertions(+), 132 deletions(-) create mode 100644 components/ui/form.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 types/prisma-item.d.ts diff --git a/app/(content)/(gaming)/games/[gameid]/page.tsx b/app/(content)/(gaming)/games/[gameid]/page.tsx index 5a6d45a..20d751b 100644 --- a/app/(content)/(gaming)/games/[gameid]/page.tsx +++ b/app/(content)/(gaming)/games/[gameid]/page.tsx @@ -2,6 +2,7 @@ import { AspectRatio } from "@/components/ui/aspect-ratio"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { getGame } from "@/lib/igdb"; +import { formatDate } from "@/lib/utils"; import { IGame } from "@/types/igdb-types"; import Image from "next/image"; @@ -9,11 +10,7 @@ import Image from "next/image"; export default async function GameDetail({ params }: { params: { gameid: string } }) { const data: IGame[] = await getGame(parseInt(params.gameid)) - const date = new Date(data[0].first_release_date * 1000).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }) + const date = formatDate(data[0].first_release_date * 1000) const companies = data[0].involved_companies.map((company) => { if (company !== data[0].involved_companies[0]) { diff --git a/app/(content)/(home)/home/page.tsx b/app/(content)/(home)/home/page.tsx index d3989d4..02156d8 100644 --- a/app/(content)/(home)/home/page.tsx +++ b/app/(content)/(home)/home/page.tsx @@ -1,13 +1,7 @@ import PostItem from "@/components/post-item"; -import PostMessageForm from "@/components/post-messages"; +import { PostMessageForm } from "@/components/post-messages"; +import { Card } from "@/components/ui/card"; import { db } from "@/lib/db"; -import { Prisma } from "@prisma/client"; - - -type messageType = any // Prisma.PostUncheckedCreateInput -type messageItemProps = { - msg: messageType; -}; export default async function HomePage() { let messages = null @@ -22,27 +16,27 @@ export default async function HomePage() { Like: true }, }) - } catch (error) { - console.log("the database is not running, try: 'npx prisma migrate dev --name init' if you want to use the database") + throw new Error("the database is not running, check your .env file") } return ( - <div> - <h1>Home WIP</h1> - <p>This will be where all Posts show up.</p> - <p>Needs a reload after posting!!</p> + // <div className="main-content px-3"> + <div className="relative md:gap-10 lg:grid lg:grid-cols-[1fr_240px] px-3"> <PostMessageForm /> - {messages ? - <> - {messages.map((msg) => ( - <PostItem msg={msg} key={msg.id} /> - ))} - </> - : - <p>no messages / no database</p>} + <Card className="w-full h-full overflow-hidden p-6 md:p-12"> + {messages ? messages.map((msg) => ( + <PostItem msg={msg} key={msg.id} /> + )) + : + <p>There are no messages currently</p>} + </Card> + + {/* <div className="side-content"> */} + <div className="hidden lg:block flex-col"> + a + </div> </div> ) -} - +} \ No newline at end of file diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts index 6abf1de..2e6107b 100644 --- a/app/api/messages/route.ts +++ b/app/api/messages/route.ts @@ -1,64 +1,33 @@ -import { authOptions } from "@/lib/auth"; import { db } from "@/lib/db"; -import { Prisma } from "@prisma/client"; -import { getServerSession } from "next-auth/next"; +import { getCurrentUser } from "@/lib/session"; import { revalidatePath } from "next/cache"; import { NextRequest, NextResponse } from "next/server"; -type post = Prisma.PostUncheckedCreateInput - export async function POST(req: NextRequest) { - const session = await getServerSession(authOptions); + const user = await getCurrentUser(); - if (!session) { - return NextResponse.json({ status: 401 }); + if (!user) { + return NextResponse.json({ status: 401, message: 'Unauthorized' }); } - const userId = session.user.id - const data = await req.json() - - console.log("router data: " + data.content, "status:") + const userId = user.id; + const content = await req.json() + console.log(content); + console.log(userId); try { await db.post.create({ - /* data: data */ data: { - content: data.content, + content: content.gweet, userId: userId, - published: true } }) - console.log("created") const path = req.nextUrl.searchParams.get('path') || '/'; revalidatePath(path); return NextResponse.json({ status: 201, message: 'Message Created' }) } catch (error: any) { - console.log("fail" + error); - } - console.log("post") -} - -export async function GET(req: NextRequest, res: NextResponse) { - try { - const data = await req.json() - console.log("router data: " + data, "status:") - } catch (error) { - - } - - try { - const messages = await db.post.findMany({ - orderBy: { - createdAt: "desc" - } - }) - - return NextResponse.json({ status: 200, messages: messages }) - } catch (error) { - console.log("fail" + error); - // res.status(400) + return NextResponse.json({ status: 500, message: error.message }) } - console.log("get") } \ No newline at end of file diff --git a/components/post-item.tsx b/components/post-item.tsx index c348a94..0588a4b 100644 --- a/components/post-item.tsx +++ b/components/post-item.tsx @@ -1,12 +1,8 @@ +import { formatDate } from "@/lib/utils"; import CommentButton from "./comment-button"; import LikeButton from "./like-button"; -type messageType = any // Prisma.PostUncheckedCreateInput -type messageItemProps = { - msg: messageType; -}; - -export default function PostItem({ msg }: any) { +export default function PostItem({ msg }: { msg: any }) { if (!msg.id) { return <div></div>; } @@ -20,7 +16,7 @@ export default function PostItem({ msg }: any) { <div className="flex items-center"> <span className="font-bold mr-2">{msg.user.name}</span> <span className="text-gray-500 text-sm"> - {formatDate(new Date(msg.createdAt!))} + {formatDate(msg.createdAt)} </span> </div> <div className="text-gray-800">{msg.content}</div> @@ -36,12 +32,4 @@ export default function PostItem({ msg }: any) { </div> </div> ) -}; - -function formatDate(date: Date) { - return date.toLocaleDateString("en-US", { - day: "numeric", - month: "short", - year: "numeric" - }); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/components/post-messages.tsx b/components/post-messages.tsx index 85ee1cf..727aa06 100644 --- a/components/post-messages.tsx +++ b/components/post-messages.tsx @@ -1,62 +1,77 @@ "use client" -import { Post, Prisma } from "@prisma/client"; -import { useRouter } from "next/navigation"; -import { startTransition, useEffect, useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" -type messageType = Prisma.PostUncheckedCreateInput +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" -export default function PostMessageForm() { +import { Textarea } from "@/components/ui/textarea" +import { toast } from "@/components/ui/use-toast" - const [formData, setFormData] = useState<messageType>({ content: "" } as messageType); - const router = useRouter(); +const FormSchema = z.object({ + gweet: z + .string() + .min(1, { message: "Come on post something...", }) + .max(1000, { message: "Gweets cannot be more that 1000 characters.", }), +}) - async function postMessage(e: any) { - e.preventDefault() - console.log(formData) - const response = await fetch('http://localhost:3000/api/messages', { +export function PostMessageForm() { + const form = useForm<z.infer<typeof FormSchema>>({ + resolver: zodResolver(FormSchema), + }) + + async function onSubmit(data: z.infer<typeof FormSchema>) { + await fetch('/api/messages', { method: 'POST', - body: JSON.stringify(formData), + body: JSON.stringify(data), next: { tags: ['collection'] } }) - startTransition(() => { - // Refresh the current route and fetch new data from the server without - // losing client-side browser or React state. - router.refresh(); - }); - return await response.json() + toast({ + title: "Your gweet is being processed...", + description: ( + <pre className="mt-2 w-[340px] rounded-md bg-slate-600 p-4"> + <code className="text-white">{JSON.stringify(data, null, 2)}</code> + </pre> + ), + }) } - const characterCount = formData.content.length; - const isOverLimit = characterCount >= 1000; - - const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - const { value } = e.target; - setFormData({ ...formData, content: value }); - }; - return ( - <div> - <form onSubmit={postMessage}> - <textarea - placeholder="Write something..." - name="content" - value={formData.content} - onChange={handleInputChange} - className="w-full p-2 border border-gray-600 rounded-xl resize-none" - rows={5} - maxLength={1000} - ></textarea> - <div className="flex justify-end mt-2"> - <span className={`${isOverLimit ? "text-red-500" : "text-gray-500"} text-sm`}> - {characterCount}/{1000} - </span> - </div> - <button type="submit" className="mt-2 bg-gray-300 text-gray-800 px-4 py-2 rounded float-right"> - Post - </button> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> + <FormField + control={form.control} + name="gweet" + render={({ field }) => ( + <FormItem> + <FormLabel>Gweet</FormLabel> + <FormControl> + <Textarea + placeholder="What's on your mind?" + className="resize-none" + {...field} + /> + </FormControl> + <FormDescription> + Your gweets will be public, and everyone can see them. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + <Button type="submit">Submit</Button> </form> - </div> + </Form> ) } \ No newline at end of file diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..63b6a61 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,177 @@ +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import * as React from "react" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> +> = { + name: TName +} + +const FormFieldContext = React.createContext<FormFieldContextValue>( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> +>({ + ...props +}: ControllerProps<TFieldValues, TName>) => { + return ( + <FormFieldContext.Provider value={{ name: props.name }}> + <Controller {...props} /> + </FormFieldContext.Provider> + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within <FormField>") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext<FormItemContextValue>( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + <FormItemContext.Provider value={{ id }}> + <div ref={ref} className={cn("space-y-2", className)} {...props} /> + </FormItemContext.Provider> + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef<typeof LabelPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( + <Label + ref={ref} + className={cn(error && "text-destructive", className)} + htmlFor={formItemId} + {...props} + /> + ) +}) +FormLabel.displayName = "FormLabel" + +const FormControl = React.forwardRef< + React.ElementRef<typeof Slot>, + React.ComponentPropsWithoutRef<typeof Slot> +>(({ ...props }, ref) => { + const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + + return ( + <Slot + ref={ref} + id={formItemId} + aria-describedby={ + !error + ? `${formDescriptionId}` + : `${formDescriptionId} ${formMessageId}` + } + aria-invalid={!!error} + {...props} + /> + ) +}) +FormControl.displayName = "FormControl" + +const FormDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> +>(({ className, ...props }, ref) => { + const { formDescriptionId } = useFormField() + + return ( + <p + ref={ref} + id={formDescriptionId} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> + ) +}) +FormDescription.displayName = "FormDescription" + +const FormMessage = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> +>(({ className, children, ...props }, ref) => { + const { error, formMessageId } = useFormField() + const body = error ? String(error?.message) : children + + if (!body) { + return null + } + + return ( + <p + ref={ref} + id={formMessageId} + className={cn("text-sm font-medium text-destructive", className)} + {...props} + > + {body} + </p> + ) +}) +FormMessage.displayName = "FormMessage" + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +} + diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx new file mode 100644 index 0000000..4c5d6b5 --- /dev/null +++ b/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} + +const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( + ({ className, ...props }, ref) => { + return ( + <textarea + className={cn( + "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + className + )} + ref={ref} + {...props} + /> + ) + } +) +Textarea.displayName = "Textarea" + +export { Textarea } diff --git a/lib/utils.ts b/lib/utils.ts index 3d7f8f6..a4563b1 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -16,4 +16,13 @@ export function getImageURL(hashId: string, size: string): string { // calculates the offset for the query export function calculateOffset(page: number, limit: number): number { return (page - 1) * limit +} + +export function formatDate(data: number) { + const date = new Date(data) + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }) } \ No newline at end of file diff --git a/types/prisma-item.d.ts b/types/prisma-item.d.ts new file mode 100644 index 0000000..e281755 --- /dev/null +++ b/types/prisma-item.d.ts @@ -0,0 +1,9 @@ +import { Comment, Like, Post, User } from "@prisma/client"; + +export interface IPost { + user: Post & { + user: User; + Comment: Comment[]; + Like: Like[]; + } +} \ No newline at end of file -- GitLab