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

image upload and ui fixes and other fixes

parent f212cfcb
No related branches found
No related tags found
1 merge request!30Rewrite.gweets
Showing
with 169 additions and 177 deletions
import { CreateGweet } from "@/components/create-gweet"; import { CreateGweet } from "@/components/create-gweet/components/create-gweet";
import { Gweets } from "@/components/gweets"; import { Gweets } from "@/components/gweets/components/gweets";
export default async function HomePage() { export default async function HomePage() {
return ( return (
......
import { GweetDetails } from "@/components/gweets"; import { GweetDetails } from "@/components/gweets/components/gweet-details";
import { GweetHeader } from "@/components/layout"; import { GweetHeader } from "@/components/layout";
export default async function GweetDetailPage() { export default async function GweetDetailPage() {
......
...@@ -58,7 +58,6 @@ export async function GET(request: Request, { params }: { params: { id: string } ...@@ -58,7 +58,6 @@ export async function GET(request: Request, { params }: { params: { id: string }
media: true, media: true,
}, },
}, },
allQuotes: { allQuotes: {
include: { include: {
likes: true, likes: true,
...@@ -75,6 +74,7 @@ export async function GET(request: Request, { params }: { params: { id: string } ...@@ -75,6 +74,7 @@ export async function GET(request: Request, { params }: { params: { id: string }
createdAt: "desc", createdAt: "desc",
}, },
}, },
allComments: true,
}, },
}); });
......
...@@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; ...@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { z } from "zod"; import { z } from "zod";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { utapi } from "uploadthing/server";
// get gweets // get gweets
export async function GET(request: Request) { export async function GET(request: Request) {
...@@ -87,7 +88,7 @@ export async function GET(request: Request) { ...@@ -87,7 +88,7 @@ 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 = 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),
...@@ -144,6 +145,16 @@ export async function DELETE(request: Request) { ...@@ -144,6 +145,16 @@ export async function DELETE(request: Request) {
} }
try { try {
const checkMedia = await db.media.findMany({
where: {
gweetId: id,
},
});
if (checkMedia.length > 0) {
await utapi.deleteFiles(checkMedia.map((media) => media.key));
}
await db.gweet.delete({ await db.gweet.delete({
where: { where: {
id, id,
......
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
import { getCurrentUser } from "@/lib/session";
import { createUploadthing, type FileRouter } from "uploadthing/next";
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: "4MB" } })
.middleware(async ({ req }) => {
const user = await getCurrentUser();
if (!user) throw new Error("Unauthorized");
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Upload complete for userId:", metadata.userId);
console.log("file url", file.url);
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
\ No newline at end of file
import { createNextRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
export const { GET, POST } = createNextRouteHandler({
router: ourFileRouter,
});
\ No newline at end of file
import { postHashtags, retrieveHashtagsFromGweet } from "@/components/trends"; import { postHashtags, retrieveHashtagsFromGweet } from "@/components/trends";
import { postMedia } from "./post-media"; import { uploadFiles } from "@/lib/uploadthing";
export const postGweet = async ({ export const postGweet = async ({
content, content,
files, files,
userId, authorId,
replyToGweetId, replyToGweetId,
quoteGweetId, quoteGweetId,
}: { }: {
content: string; content: string;
files: File[]; files: File[];
userId: string; authorId: string;
replyToGweetId?: string | null; replyToGweetId?: string | null;
quoteGweetId?: string | null; quoteGweetId?: string | null;
}) => { }) => {
const gweet = { const gweet = {
content, content,
userId, authorId,
...(replyToGweetId && { replyToGweetId }), ...(replyToGweetId && { replyToGweetId }),
...(quoteGweetId && { quoteGweetId }), ...(quoteGweetId && { quoteGweetId }),
}; };
...@@ -28,7 +28,22 @@ export const postGweet = async ({ ...@@ -28,7 +28,22 @@ export const postGweet = async ({
}).then((result) => result.json()) }).then((result) => result.json())
if (files.length > 0) { if (files.length > 0) {
await postMedia({ files, gweet_id: data.id }); 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);
......
import dbmedia from "@/lib/db-media";
import { createId } from '@paralleldrive/cuid2';
export const postMedia = async ({
files,
gweet_id,
}: {
files: File[];
gweet_id?: string;
}) => {
try {
files.forEach(async (file) => {
const imagePath = createId();
const { error } = await dbmedia.storage
.from("images")
.upload(`image-${imagePath}`, file);
if (error) {
console.log("error", error);
throw new Error("Failed to upload image");
} else {
const { data: mediaUrl } = dbmedia.storage
.from("images")
.getPublicUrl(`image-${imagePath}`);
const media = {
...(gweet_id && { gweet_id }),
url: mediaUrl?.publicUrl,
type: "image",
};
await fetch('/api/media', {
method: 'POST',
body: JSON.stringify(media)
})
}
});
return true;
} catch (error: any) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log("Error", error.message);
}
console.log(error.config);
}
};
...@@ -12,10 +12,10 @@ export const CreateGweetWrapper = ({ ...@@ -12,10 +12,10 @@ export const CreateGweetWrapper = ({
const [isComment, setIsComment] = useState(true); const [isComment, setIsComment] = useState(true);
return ( return (
<div className="relative border-b border-border"> <div className="">
<CreateGweet <CreateGweet
replyToGweetId={replyToGweetId} replyToGweetId={replyToGweetId}
placeholder="Gweet your reply" placeholder="Gweet your reply..."
isComment={isComment} isComment={isComment}
/> />
{isComment && ( {isComment && (
...@@ -23,8 +23,7 @@ export const CreateGweetWrapper = ({ ...@@ -23,8 +23,7 @@ export const CreateGweetWrapper = ({
onClick={() => { onClick={() => {
setIsComment(false); setIsComment(false);
}} }}
className="absolute top-0 right-0 w-full h-full z-1 pointer-events-auto" />
></button>
)} )}
</div> </div>
); );
......
...@@ -20,10 +20,10 @@ import { ...@@ -20,10 +20,10 @@ import {
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { IGweet } from "@/components/gweets"; import { IGweet } from "@/components/gweets/types";
import { Icons } from "@/components/icons";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { Icons } from "@/components/icons";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { useCreateGweet } from "../hooks/use-create-gweet"; import { useCreateGweet } from "../hooks/use-create-gweet";
import { IChosenImages } from "../types"; import { IChosenImages } from "../types";
...@@ -67,7 +67,7 @@ export const CreateGweet = ({ ...@@ -67,7 +67,7 @@ export const CreateGweet = ({
mutation.mutate({ mutation.mutate({
content: data.gweet, content: data.gweet,
files: [], files: [],
userId: session?.user?.id, authorId: session?.user?.id,
replyToGweetId, replyToGweetId,
quoteGweetId: quoted_gweet?.id || null, quoteGweetId: quoted_gweet?.id || null,
}) })
...@@ -166,45 +166,7 @@ export const CreateGweet = ({ ...@@ -166,45 +166,7 @@ export const CreateGweet = ({
disabled={isGweetLoading || !session.user} disabled={isGweetLoading || !session.user}
{...field} {...field}
/> />
<input
className="hidden"
type="file"
onChange={(e) => chooseImage(e, setChosenImages)}
ref={imageUploadRef}
/>
</FormControl> </FormControl>
<div className={`grid object-cover grid-cols-
${chosenImages.length === 1 ? "1"
: chosenImages.length === 2 ? "2 space-x-3"
: chosenImages.length === 3 ? "3 space-x-3 space-y-3"
: chosenImages.length === 4 ? "4 space-x-3 space-y-3"
: ""
}`}
>
{chosenImages.map((image, i) => {
return (
<div key={i} className="relative max-h-[700px] overflow-hidden">
<Button
size="sm"
onClick={() => {
setChosenImages(
chosenImages.filter((img, j) => j !== i),
);
}}
>
<Icons.close />
</Button>
<Image
src={image.url as string}
alt="gweet image"
width={1000}
height={1000}
/>
</div>
);
})}
{/* {quoted_gweet && <QuotedGweet gweet={quoted_gweet} />} */}
</div>
{!isComment ? {!isComment ?
<FormDescription className="pt-3"> <FormDescription className="pt-3">
Your gweets will be public, and everyone can see them. Your gweets will be public, and everyone can see them.
...@@ -212,6 +174,49 @@ export const CreateGweet = ({ ...@@ -212,6 +174,49 @@ export const CreateGweet = ({
: null : null
} }
<FormMessage /> <FormMessage />
<input
className="hidden w-full"
type="file"
onChange={(e) => chooseImage(e, setChosenImages)}
ref={imageUploadRef}
/>
{chosenImages.length > 0 && (
<div className={`grid object-cover h-[680px]
${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 (
<Card key={i} className={`relative max-h-[680px] overflow-hidden ${isFirstImage ? "row-span-2" : ""}`}>
<Button
size="icon"
variant="secondary"
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}
alt="gweet image"
fill
className="object-cover rounded-lg"
/>
</Card>
);
})}
</div>
)}
{/* {quoted_gweet && <QuotedGweet gweet={quoted_gweet} />} */}
</div> </div>
</FormItem> </FormItem>
)} )}
......
...@@ -10,21 +10,21 @@ export const useCreateGweet = () => { ...@@ -10,21 +10,21 @@ export const useCreateGweet = () => {
({ ({
content, content,
files, files,
userId, authorId,
replyToGweetId, replyToGweetId,
quoteGweetId, quoteGweetId,
}: { }: {
content: string; content: string;
files: File[]; files: File[];
userId: string; authorId: string;
replyToGweetId?: string | null; replyToGweetId?: string | null;
quoteGweetId?: string | null; quoteGweetId?: string | null;
}) => { }) => {
return postGweet({ return postGweet({
content, content,
files, files,
userId, authorId,
replyToGweetId, replyToGweetId,
quoteGweetId, quoteGweetId,
}); });
......
export * from "./api/post-media";
export * from "./components/create-gweet";
export * from "./components/create-gweet-wrapper";
export * from "./types";
import dbmedia from "@/lib/db-media";
export const deleteMedia = async (media: string[]) => {
try {
const { error } = await dbmedia.storage.from("images").remove(media);
if (error) {
throw new Error("Failed to delete media");
}
} catch (error: any) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log("Error", error.message);
}
console.log(error.config);
}
};
\ No newline at end of file
...@@ -5,17 +5,15 @@ import { Icons } from "@/components/icons"; ...@@ -5,17 +5,15 @@ import { Icons } from "@/components/icons";
export const CommentButton = ({ export const CommentButton = ({
gweet, gweet,
showStats = false,
}: { }: {
gweet: IGweet; gweet: IGweet;
showStats: boolean;
}) => { }) => {
return ( return (
<Button variant="ghost" size="lg" className="px-6 py-3 hover:bg-sky-800"> <Button variant="ghost" size="lg" className="px-6 py-3 hover:bg-sky-800">
<Icons.messagecircle className="h-5 w-5" /> <Icons.messagecircle className="h-5 w-5" />
<span className="pl-2"> <span className="pl-2">
{showStats && gweet?.comments?.length > 0 && ( {gweet?.allComments?.length > 0 && (
<span className="">{gweet?.comments?.length}</span> <span className="">{gweet?.allComments?.length}</span>
)} )}
</span> </span>
</Button> </Button>
......
...@@ -7,11 +7,9 @@ import { IGweet } from "../../types"; ...@@ -7,11 +7,9 @@ import { IGweet } from "../../types";
export const LikeButton = ({ export const LikeButton = ({
gweet, gweet,
showStats = false,
}: { }: {
gweet?: IGweet; gweet?: IGweet;
smallIcons?: boolean; smallIcons?: boolean;
showStats?: boolean;
}) => { }) => {
const { data: session } = useSession(); const { data: session } = useSession();
const hasLiked = gweet?.likes?.some( const hasLiked = gweet?.likes?.some(
...@@ -40,7 +38,7 @@ export const LikeButton = ({ ...@@ -40,7 +38,7 @@ export const LikeButton = ({
} }
<span className="pl-2"> <span className="pl-2">
{showStats && gweet?.likes && gweet?.likes?.length > 0 && ( {gweet?.likes && gweet?.likes?.length > 0 && (
<span className="">{gweet?.likes?.length}</span> <span className="">{gweet?.likes?.length}</span>
)} )}
</span> </span>
......
...@@ -27,7 +27,7 @@ export const RegweetButton = ({ gweet }: { gweet: IGweet }) => { ...@@ -27,7 +27,7 @@ export const RegweetButton = ({ gweet }: { gweet: IGweet }) => {
variant="ghost" size="lg" className="px-6 py-3 hover:bg-green-800" variant="ghost" size="lg" className="px-6 py-3 hover:bg-green-800"
> >
{hasRegweeted ? {hasRegweeted ?
<Icons.regweet className="h-5 w-5 fill-green-600 text-green-600" /> <Icons.regweet className="h-5 w-5 text-green-600" />
: <Icons.regweet className="h-5 w-5" /> : <Icons.regweet className="h-5 w-5" />
} }
......
...@@ -29,14 +29,12 @@ export const Comments = ({ gweetId }: { gweetId: string }) => { ...@@ -29,14 +29,12 @@ export const Comments = ({ gweetId }: { gweetId: string }) => {
} }
return ( return (
<div className=""> <InfiniteGweets
<InfiniteGweets gweets={comments}
gweets={comments} fetchNextPage={fetchNextPage}
fetchNextPage={fetchNextPage} hasNextPage={hasNextPage}
hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage}
isFetchingNextPage={isFetchingNextPage} isSuccess={isSuccess}
isSuccess={isSuccess} />
/>
</div>
); );
}; };
\ No newline at end of file
...@@ -6,16 +6,14 @@ import { RegweetButton } from "./actions/regweet-button"; ...@@ -6,16 +6,14 @@ import { RegweetButton } from "./actions/regweet-button";
export const GweetActions = ({ export const GweetActions = ({
gweet, gweet,
showStats = false,
}: { }: {
gweet: IGweet; gweet: IGweet;
showStats?: boolean | undefined;
}) => { }) => {
return ( return (
<div className="space-x-2"> <div className="space-x-2">
<CommentButton gweet={gweet} showStats={showStats} /> <CommentButton gweet={gweet} />
<RegweetButton gweet={gweet} /> <RegweetButton gweet={gweet} />
<LikeButton gweet={gweet} smallIcons={false} showStats={showStats} /> <LikeButton gweet={gweet} smallIcons={false} />
</div> </div>
); );
}; };
\ No newline at end of file
import { GweetOptions, IGweet } from "@/components/gweets";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { IGweet } from "../types";
import { GweetOptions } from "./gweet-options";
export const GweetAuthor = ({ gweet }: { gweet: IGweet }) => { export const GweetAuthor = ({ gweet }: { gweet: IGweet }) => {
return ( return (
...@@ -9,8 +10,8 @@ export const GweetAuthor = ({ gweet }: { gweet: IGweet }) => { ...@@ -9,8 +10,8 @@ export const GweetAuthor = ({ gweet }: { gweet: IGweet }) => {
className="h-10 w-10" className="h-10 w-10"
/> />
<div> <div className="flex flex-col ml-3">
<span className="font-bold mr-2">{gweet.author.name}</span> <span className="font-bold">{gweet.author.name}</span>
<span className="text-sky-500 text-sm">@{gweet.author.username}</span> <span className="text-sky-500 text-sm">@{gweet.author.username}</span>
</div> </div>
......
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