Skip to content
Snippets Groups Projects
Commit f5ccf2cb authored by Yusuf Akgül's avatar Yusuf Akgül :hatching_chick:
Browse files

fix image upload, cleanup ui

parent 8b3059a1
No related branches found
No related tags found
1 merge request!30Rewrite.gweets
Showing
with 370 additions and 315 deletions
...@@ -87,8 +87,8 @@ export async function GET(request: Request) { ...@@ -87,8 +87,8 @@ export async function GET(request: Request) {
// create gweet // create gweet
export async function POST(request: Request) { export async function POST(request: Request) {
const gweet = await request.json(); const { gweet, fileprops } = await request.json();
console.log(gweet)
const gweetSchema = z const gweetSchema = z
.object({ .object({
content: z.string().min(1).max(280), content: z.string().min(1).max(280),
...@@ -98,13 +98,22 @@ export async function POST(request: Request) { ...@@ -98,13 +98,22 @@ export async function POST(request: Request) {
}) })
.strict(); .strict();
const zod = gweetSchema.safeParse(gweet); const zodGweet = gweetSchema.safeParse(gweet);
if (!zod.success) { const mediaSchema = z.array(
z.object({
gweetId: z.string().nullable().optional(),
url: z.string(),
key: z.string(),
type: z.string(),
}).strict()
);
if (!zodGweet.success) {
return NextResponse.json( return NextResponse.json(
{ {
message: "Invalid request body", message: "Invalid request body",
error: zod.error.formErrors, error: zodGweet.error.formErrors,
}, { status: 400 }, }, { status: 400 },
); );
} }
...@@ -116,8 +125,37 @@ export async function POST(request: Request) { ...@@ -116,8 +125,37 @@ export async function POST(request: Request) {
}, },
}); });
if (fileprops.length > 0) {
const mediaArray = fileprops.map((fileprop: { fileUrl: string; fileKey: string; }) => {
const media = {
gweetId: created_gweet.id,
url: fileprop.fileUrl,
key: fileprop.fileKey,
type: "IMAGE",
}
return media;
});
const zodMedia = mediaSchema.safeParse(mediaArray);
if (!zodMedia.success) {
return NextResponse.json(
{
message: "Invalid media body",
error: zodMedia.error.formErrors,
}, { status: 400 },
);
}
await db.media.createMany({
data: mediaArray,
});
}
return NextResponse.json(created_gweet, { status: 200 }); return NextResponse.json(created_gweet, { status: 200 });
} catch (error: any) { } catch (error: any) {
console.log(error);
return NextResponse.json( return NextResponse.json(
{ {
message: "Something went wrong", message: "Something went wrong",
......
import { NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
export async function POST(request: Request) {
const { media } = await request.json();
const mediaSchema = z
.object({
gweetId: z.string().nullable().optional(),
url: z.string(),
key: z.string(),
type: z.string(),
})
.strict();
const zod = mediaSchema.safeParse(media);
if (!zod.success) {
return NextResponse.json({ error: zod.error }, { status: 400 });
}
try {
await db.media.create({
data: {
...media,
},
});
return NextResponse.json({ message: "Media created successfully" }, { status: 200 });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
\ No newline at end of file
...@@ -4,7 +4,7 @@ import { createUploadthing, type FileRouter } from "uploadthing/next"; ...@@ -4,7 +4,7 @@ import { createUploadthing, type FileRouter } from "uploadthing/next";
const f = createUploadthing(); const f = createUploadthing();
export const ourFileRouter = { export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: "4MB" } }) imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 4 } })
.middleware(async ({ req }) => { .middleware(async ({ req }) => {
const user = await getCurrentUser(); const user = await getCurrentUser();
...@@ -12,11 +12,7 @@ export const ourFileRouter = { ...@@ -12,11 +12,7 @@ export const ourFileRouter = {
return { userId: user.id }; return { userId: user.id };
}) })
.onUploadComplete(async ({ metadata, file }) => { .onUploadComplete(async ({ metadata, file }) => { }),
console.log("Upload complete for userId:", metadata.userId);
console.log("file url", file.url);
}),
} satisfies FileRouter; } satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter; export type OurFileRouter = typeof ourFileRouter;
\ No newline at end of file
...@@ -22,30 +22,16 @@ export const postGweet = async ({ ...@@ -22,30 +22,16 @@ export const postGweet = async ({
}; };
try { try {
let fileprops: { fileUrl: string; fileKey: string; }[] = [];
if (files.length > 0) {
fileprops = await uploadFiles({ files, endpoint: 'imageUploader' })
}
const data = await fetch('/api/gweets', { const data = await fetch('/api/gweets', {
method: 'POST', method: 'POST',
body: JSON.stringify(gweet) body: JSON.stringify({ gweet, fileprops })
}).then((result) => result.json()) }).then((result) => result.json())
if (files.length > 0) {
const gweet_id = data.id;
files.forEach(async (file) => {
const [res] = await uploadFiles({ files: [file], endpoint: 'imageUploader' })
const media = {
...(gweet_id && { gweet_id }),
url: res.fileUrl,
key: res.fileKey,
type: "image",
};
await fetch('/api/media', {
method: 'POST',
body: JSON.stringify(media)
})
})
}
const hashtags = retrieveHashtagsFromGweet(content); const hashtags = retrieveHashtagsFromGweet(content);
if (hashtags) await postHashtags(hashtags); if (hashtags) await postHashtags(hashtags);
......
...@@ -12,7 +12,7 @@ export const CreateGweetWrapper = ({ ...@@ -12,7 +12,7 @@ export const CreateGweetWrapper = ({
const [isComment, setIsComment] = useState(true); const [isComment, setIsComment] = useState(true);
return ( return (
<div className=""> <div className="px-6">
<CreateGweet <CreateGweet
replyToGweetId={replyToGweetId} replyToGweetId={replyToGweetId}
placeholder="Gweet your reply..." placeholder="Gweet your reply..."
......
...@@ -31,8 +31,8 @@ import { IChosenImages } from "../types"; ...@@ -31,8 +31,8 @@ import { IChosenImages } from "../types";
const FormSchema = z.object({ const FormSchema = z.object({
gweet: z gweet: z
.string() .string()
.min(1, { message: "Come on post something...", }) .min(1, { message: "Come on post something..." })
.max(1000, { message: "Gweets cannot be more that 1000 characters.", }), .max(240, { message: "Gweets cannot be more that 240 characters." }),
}) })
export const CreateGweet = ({ export const CreateGweet = ({
...@@ -48,27 +48,24 @@ export const CreateGweet = ({ ...@@ -48,27 +48,24 @@ export const CreateGweet = ({
placeholder?: string | null; placeholder?: string | null;
isComment?: boolean; isComment?: boolean;
}) => { }) => {
const [isGweetLoading, setIsGweetLoading] = useState<boolean>(false);
const [chosenImages, setChosenImages] = useState<IChosenImages[]>([]); const [chosenImages, setChosenImages] = useState<IChosenImages[]>([]);
const imageUploadRef = useRef<HTMLInputElement>(null); const imageUploadRef = useRef<HTMLInputElement>(null);
const { data: session } = useSession(); const { data: session } = useSession();
const mutation = useCreateGweet(); const { isLoading, mutate, data } = useCreateGweet();
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
}) })
function onGweet(data: z.infer<typeof FormSchema>) { async function onGweet(formData: z.infer<typeof FormSchema>) {
if (!session) return null; if (!session) return null;
setIsGweetLoading(true); mutate({
content: formData.gweet,
mutation.mutate({
content: data.gweet,
files: [],
authorId: session?.user?.id, authorId: session?.user?.id,
replyToGweetId, replyToGweetId,
files: chosenImages.map((image) => image.file),
quoteGweetId: quoted_gweet?.id || null, quoteGweetId: quoted_gweet?.id || null,
}) })
...@@ -76,28 +73,44 @@ export const CreateGweet = ({ ...@@ -76,28 +73,44 @@ export const CreateGweet = ({
description: "Your gweet was send.", description: "Your gweet was send.",
}) })
setIsGweetLoading(false);
form.setValue('gweet', ''); form.setValue('gweet', '');
setChosenImages([]);
} }
const chooseImage = async ( const chooseImages = async (
event: React.ChangeEvent<HTMLInputElement>, event: React.ChangeEvent<HTMLInputElement>,
setChosenImages: (images: IChosenImages[]) => void, setChosenImages: (images: IChosenImages[]) => void,
) => { ) => {
const file = event?.target?.files?.[0]; const files = event.target.files;
if (file) { if (files && files.length > 0) {
const reader = new FileReader(); const newImages: IChosenImages[] = [];
reader.readAsDataURL(file); const totalSelectedImages = chosenImages.length + files.length;
reader.onload = () => {
setChosenImages([ if (totalSelectedImages > 4) {
...chosenImages, return toast({
{ variant: "destructive",
description: "You can only upload 4 images per gweet.",
})
}
for (let i = 0; i < files.length; i++) {
const filePath = files[i];
const reader = new FileReader();
reader.readAsDataURL(filePath);
reader.onload = () => {
newImages.push({
url: reader.result, url: reader.result,
file: file, file: filePath,
}, });
]);
}; if (newImages.length === files.length) {
setChosenImages([...chosenImages, ...newImages]);
}
};
}
} }
}; };
...@@ -105,17 +118,18 @@ export const CreateGweet = ({ ...@@ -105,17 +118,18 @@ export const CreateGweet = ({
return ( return (
<> <>
<div className="grid grid-cols-2 gap-11 p-4"> {/* TODO showing if is replying */}
{parent_gweet && ( {parent_gweet && (
<div className="grid grid-cols-2 gap-11 p-4">
<div className="grid place-items-center grid-rows-2 gap-1"> <div className="grid place-items-center grid-rows-2 gap-1">
<UserAvatar <UserAvatar
user={{ username: parent_gweet?.author?.username, image: parent_gweet?.author?.image || null }} user={{ username: parent_gweet?.author?.username, image: parent_gweet?.author?.image || null }}
/> />
<div className="bg-gray-300 h-full w-px"></div> <div className="bg-gray-300 h-full w-px"></div>
</div> </div>
)}
<div className="flex flex-col"> <div className="flex flex-col">
{parent_gweet && (
<> <>
<div className="flex gap-1"> <div className="flex gap-1">
<span className="text-secondary text-sm font-medium truncate hover:underline"> <span className="text-secondary text-sm font-medium truncate hover:underline">
...@@ -132,107 +146,117 @@ export const CreateGweet = ({ ...@@ -132,107 +146,117 @@ export const CreateGweet = ({
)} )}
</div> </div>
</> </>
)}
{!isComment && (
{parent_gweet && !isComment && ( <div className={`${!parent_gweet ? 'ml-16' : ''} flex items-center gap-1 cursor-pointer`}>
<div className={`${!parent_gweet ? 'ml-16' : ''} flex items-center gap-1 cursor-pointer`}> <span className="text-tertiary truncate">Replying to</span>
<span className="text-tertiary truncate">Replying to</span> <span className="text-primary truncate">
<span className="text-primary truncate"> @{parent_gweet?.authorId}
@{parent_gweet?.authorId} </span>
</span> </div>
</div> )}
)} </div>
</div> </div>
</div> )}
<div className="relative"> <div className="relative">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onGweet)} className="space-y-6"> <form onSubmit={form.handleSubmit(onGweet)} className="space-y-6">
<Card className="p-3"> <Card className="p-3 flex items-start relative">
<FormField <UserAvatar
control={form.control} className="mr-3"
name="gweet" user={{ username: session?.user.username || null, image: session?.user?.image || null }}
render={({ field }) => ( />
<FormItem className="flex items-start"> <Button
<UserAvatar type="button"
className="mr-3" variant="ghost"
user={{ username: session?.user.username || null, image: session?.user?.image || null }} size="icon"
/> className="absolute bottom-0 left-0 mb-3 ml-3"
<div className="flex-grow"> onClick={() => imageUploadRef.current?.click()}
disabled={isLoading || !session.user}
>
<Icons.media className="text-muted-foreground" />
</Button>
<div className="flex-grow">
<FormField
control={form.control}
name="gweet"
render={({ field }) => (
<FormItem>
<FormControl> <FormControl>
<Textarea <Textarea
placeholder={placeholder || "What's on your mind?"} placeholder={placeholder || "What's on your mind?"}
className="resize-none" className="resize-none min-h-[100px]"
disabled={isGweetLoading || !session.user} disabled={isLoading || !session.user}
{...field} {...field}
/> />
</FormControl> </FormControl>
{!isComment ? {!isComment ?
<FormDescription className="pt-3"> <FormDescription>
Your gweets will be public, and everyone can see them. Your gweets will be public, and everyone can see them.
</FormDescription> </FormDescription>
: null : null
} }
<FormMessage /> <FormMessage />
</FormItem>
)}
/>
<input <input
className="hidden w-full" className="hidden w-full resize-none"
type="file" type="file"
onChange={(e) => chooseImage(e, setChosenImages)} multiple
ref={imageUploadRef} onChange={(e) => chooseImages(e, setChosenImages)}
/> ref={imageUploadRef}
{chosenImages.length > 0 && ( disabled={isLoading || !session.user}
<div className={`grid object-cover h-[680px] />
${chosenImages.length === 1 ? "grid-cols-1" {chosenImages.length > 0 && (
: chosenImages.length === 2 ? "grid-cols-2 gap-3" <div className={`grid object-cover h-[600px] pt-2
: chosenImages.length === 3 || 4 ? "grid-cols-2 grid-rows-2 gap-3" ${chosenImages.length === 1 ? "grid-cols-1"
: "" : chosenImages.length === 2 ? "grid-cols-2 gap-3"
}`} : chosenImages.length === 3 || 4 ? "grid-cols-2 grid-rows-2 gap-3"
> : ""
{chosenImages.map((image, i) => { }`}
const isFirstImage = chosenImages.length === 3 && i === 0; >
return ( {chosenImages.map((image, i) => {
<Card key={i} className={`relative max-h-[680px] overflow-hidden ${isFirstImage ? "row-span-2" : ""}`}> const isFirstImage = chosenImages.length === 3 && i === 0;
<Button return (
size="icon" <Card key={i} className={`relative max-h-[600px] overflow-hidden ${isFirstImage ? "row-span-2" : ""}`}>
variant="secondary" <Button
className="rounded-full absolute top-1 right-1 z-50" type="button"
onClick={() => { size="icon"
setChosenImages( variant="secondary"
chosenImages.filter((img, j) => j !== i), className="rounded-full absolute top-1 right-1 z-50"
); onClick={() => {
}} setChosenImages(
> chosenImages.filter((img, j) => j !== i),
<Icons.close /> );
</Button> }}
<Image >
src={image.url as string} <Icons.close className="w-6 h-6" />
alt="gweet image" </Button>
fill <Image
className="object-cover rounded-lg" src={image.url as string}
/> alt="gweet image"
</Card> fill
); className="object-cover rounded-lg"
})} />
</div> </Card>
)} );
{/* {quoted_gweet && <QuotedGweet gweet={quoted_gweet} />} */} })}
</div> </div>
</FormItem>
)} )}
/> {/* {quoted_gweet && <QuotedGweet gweet={quoted_gweet} />} */}
<Button </div>
variant="ghost"
size="sm"
className="absolute bottom-3 right-3"
onClick={() => imageUploadRef.current?.click()}
disabled={isGweetLoading || !session.user}
/>
</Card> </Card>
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" size="lg" disabled={isGweetLoading || !session.user}> <Button type="submit" size="lg" className=" w-20" disabled={isLoading || !session.user}>
{isComment ? 'Reply' : 'Gweet'} {isLoading ? (
<Icons.spinner className="h-4 w-4 animate-spin" />
) : (
isComment ? 'Reply' : 'Gweet'
)}
</Button> </Button>
</div> </div>
</form> </form>
......
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { postGweet } from "../api/post-gweet"; import { postGweet } from "../api/post-gweet";
export const useCreateGweet = () => { export const useCreateGweet = () => {
...@@ -13,7 +12,6 @@ export const useCreateGweet = () => { ...@@ -13,7 +12,6 @@ export const useCreateGweet = () => {
authorId, authorId,
replyToGweetId, replyToGweetId,
quoteGweetId, quoteGweetId,
}: { }: {
content: string; content: string;
files: File[]; files: File[];
......
...@@ -3,19 +3,16 @@ import { IGweet } from "../../types"; ...@@ -3,19 +3,16 @@ import { IGweet } from "../../types";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";
export const CommentButton = ({ export const CommentButton = ({ gweet }: { gweet: IGweet }) => {
gweet,
}: {
gweet: IGweet;
}) => {
return ( return (
<Button variant="ghost" size="lg" className="px-6 py-3 hover:bg-sky-800"> <div className="relative inline-flex items-center">
<Icons.messagecircle className="h-5 w-5" /> <Button variant="ghost" size="icon" className="hover:bg-sky-800 z-50">
<span className="pl-2"> <Icons.messagecircle className="w-6 h-6" />
{gweet?.allComments?.length > 0 && ( </Button>
<span className="">{gweet?.allComments?.length}</span>
)} {gweet.allComments.length > 0 && (
</span> <span className="absolute pl-12">{gweet.allComments.length}</span>
</Button> )}
</div>
); );
}; };
\ No newline at end of file
...@@ -5,43 +5,37 @@ import { Button } from "@/components/ui/button"; ...@@ -5,43 +5,37 @@ import { Button } from "@/components/ui/button";
import { useLike } from "../../hooks/use-like"; import { useLike } from "../../hooks/use-like";
import { IGweet } from "../../types"; import { IGweet } from "../../types";
export const LikeButton = ({ export const LikeButton = ({ gweet }: { gweet: IGweet }) => {
gweet,
}: {
gweet?: IGweet;
smallIcons?: boolean;
}) => {
const { data: session } = useSession(); const { data: session } = useSession();
const hasLiked = gweet?.likes?.some( const hasLiked = gweet.likes.some(
(like) => like.userId === session?.user?.id, (like) => like.userId === session?.user.id,
); );
const mutation = useLike({ const mutation = useLike({
gweetAuthorId: gweet?.author?.id, gweetAuthorId: gweet.author.id,
sessionOwnerId: session?.user?.id, sessionOwnerId: session?.user.id,
}); });
return ( return (
<Button <div className="inline-flex items-center">
onClick={(e) => { <Button
e.stopPropagation(); onClick={(e) => {
if (!session) { e.stopPropagation();
return; if (session) {
mutation.mutate({ gweetId: gweet.id, userId: session.user.id });
}
}}
variant="ghost" size="icon" className="hover:bg-red-800"
>
{hasLiked ?
<Icons.heart className="w-6 h-6 fill-red-600 text-red-600" />
: <Icons.heart className="w-6 h-6" />
} }
mutation.mutate({ gweetId: gweet?.id, userId: session?.user?.id }); </Button>
}}
variant="ghost" size="lg" className="px-6 py-3 hover:bg-red-800"
>
{hasLiked ?
<Icons.heart className="h-5 w-5 fill-red-600 text-red-600" />
: <Icons.heart className="h-5 w-5" />
}
<span className="pl-2"> {gweet.likes.length > 0 && (
{gweet?.likes && gweet?.likes?.length > 0 && ( <span className="px-2">{gweet?.likes?.length}</span>
<span className="">{gweet?.likes?.length}</span> )}
)} </div>
</span>
</Button>
); );
}; };
\ No newline at end of file
...@@ -6,36 +6,35 @@ import { IGweet } from "../../types"; ...@@ -6,36 +6,35 @@ import { IGweet } from "../../types";
export const RegweetButton = ({ gweet }: { gweet: IGweet }) => { export const RegweetButton = ({ gweet }: { gweet: IGweet }) => {
const { data: session } = useSession(); const { data: session } = useSession();
const hasRegweeted = gweet?.regweets?.some( const hasRegweeted = gweet.regweets.some(
(regweet) => regweet?.userId === session?.user?.id, (regweet) => regweet.userId === session?.user?.id,
); );
const mutation = useRegweet(); const mutation = useRegweet();
return ( return (
<Button <div className="relative inline-flex items-center">
onClick={(e) => { <Button
e.stopPropagation(); onClick={(e) => {
if (!session) { e.stopPropagation();
return; // TODO: show login modal if (session) {
mutation.mutate({
gweetId: gweet.id,
userId: session.user.id,
});
}
}}
variant="ghost" size="icon" className="hover:bg-green-800 z-50"
>
{hasRegweeted ?
<Icons.regweet className="w-6 h-6 text-green-600" />
: <Icons.regweet className="w-6 h-6" />
} }
mutation.mutate({ </Button>
gweetId: gweet?.id,
userId: session?.user?.id,
});
}}
variant="ghost" size="lg" className="px-6 py-3 hover:bg-green-800"
>
{hasRegweeted ?
<Icons.regweet className="h-5 w-5 text-green-600" />
: <Icons.regweet className="h-5 w-5" />
}
<span className="pl-2"> {gweet.regweets.length > 0 && (
{gweet && gweet?.regweets?.length > 0 && ( <span className="absolute pl-12">{gweet.regweets.length}</span>
<span className="">{gweet?.regweets?.length}</span> )}
)} </div>
</span>
</Button>
); );
}; };
\ No newline at end of file
...@@ -4,6 +4,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; ...@@ -4,6 +4,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useDeleteGweet } from "../hooks/use-delete-gweet"; import { useDeleteGweet } from "../hooks/use-delete-gweet";
import { IGweet } from "../types"; import { IGweet } from "../types";
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
...@@ -14,10 +15,14 @@ import { ...@@ -14,10 +15,14 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useRouter } from "next/navigation";
export const DeleteGweetModal = ({ gweet, props, forwardedRef }: { gweet: IGweet, props: any, forwardedRef: any }) => { export const DeleteGweetModal = ({ gweet, props, forwardedRef }: { gweet: IGweet, props: any, forwardedRef: any }) => {
const { triggerChildren, onSelect, onOpenChange, ...itemProps } = props; const { triggerChildren, onSelect, onOpenChange, ...itemProps } = props;
const mutation = useDeleteGweet(); const { isLoading, mutate, isSuccess } = useDeleteGweet();
const router = useRouter();
if (isSuccess) router.push("/home");
return ( return (
<Dialog onOpenChange={onOpenChange}> <Dialog onOpenChange={onOpenChange}>
...@@ -41,12 +46,20 @@ export const DeleteGweetModal = ({ gweet, props, forwardedRef }: { gweet: IGweet ...@@ -41,12 +46,20 @@ export const DeleteGweetModal = ({ gweet, props, forwardedRef }: { gweet: IGweet
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button type="submit" <Button
variant="destructive"
type="submit"
disabled={isLoading}
onClick={() => { onClick={() => {
mutation.mutate({ mutate({
gweetId: gweet?.id, gweetId: gweet?.id,
}); });
}}>Delete</Button> }}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Delete
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
......
...@@ -10,10 +10,10 @@ export const GweetActions = ({ ...@@ -10,10 +10,10 @@ export const GweetActions = ({
gweet: IGweet; gweet: IGweet;
}) => { }) => {
return ( return (
<div className="space-x-2"> <div className="space-x-12 w-60">
<CommentButton gweet={gweet} /> <CommentButton gweet={gweet} />
<RegweetButton gweet={gweet} /> <RegweetButton gweet={gweet} />
<LikeButton gweet={gweet} smallIcons={false} /> <LikeButton gweet={gweet} />
</div> </div>
); );
}; };
\ No newline at end of file
...@@ -33,33 +33,34 @@ export const GweetDetails = () => { ...@@ -33,33 +33,34 @@ export const GweetDetails = () => {
} }
return ( return (
<Card className="w-full h-full mt-12 p-2 xl:p-4 "> <Card className="w-full h-full mt-12 p-3 xl:p-6 ">
<div className="flex flex-col space-y-2 p-3"> <div className="flex flex-col space-y-3 px-3 pt-3">
<GweetAuthor gweet={gweet} /> <GweetAuthor gweet={gweet} />
{/* TODO needs handling of all gweets above and under the gweet */} {/* TODO needs handling of all gweets above and under the gweet */}
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-3">
{gweet.content && <h1 className="mt-2">{gweet.content}</h1>} {gweet.content && <h1 className="break-words">{gweet.content}</h1>}
{gweet.media && gweet.media.length > 0 && ( {/* TODO make own component */}
<div className={`grid object-cover grid-cols- {gweet.media.length > 0 && (
${gweet.media.length === 1 ? "1" <div className={`grid object-cover h-[600px] ${gweet.media.length === 1 ? "grid-cols-1"
: gweet.media.length === 2 ? "2 space-x-3" : gweet.media.length === 2 ? "grid-cols-2 gap-3"
: gweet.media.length === 3 ? "3 space-x-3 space-y-3" : gweet.media.length === 3 || 4 ? "grid-cols-2 grid-rows-2 gap-3"
: gweet.media.length === 4 ? "4 space-x-3 space-y-3" : ""
: ""
}`} }`}
> >
{gweet.media.slice(0, 4).map((media) => { {gweet.media.map((image, i) => {
const isFirstImage = gweet.media.length === 3 && i === 0;
return ( return (
<Image <Card key={i} className={`relative max-h-[600px] overflow-hidden ${isFirstImage ? "row-span-2" : ""}`}>
key={media.id} <Image
src={media.url} src={image.url as string}
alt={"image-" + media.id} alt="gweet image"
width={1000} fill
height={1000} className="object-cover rounded-lg"
/> />
</Card>
); );
})} })}
</div> </div>
...@@ -69,22 +70,18 @@ export const GweetDetails = () => { ...@@ -69,22 +70,18 @@ export const GweetDetails = () => {
{/* {gweet?.quotedGweet && <QuotedGweet gweet={gweet?.quotedGweet} />} */} {/* {gweet?.quotedGweet && <QuotedGweet gweet={gweet?.quotedGweet} />} */}
</div> </div>
<div className="w-full"> <div className="flex flex-col items-center flex-wrap flex-grow sm:flex-row space-y-3 sm:space-y-0">
<div className="flex flex-col items-center flex-wrap md:flex-row space-y-3 md:space-y-0"> <div className="flex-grow text-muted-foreground">
<div className="flex-grow text-muted-foreground"> <GweetCreationDate date={gweet.createdAt} />
<GweetCreationDate date={gweet.createdAt} /> </div>
</div> <div className="sm:items-end">
<div className="md:items-end"> <GweetActions gweet={gweet} />
<GweetActions gweet={gweet} />
</div>
</div> </div>
</div> </div>
<CreateGweetWrapper replyToGweetId={gweet?.id} />
</div> </div>
<Separator className="h-1 mb-3" />
<CreateGweetWrapper replyToGweetId={gweet?.id} />
<Separator className=" h-1 mb-3" />
<Comments gweetId={gweet?.id} /> <Comments gweetId={gweet?.id} />
</Card> </Card>
); );
......
...@@ -40,13 +40,19 @@ export const GweetOptions = ({ gweet }: { gweet: IGweet }) => { ...@@ -40,13 +40,19 @@ export const GweetOptions = ({ gweet }: { gweet: IGweet }) => {
return ( return (
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" ref={dropdownTriggerRef}> <div className="h-5 flex items-center">
<Icons.moreOptions /> <Button variant="ghost" size="option" ref={dropdownTriggerRef}>
</Button> <Icons.moreOptions />
</Button>
</div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className="w-56 font-bold cursor-pointer" align="end"
className="font-bold cursor-pointer"
hidden={hasOpenDialog} hidden={hasOpenDialog}
onClick={(event) => {
event.stopPropagation();
}}
onCloseAutoFocus={(event) => { onCloseAutoFocus={(event) => {
if (focusRef.current) { if (focusRef.current) {
focusRef.current.focus(); focusRef.current.focus();
...@@ -60,7 +66,7 @@ export const GweetOptions = ({ gweet }: { gweet: IGweet }) => { ...@@ -60,7 +66,7 @@ export const GweetOptions = ({ gweet }: { gweet: IGweet }) => {
gweet={gweet} gweet={gweet}
props={{ props={{
triggerChildren: ( triggerChildren: (
<div className="text-red-600"> <div className="text-red-600 flex">
<Icons.trash className="mr-2 h-4 w-4 " /> <Icons.trash className="mr-2 h-4 w-4 " />
<span>Delete</span> <span>Delete</span>
</div> </div>
......
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { cn, formatTimeElapsed } from "@/lib/utils"; import { cn, formatTimeElapsed } from "@/lib/utils";
import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { IGweet } from "../types"; import { IGweet } from "../types";
import { GweetActions } from "./gweet-actions"; import { GweetActions } from "./gweet-actions";
import { GweetOptions } from "./gweet-options";
export const Gweet = ({ gweet }: { gweet: IGweet }) => { export const Gweet = ({ gweet }: { gweet: IGweet }) => {
const router = useRouter(); const router = useRouter();
...@@ -13,25 +15,10 @@ export const Gweet = ({ gweet }: { gweet: IGweet }) => { ...@@ -13,25 +15,10 @@ export const Gweet = ({ gweet }: { gweet: IGweet }) => {
<div <div
tabIndex={0} tabIndex={0}
onClick={() => router.push(`/${gweet.author.username}/status/${gweet.id}`)} onClick={() => router.push(`/${gweet.author.username}/status/${gweet.id}`)}
className={cn(buttonVariants({ variant: "ghost" }), "flex h-auto w-full text-left cursor-pointer")} className={cn(buttonVariants({ variant: "ghost" }), "flex flex-col flex-grow h-auto w-full text-left cursor-pointer items-start p-3 space-y-3")}
> >
<UserAvatar {/* TODO replyto */}
user={{ username: gweet.author.username, image: gweet.author.image }} {/* <div className="">
className="h-10 w-10"
/>
<div className="ml-4 flex flex-col flex-grow">
<div>
<div className="flex items-center">
<h1 className="font-bold mr-2">{gweet.author.name}</h1>
<h1 className="text-sky-500 text-sm">
@{gweet.author.username}
</h1>
<h1 className="text-gray-500 text-sm ml-auto">
{formatTimeElapsed(gweet.createdAt)}
</h1>
</div>
{/* <div className="">
{gweet?.replyToGweetId && ( {gweet?.replyToGweetId && (
<div className=""> <div className="">
<span className="">Replying to</span> <span className="">Replying to</span>
...@@ -47,15 +34,66 @@ export const Gweet = ({ gweet }: { gweet: IGweet }) => { ...@@ -47,15 +34,66 @@ export const Gweet = ({ gweet }: { gweet: IGweet }) => {
</div> </div>
)} )}
</div> */} </div> */}
<div className="flex flex-row h-auto w-full">
<UserAvatar
user={{ username: gweet.author.username, image: gweet.author.image }}
className="h-10 w-10"
/>
{gweet?.content && ( <div className="flex flex-col flex-grow space-y-3 ml-3 w-1">
<h1>{gweet.content}</h1> <div className="flex items-start">
)}
</div> <div className="flex space-x-2 flex-grow">
<div className="flex justify-end" > <h1 className="font-bold">{gweet.author.name}</h1>
<GweetActions gweet={gweet} /> <h1 className="text-sky-500 text-sm">
@{gweet.author.username}
</h1>
<span>·</span>
<h1 className="text-gray-500 text-sm">
{formatTimeElapsed(gweet.createdAt)}
</h1>
</div>
<div className="ml-auto">
<GweetOptions gweet={gweet} />
</div>
</div>
<div className="flex flex-col flex-grow space-y-3 w-full">
{gweet.content && <h1 className="break-words">{gweet.content}</h1>}
{gweet.media.length > 0 && (
<div className={`grid object-cover h-[600px] ${gweet.media.length === 1 ? "grid-cols-1"
: gweet.media.length === 2 ? "grid-cols-2 gap-3"
: gweet.media.length === 3 || 4 ? "grid-cols-2 grid-rows-2 gap-3"
: ""
}`}
>
{gweet.media.map((image, i) => {
const isFirstImage = gweet.media.length === 3 && i === 0;
return (
<Card key={i} className={`relative max-h-[600px] overflow-hidden ${isFirstImage ? "row-span-2" : ""}`}>
<Image
src={image.url as string}
alt="gweet image"
fill
className="object-cover rounded-lg"
/>
</Card>
);
})}
</div>
)}
{/* TODO */}
{/* {quoted_gweet && <QuotedGweet gweet={quoted_gweet} />} */}
</div>
</div> </div>
</div> </div>
<div className="flex justify-end flex-grow w-full" >
<GweetActions gweet={gweet} />
</div>
</div> </div>
); );
}; };
\ No newline at end of file
...@@ -28,7 +28,7 @@ export const Gweets = () => { ...@@ -28,7 +28,7 @@ export const Gweets = () => {
} }
return ( return (
<Card className="w-full h-full mt-12 p-2 xl:p-4 "> <Card className="w-full h-full mt-6 p-2 xl:p-4 ">
<InfiniteGweets <InfiniteGweets
gweets={gweets} gweets={gweets}
isSuccess={isSuccess} isSuccess={isSuccess}
......
...@@ -17,13 +17,15 @@ const buttonVariants = cva( ...@@ -17,13 +17,15 @@ const buttonVariants = cva(
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
hover: "hover:bg-accent hover:text-accent-foreground hover:shadow-md hover:ring-2 hover:ring-ring hover:ring-offset-2",
link: "underline-offset-4 hover:underline text-primary", link: "underline-offset-4 hover:underline text-primary",
}, },
size: { size: {
default: "h-10 py-2 px-4", default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md", sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-full", lg: "h-11 px-8 rounded-full",
icon: "h-10 w-10", icon: "h-10 w-10 rounded-full",
option: "h-8 w-8 rounded-full",
}, },
}, },
defaultVariants: { defaultVariants: {
......
import { env } from "@/env.mjs" import { env } from "@/env.mjs"
import { ClassValue, clsx } from "clsx" import { ClassValue, clsx } from "clsx"
import dayjs from "dayjs"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
// tailwindcss classnames generator from shadcn // tailwindcss classnames generator from shadcn
...@@ -28,15 +29,15 @@ export function formatDate(data: number) { ...@@ -28,15 +29,15 @@ export function formatDate(data: number) {
} }
export function formatTimeElapsed(createdAt: Date) { export function formatTimeElapsed(createdAt: Date) {
const now = new Date(); const now = dayjs();
const timeDiff = Math.abs(now.getTime() - new Date(createdAt).getTime()); // Difference in milliseconds const timeDiff = Math.abs(now.diff(dayjs(createdAt))); // Difference in milliseconds
const seconds = Math.floor(timeDiff / 1000); // Convert to seconds const seconds = Math.floor(timeDiff / 1000); // Convert to seconds
const minutes = Math.floor(seconds / 60); // Convert to minutes const minutes = Math.floor(seconds / 60); // Convert to minutes
const hours = Math.floor(minutes / 60); // Convert to hours const hours = Math.floor(minutes / 60); // Convert to hours
const days = Math.floor(hours / 24); // Convert to days const days = Math.floor(hours / 24); // Convert to days
if (days > 0) { if (days > 0) {
return new Date(createdAt).toLocaleDateString(); // Show the date if days have passed return dayjs(createdAt).format('L'); // Show the date if days have passed
} else if (hours > 0) { } else if (hours > 0) {
return hours + 'h'; // Show hours if hours have passed return hours + 'h'; // Show hours if hours have passed
} else if (minutes > 0) { } else if (minutes > 0) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment