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

added regweets and media, but with error / bugs

parent f8577bff
No related branches found
No related tags found
1 merge request!30Rewrite.gweets
Pipeline #38662 failed
Showing
with 383 additions and 168 deletions
......@@ -6,6 +6,10 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000"
# Database for connecting to Prisma
DATABASE_URL="file:./dev.db"
# Database for connecting to Supabase for media
NEXT_PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
# URLs
TWITCH_AUTH_BASE_URL="https://id.twitch.tv/oauth2"
IGDB_BASE_URL="https://api.igdb.com/v4"
......
import { GweetDetails } from "@/components/gweets";
import { GweetHeader } from "@/components/layout";
export default async function TweetClientPage() {
export default async function GweetDetailPage() {
return (
<div>
<GweetHeader />
......
......@@ -26,7 +26,7 @@ export async function GET(request: Request, { params }: { params: { id: string }
id,
},
include: {
user: true,
author: true,
likes: {
include: {
user: {
......@@ -39,6 +39,42 @@ export async function GET(request: Request, { params }: { params: { id: string }
createdAt: "desc",
},
},
media: true,
regweets: {
include: {
user: {
include: {
followers: true,
},
},
},
orderBy: {
createdAt: "desc",
},
},
quote: {
include: {
author: true,
media: true,
},
},
allQuotes: {
include: {
likes: true,
regweets: true,
author: true,
quote: {
include: {
author: true,
},
},
},
orderBy: {
createdAt: "desc",
},
},
},
});
......
......@@ -3,7 +3,7 @@ import { z } from "zod";
import { db } from "@/lib/db";
// get likes
// get likes from user
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const user_id = searchParams.get("user_id") || undefined;
......@@ -31,8 +31,10 @@ export async function GET(request: Request) {
},
include: {
user: true,
author: true,
media: true,
likes: true,
regweets: true,
allComments: true,
},
});
......@@ -71,12 +73,6 @@ export async function POST(request: Request) {
}
try {
const gweet = await db.gweet.findUnique({
where: {
id: gweet_id,
},
});
const like = await db.like.findFirst({
where: {
gweetId: gweet_id,
......@@ -91,19 +87,6 @@ export async function POST(request: Request) {
},
});
if (gweet && gweet.likeCount > 0)
await db.gweet.update({
where: {
id: gweet_id,
},
data: {
likeCount: {
decrement: 1,
},
},
});
return NextResponse.json({ message: "Gweet unliked" });
} else {
await db.like.create({
......@@ -113,20 +96,6 @@ export async function POST(request: Request) {
},
});
if (gweet) {
await db.gweet.update({
where: {
id: gweet_id,
},
data: {
likeCount: {
increment: 1,
},
},
});
}
return NextResponse.json({ message: "Gweet liked" });
}
} catch (error: any) {
......@@ -135,4 +104,4 @@ export async function POST(request: Request) {
error: error.message,
});
}
}
}
\ No newline at end of file
import { NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
export async function POST(request: Request) {
const { gweet_id, user_id } = await request.json();
const regweetSchema = z
.object({
gweet_id: z.string().cuid(),
user_id: z.string().cuid(),
})
.strict();
const zod = regweetSchema.safeParse({ gweet_id, user_id });
if (!zod.success) {
return NextResponse.json(
{
message: "Invalid request body",
error: zod.error.formErrors,
}, { status: 400 },
);
}
try {
const regweet = await db.regweet.findFirst({
where: {
gweetId: gweet_id,
userId: user_id,
},
});
if (regweet) {
await db.regweet.delete({
where: {
id: regweet.id,
},
});
return NextResponse.json({ message: "Deleted gweet regweet" });
} else {
await db.regweet.create({
data: {
gweetId: gweet_id,
userId: user_id,
},
});
return NextResponse.json({ message: "Gweet regweeted" });
}
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
......@@ -35,14 +35,13 @@ export async function GET(request: Request) {
}),
...(type === "user_gweets" && {
userId: id,
authorId: id,
}),
...(type === "user_replies" && {
userId: id,
authorId: id,
NOT: {
replyToGweetId: null,
replyToUserId: null,
},
}),
......@@ -56,9 +55,20 @@ export async function GET(request: Request) {
},
include: {
user: true,
author: true,
likes: true,
media: true,
regweets: true,
quote: {
include: {
author: true,
media: true,
},
},
allComments: true,
allQuotes: true,
},
orderBy: {
......@@ -81,9 +91,9 @@ export async function POST(request: Request) {
const gweetSchema = z
.object({
content: z.string().min(1).max(280),
userId: z.string().cuid(),
replyToUserId: z.string().optional(),
authorId: z.string().cuid(),
replyToGweetId: z.string().cuid().optional(),
quoteGweetId: z.string().cuid().optional(),
})
.strict();
......
{
"style": "default",
"rsc": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
\ No newline at end of file
import { postHashtags, retrieveHashtagsFromGweet } from "@/components/trends";
import { postMedia } from "./post-media";
export const postGweet = async ({
content,
files,
userId,
replyToUserId,
replyToGweetId,
quoteGweetId,
}: {
content: string;
files: File[];
userId: string;
replyToUserId?: string | null;
replyToGweetId?: string | null;
quoteGweetId?: string | null;
}) => {
const gweet = {
content,
userId,
// if no replyToUserId, don't send it
...(replyToUserId && { replyToUserId }),
// if no replyToGweetId, don't send it
...(replyToGweetId && { replyToGweetId }),
...(quoteGweetId && { quoteGweetId }),
};
try {
......@@ -26,6 +27,10 @@ export const postGweet = async ({
body: JSON.stringify(gweet)
}).then((result) => result.json())
if (files.length > 0) {
await postMedia({ files, gweet_id: data.id });
}
const hashtags = retrieveHashtagsFromGweet(content);
if (hashtags) await postHashtags(hashtags);
......
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);
}
};
......@@ -5,10 +5,8 @@ import { useState } from "react";
import { CreateGweet } from "./create-gweet";
export const CreateGweetWrapper = ({
replyToUserId,
replyToGweetId,
}: {
replyToUserId: string | undefined;
replyToGweetId: string | null;
}) => {
const [isComment, setIsComment] = useState(true);
......@@ -16,7 +14,6 @@ export const CreateGweetWrapper = ({
return (
<div className="relative border-b border-border">
<CreateGweet
replyToUserId={replyToUserId}
replyToGweetId={replyToGweetId}
placeholder="Gweet your reply"
isComment={isComment}
......@@ -31,4 +28,4 @@ export const CreateGweetWrapper = ({
)}
</div>
);
};
};
\ No newline at end of file
......@@ -2,7 +2,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useSession } from "next-auth/react";
import { useState } from "react";
import Image from "next/image";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
......@@ -25,6 +26,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { Card } from "@/components/ui/card";
import { useCreateGweet } from "../hooks/use-create-gweet";
import { IChosenImages } from "../types";
const FormSchema = z.object({
gweet: z
......@@ -35,18 +37,20 @@ const FormSchema = z.object({
export const CreateGweet = ({
parent_gweet,
replyToUserId,
quoted_gweet,
replyToGweetId,
placeholder,
isComment = false,
}: {
parent_gweet?: IGweet | null;
replyToUserId?: string | null;
quoted_gweet?: IGweet | null;
replyToGweetId?: string | null;
placeholder?: string | null;
isComment?: boolean;
}) => {
const [isGweetLoading, setIsGweetLoading] = useState<boolean>(false);
const [chosenImages, setChosenImages] = useState<IChosenImages[]>([]);
const imageUploadRef = useRef<HTMLInputElement>(null);
const { data: session } = useSession();
const mutation = useCreateGweet();
......@@ -62,9 +66,10 @@ export const CreateGweet = ({
mutation.mutate({
content: data.gweet,
files: [],
userId: session?.user?.id,
replyToUserId,
replyToGweetId,
quoteGweetId: quoted_gweet?.id || null,
})
toast({
......@@ -75,6 +80,27 @@ export const CreateGweet = ({
form.setValue('gweet', '');
}
const chooseImage = async (
event: React.ChangeEvent<HTMLInputElement>,
setChosenImages: (images: IChosenImages[]) => void,
) => {
const file = event?.target?.files?.[0];
if (file) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
setChosenImages([
...chosenImages,
{
url: reader.result,
file: file,
},
]);
};
}
};
if (!session) return null;
return (
......@@ -83,7 +109,7 @@ export const CreateGweet = ({
{parent_gweet && (
<div className="grid place-items-center grid-rows-2 gap-1">
<UserAvatar
user={{ username: parent_gweet?.user?.username, image: parent_gweet?.user?.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>
......@@ -93,10 +119,10 @@ export const CreateGweet = ({
<>
<div className="flex gap-1">
<span className="text-secondary text-sm font-medium truncate hover:underline">
{parent_gweet?.user?.name}
{parent_gweet?.author?.name}
</span>
<span className="text-tertiary text-sm truncate">
@{parent_gweet?.user?.email?.split("@")[0]}
@{parent_gweet?.author?.email?.split("@")[0]}
</span>
<span className="text-tertiary">·</span>
</div>
......@@ -108,11 +134,11 @@ export const CreateGweet = ({
</>
)}
{replyToUserId && !isComment && (
{parent_gweet && !isComment && (
<div className={`${!parent_gweet ? 'ml-16' : ''} flex items-center gap-1 cursor-pointer`}>
<span className="text-tertiary truncate">Replying to</span>
<span className="text-primary truncate">
@{replyToUserId}
@{parent_gweet?.authorId}
</span>
</div>
)}
......@@ -140,7 +166,45 @@ export const CreateGweet = ({
disabled={isGweetLoading || !session.user}
{...field}
/>
<input
className="hidden"
type="file"
onChange={(e) => chooseImage(e, setChosenImages)}
ref={imageUploadRef}
/>
</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 ?
<FormDescription className="pt-3">
Your gweets will be public, and everyone can see them.
......@@ -152,6 +216,13 @@ export const CreateGweet = ({
</FormItem>
)}
/>
<Button
variant="ghost"
size="sm"
className="absolute bottom-3 right-3"
onClick={() => imageUploadRef.current?.click()}
disabled={isGweetLoading || !session.user}
/>
</Card>
<div className="flex justify-end">
......
......@@ -9,20 +9,24 @@ export const useCreateGweet = () => {
return useMutation(
({
content,
files,
userId,
replyToUserId,
replyToGweetId,
quoteGweetId,
}: {
content: string;
files: File[];
userId: string;
replyToUserId?: string | null;
replyToGweetId?: string | null;
quoteGweetId?: string | null;
}) => {
return postGweet({
content,
files,
userId,
replyToUserId,
replyToGweetId,
quoteGweetId,
});
},
{
......
export * from "./api/post-media";
export * from "./components/create-gweet";
export * from "./components/create-gweet-wrapper";
export * from "./types";
export interface Post {
id: string;
}
export interface IChosenImages {
url: string | ArrayBuffer | null;
file: File;
}
\ No newline at end of file
"use client"
// import { PrismaClient } from '@prisma/client';
import { useState } from 'react';
import { Button } from './ui/button';
// Muss in die API route
// const prisma = new PrismaClient();
// async function getFollower(userId: number, followerId: number) {
// const follower = await prisma.follows.findFirst({
// where: {
// followerId: followerId,
// followingId: userId,
// },
// });
// return follower;
// }
export default function FollowButton({ userId, followerId }: { userId: number; followerId: number }) {
const [isFollowing, setIsFollowing] = useState(false);
const handleFollow = async () => {
// const follower = await getFollower(userId, followerId);
// if (follower) {
// // User is already following, so unfollow
// await prisma.follows.delete({
// where: {
// followerId_followingId: {
// followerId: followerId,
// followingId: userId,
// },
// },
// });
// setIsFollowing(false);
// } else {
// // User is not following, so follow
// await prisma.follows.create({
// data: {
// followerId: followerId,
// followingId: userId,
// },
// });
// setIsFollowing(true);
// }
};
return (
<Button onClick={handleFollow}>
{isFollowing ? 'Unfollow' : 'Follow'}
</Button>
);
}
\ No newline at end of file
"use client"
// import { PrismaClient } from '@prisma/client';
import { useEffect, useState } from 'react';
// Muss in die API route
// const prisma = new PrismaClient();
interface Follower {
id: number;
name: string;
email: string | null;
}
export default function FollowersList({ userId }: { userId: number }) {
const [followers, setFollowers] = useState<Follower[]>([]);
useEffect(() => {
async function fetchFollowers() {
// const followersList = await prisma.follows.findMany({
// where: {
// followingId: userId,
// },
// include: {
// follower: true,
// },
// });
// const filteredFollowers = followersList.map((follow: any) => {
// const { id, name, email } = follow.follower;
// return { id, name: name ?? "", email };
// });
// setFollowers(filteredFollowers);
}
fetchFollowers();
}, [userId]);
return (
<ul>
{followers.map((follower) => (
<li key={follower.id}>{follower.name} ({follower.email})</li>
))}
</ul>
);
}
\ No newline at end of file
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
export const handleRegweet = async (gweetId: string, userId: string) => {
try {
const data = await fetch("/api/gweets/regweets", {
method: "POST",
body: JSON.stringify({
gweet_id: gweetId,
user_id: userId,
}),
}).then((result) => result.json());
return data;
} catch (error: any) {
return error.response.data;
}
};
\ No newline at end of file
......@@ -19,7 +19,7 @@ export const LikeButton = ({
);
const mutation = useLike({
gweetAuthorId: gweet?.user?.id,
gweetAuthorId: gweet?.author?.id,
sessionOwnerId: session?.user?.id,
});
......
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { useSession } from "next-auth/react";
import { useRegweet } from "../../hooks/use-regweet";
import { IGweet } from "../../types";
export const RegweetButton = ({ gweet }: { gweet: IGweet }) => {
const { data: session } = useSession();
const hasRegweeted = gweet?.regweets?.some(
(regweet) => regweet?.userId === session?.user?.id,
);
const mutation = useRegweet();
return (
<Button
onClick={(e) => {
e.stopPropagation();
if (!session) {
return; // TODO: show login modal
}
mutation.mutate({
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 fill-green-600 text-green-600" />
: <Icons.regweet className="h-5 w-5" />
}
<span className="pl-2">
{gweet && gweet?.regweets?.length > 0 && (
<span className="">{gweet?.regweets?.length}</span>
)}
</span>
</Button>
);
};
\ No newline at end of file
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