Post Update
This commit is contained in:
@@ -2,33 +2,45 @@
|
|||||||
|
|
||||||
import { useDevice } from "@/lib/context/DeviceContext";
|
import { useDevice } from "@/lib/context/DeviceContext";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Post from "../../../lib/components/Post";
|
||||||
|
|
||||||
export default function PostsPage() {
|
export default function PostsPage() {
|
||||||
const [postText, setPostText] = useState("");
|
|
||||||
const [posts, setPosts] = useState<any[]>([]);
|
const [posts, setPosts] = useState<any[]>([]);
|
||||||
const [userReactions, setUserReactions] = useState<{
|
const [userReactions, setUserReactions] = useState<{
|
||||||
[index: number]: { liked: boolean; warned: boolean };
|
[index: number]: { liked: boolean; warned: boolean };
|
||||||
}>({});
|
}>({});
|
||||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||||
|
const [alertMessage, setAlertMessage] = useState<string | null>(null); // State for alert message
|
||||||
const { isAuthenticated, session } = useDevice();
|
const { isAuthenticated, session } = useDevice();
|
||||||
|
|
||||||
|
// Fetch posts on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated && session?.username) {
|
if (isAuthenticated && session?.id) {
|
||||||
fetch(`/api/user/${session.username}`).then((res) => res.json());
|
fetch(`/api/post`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.posts) {
|
||||||
|
setPosts(data.posts);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch posts:", data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error fetching posts:", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, session?.username]);
|
}, [isAuthenticated, session?.id]);
|
||||||
|
|
||||||
const handlePostSubmit = async (e: React.FormEvent) => {
|
const handlePostSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!imageFile) {
|
if (!imageFile) {
|
||||||
alert("Please select an image to upload.");
|
setAlertMessage("Please select an image to upload.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("image", imageFile);
|
formData.append("image", imageFile);
|
||||||
formData.append("text", postText);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/post", {
|
const response = await fetch("/api/post", {
|
||||||
@@ -38,51 +50,109 @@ export default function PostsPage() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert("Post uploaded successfully!");
|
setAlertMessage(`Post uploaded successfully! You earned ${data.points} points!`);
|
||||||
setPosts([data.postData, ...posts]);
|
setPosts([data.postData, ...posts]); // Add the new post to the list
|
||||||
setPostText("");
|
setImageFile(null); // Clear the file input
|
||||||
setImageFile(null);
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || "Failed to upload post.");
|
setAlertMessage(data.message || "Failed to upload post.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading post:", error);
|
console.error("Error uploading post:", error);
|
||||||
alert("An error occurred while uploading the post.");
|
setAlertMessage("An error occurred while uploading the post.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLike = (index: number) => {
|
const handleLike = async (index: number) => {
|
||||||
const updatedPosts = [...posts];
|
const post = posts[index];
|
||||||
const reactions = { ...userReactions };
|
const reactions = { ...userReactions };
|
||||||
|
|
||||||
const alreadyLiked = reactions[index]?.liked;
|
try {
|
||||||
|
const response = await fetch(`/api/post/${post.id}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({ like: "true" }),
|
||||||
|
});
|
||||||
|
|
||||||
updatedPosts[index].likes += alreadyLiked ? -1 : 1;
|
if (response.ok) {
|
||||||
|
const updatedPosts = [...posts];
|
||||||
|
const alreadyLiked = reactions[index]?.liked;
|
||||||
|
|
||||||
reactions[index] = {
|
// Update reactions in the post
|
||||||
...reactions[index],
|
updatedPosts[index].reactions.push({ liked: !alreadyLiked, warned: false });
|
||||||
liked: !alreadyLiked,
|
|
||||||
};
|
|
||||||
|
|
||||||
setPosts(updatedPosts);
|
// Update local state
|
||||||
setUserReactions(reactions);
|
reactions[index] = {
|
||||||
|
...reactions[index],
|
||||||
|
liked: !alreadyLiked,
|
||||||
|
};
|
||||||
|
|
||||||
|
setPosts(updatedPosts);
|
||||||
|
setUserReactions(reactions);
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.message || "Failed to like the post.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error liking post:", error);
|
||||||
|
alert("An error occurred while liking the post.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWarning = (index: number) => {
|
const handleWarning = async (index: number) => {
|
||||||
const updatedPosts = [...posts];
|
const post = posts[index];
|
||||||
const reactions = { ...userReactions };
|
const reactions = { ...userReactions };
|
||||||
|
|
||||||
const alreadyWarned = reactions[index]?.warned;
|
try {
|
||||||
|
const response = await fetch(`/api/post/${post.id}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({ warn: "true" }),
|
||||||
|
});
|
||||||
|
|
||||||
updatedPosts[index].warnings += alreadyWarned ? -1 : 1;
|
if (response.ok) {
|
||||||
|
const updatedPosts = [...posts];
|
||||||
|
const alreadyWarned = reactions[index]?.warned;
|
||||||
|
|
||||||
reactions[index] = {
|
// Update reactions in the post
|
||||||
...reactions[index],
|
updatedPosts[index].reactions.push({ liked: false, warned: !alreadyWarned });
|
||||||
warned: !alreadyWarned,
|
|
||||||
};
|
|
||||||
|
|
||||||
setPosts(updatedPosts);
|
// Update local state
|
||||||
setUserReactions(reactions);
|
reactions[index] = {
|
||||||
|
...reactions[index],
|
||||||
|
warned: !alreadyWarned,
|
||||||
|
};
|
||||||
|
|
||||||
|
setPosts(updatedPosts);
|
||||||
|
setUserReactions(reactions);
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.message || "Failed to warn the post.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error warning post:", error);
|
||||||
|
alert("An error occurred while warning the post.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (index: number) => {
|
||||||
|
const post = posts[index];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/post/${post.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Post deleted successfully!");
|
||||||
|
const updatedPosts = [...posts];
|
||||||
|
updatedPosts.splice(index, 1); // Remove the deleted post from the list
|
||||||
|
setPosts(updatedPosts);
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.message || "Failed to delete the post.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting post:", error);
|
||||||
|
alert("An error occurred while deleting the post.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -93,15 +163,15 @@ export default function PostsPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Alert Message */}
|
||||||
|
{alertMessage && (
|
||||||
|
<div className="bg-success-600 text-white px-4 py-3 rounded mb-6">
|
||||||
|
{alertMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-[color:var(--color-surface-600)]/70 backdrop-blur-md rounded-xl px-6 py-5 mb-8 shadow-sm">
|
<div className="bg-[color:var(--color-surface-600)]/70 backdrop-blur-md rounded-xl px-6 py-5 mb-8 shadow-sm">
|
||||||
<form onSubmit={handlePostSubmit} className="space-y-3">
|
<form onSubmit={handlePostSubmit} className="space-y-3">
|
||||||
<textarea
|
|
||||||
className="w-full p-3 rounded bg-neutral-800 text-white"
|
|
||||||
placeholder="Share your beverage..."
|
|
||||||
value={postText}
|
|
||||||
onChange={(e) => setPostText(e.target.value)}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -117,56 +187,27 @@ export default function PostsPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
{/* Post List Card */}
|
||||||
{posts.map((post, index) => (
|
<div className="bg-[color:var(--color-surface-800)] rounded-xl px-6 py-5 shadow-md">
|
||||||
<div
|
<h2 className="text-2xl font-bold text-[color:var(--color-warning-300)] mb-4">
|
||||||
key={index}
|
Post List
|
||||||
className="bg-[color:var(--color-surface-600)]/80 rounded-xl px-6 py-5 shadow-md"
|
</h2>
|
||||||
>
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4 mb-2">
|
{posts
|
||||||
<div className="w-10 h-10 rounded-full bg-neutral-800 border border-gray-300" />
|
.slice() // Create a shallow copy of the posts array
|
||||||
<div>
|
.sort((a, b) => new Date(b.timeStamp).getTime() - new Date(a.timeStamp).getTime()) // Sort by timeStamp in descending order
|
||||||
<p className="font-semibold text-[color:var(--color-warning-300)]">
|
.map((post, index) => (
|
||||||
{post.author || "Anonymous"}
|
<Post
|
||||||
</p>
|
allowReactions={false}
|
||||||
<p className="text-sm text-gray-400">{post.date}</p>
|
key={index}
|
||||||
</div>
|
post={post}
|
||||||
</div>
|
userReactions={userReactions[index] || { liked: false, warned: false }}
|
||||||
|
onLike={() => handleLike(index)}
|
||||||
{post.imageUrl && (
|
onWarning={() => handleWarning(index)}
|
||||||
<img
|
onDelete={() => handleDelete(index)} // Pass the delete handler
|
||||||
src={post.imageUrl}
|
|
||||||
alt="Post related"
|
|
||||||
className="w-full max-h-64 object-cover rounded mb-4"
|
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
|
</div>
|
||||||
<p className="text-base text-neutral-100 mb-4">{post.text}</p>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<button
|
|
||||||
onClick={() => handleLike(index)}
|
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
|
||||||
userReactions[index]?.liked
|
|
||||||
? "bg-success-800"
|
|
||||||
: "bg-success-600 hover:bg-primary-600"
|
|
||||||
} text-white`}
|
|
||||||
>
|
|
||||||
👍 Like ({post.likes})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleWarning(index)}
|
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
|
||||||
userReactions[index]?.warned
|
|
||||||
? "bg-red-800"
|
|
||||||
: "bg-primary-500 hover:bg-red-600"
|
|
||||||
} text-white`}
|
|
||||||
>
|
|
||||||
😭 Stop drinking that ({post.warnings})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-10" />
|
<div className="h-10" />
|
||||||
|
|||||||
@@ -34,3 +34,66 @@ export async function GET(req: Request, { params }: any) {
|
|||||||
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(req: Request, { params }: any) {
|
||||||
|
try {
|
||||||
|
if (!(await authenticateUser())) return;
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const post = await db.posts.getById(id);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return NextResponse.json({ message: "Post not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await req.formData();
|
||||||
|
|
||||||
|
let like = formData.get("like");
|
||||||
|
if(like) {
|
||||||
|
await db.posts.addReaction(id, {
|
||||||
|
liked: true,
|
||||||
|
warned: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Post liked successfully" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let warn = formData.get("warn");
|
||||||
|
if(warn) {
|
||||||
|
await db.posts.addReaction(id, {
|
||||||
|
liked: false,
|
||||||
|
warned: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Post warned successfully" }, { status: 200 });
|
||||||
|
}
|
||||||
|
return NextResponse.json({ message: "No action taken" }, { status: 400 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error finding post by ID:", error);
|
||||||
|
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(req: Request, { params }: any) {
|
||||||
|
try {
|
||||||
|
if (!(await authenticateUser())) return;
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const post = await db.posts.getById(id);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return NextResponse.json({ message: "Post not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.posts.delete(id);
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Post deleted successfully" }, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting post:", error);
|
||||||
|
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,43 +19,135 @@ async function authenticateUser() {
|
|||||||
return userData;
|
return userData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
/*
|
||||||
try {
|
Points Guide
|
||||||
|
Learn how many points you receive for each drink!
|
||||||
|
|
||||||
|
Game Points System:
|
||||||
|
+150 points for drinking ≥100 oz of water
|
||||||
|
+100 points for keeping caffeine < 200 mg
|
||||||
|
+150 points for staying under the sugar cap all day
|
||||||
|
Exceeding 400mg caffeine limit or 30.5g sugar limit = 0 pts logged for those drinks
|
||||||
|
Beverage Scoring System
|
||||||
|
Drink Volume (oz) Caffeine (mg) Sugar (g) Points Earned Bonus
|
||||||
|
Water 8 n/a n/a 100 +15 for >=64oz in day
|
||||||
|
Coffee 8 95 ? 50 0 pts after 400mg caffeine
|
||||||
|
Tea 8 55 ? 50 0 pts after 400mg caffeine
|
||||||
|
Coca-Cola 8 34 39 0 Exceeds sugar
|
||||||
|
100% Fruit Juice 8 0 22 50 0 pts after 30.5g sugar
|
||||||
|
Dairy Milk (low-fat) 8 0 12 50 +0.5 calcium bonus
|
||||||
|
*/
|
||||||
|
|
||||||
|
function calculatePoints(description: string): number {
|
||||||
|
let points = 0;
|
||||||
|
|
||||||
|
// Normalize the description to lowercase for easier matching
|
||||||
|
const normalizedDescription = description.toLowerCase();
|
||||||
|
|
||||||
|
// Points calculation based on the description
|
||||||
|
if (normalizedDescription.includes("water")) {
|
||||||
|
points += 100; // Base points for water
|
||||||
|
if (normalizedDescription.includes("≥64oz") || normalizedDescription.includes("64oz or more")) {
|
||||||
|
points += 15; // Bonus for drinking ≥64oz of water
|
||||||
|
}
|
||||||
|
} else if (normalizedDescription.includes("coffee")) {
|
||||||
|
points += 50; // Base points for coffee
|
||||||
|
if (normalizedDescription.includes("≥400mg caffeine") || normalizedDescription.includes("exceeds caffeine")) {
|
||||||
|
points = 0; // No points if caffeine exceeds 400mg
|
||||||
|
}
|
||||||
|
} else if (normalizedDescription.includes("tea")) {
|
||||||
|
points += 50; // Base points for tea
|
||||||
|
if (normalizedDescription.includes("≥400mg caffeine") || normalizedDescription.includes("exceeds caffeine")) {
|
||||||
|
points = 0; // No points if caffeine exceeds 400mg
|
||||||
|
}
|
||||||
|
} else if (normalizedDescription.includes("coca-cola")) {
|
||||||
|
points = 0; // No points for Coca-Cola due to sugar
|
||||||
|
} else if (normalizedDescription.includes("fruit juice")) {
|
||||||
|
points += 50; // Base points for fruit juice
|
||||||
|
if (normalizedDescription.includes("≥30.5g sugar") || normalizedDescription.includes("exceeds sugar")) {
|
||||||
|
points = 0; // No points if sugar exceeds 30.5g
|
||||||
|
}
|
||||||
|
} else if (normalizedDescription.includes("milk")) {
|
||||||
|
points += 50; // Base points for milk
|
||||||
|
if (normalizedDescription.includes("low-fat")) {
|
||||||
|
points += 0.5; // Bonus for low-fat milk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
let userData = await authenticateUser();
|
||||||
|
if (!userData) return NextResponse.json({ message: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const formData = await req.formData();
|
||||||
|
const file = formData.get("image");
|
||||||
|
|
||||||
|
if (!file || !(file instanceof Blob)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "No image file provided" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
const prompt = `Generate a 1-3 sentence description for this image.`;
|
||||||
|
|
||||||
|
const data = await gemini.generateDescription(prompt, buffer);
|
||||||
|
const points = calculatePoints(data?.description || ""); // Calculate points based on the description
|
||||||
|
|
||||||
|
let postData = await db.posts.create(
|
||||||
|
userData.id,
|
||||||
|
data?.description,
|
||||||
|
buffer.toString("base64")
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.users.update(userData.id, { points: userData.points + points });
|
||||||
|
|
||||||
|
if (!postData) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to create post" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Image uploaded successfully", postData, points },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling image upload:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
try {
|
||||||
let userData = await authenticateUser();
|
let userData = await authenticateUser();
|
||||||
if (!userData) return NextResponse.json({ message: "User not found" }, { status: 404 });
|
if (!userData) return NextResponse.json({ message: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
const formData = await req.formData();
|
const posts = await db.posts.getAllByUserId(userData.id);
|
||||||
const file = formData.get("image");
|
|
||||||
|
|
||||||
if (!file || !(file instanceof Blob)) {
|
if (!posts) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "No image file provided" },
|
{ message: "Failed to fetch posts" },
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
|
||||||
|
|
||||||
const prompt = `Generate a 1-3 sentence description for this image.`;
|
|
||||||
|
|
||||||
const data = await gemini.generateDescription(prompt, buffer);
|
|
||||||
let postData = await db.posts.create(userData.id, data?.description, buffer);
|
|
||||||
|
|
||||||
if (!postData) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Failed to create post" },
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Image uploaded successfully", postData },
|
{ message: "Posts fetched successfully", posts },
|
||||||
{ status: 200 }
|
{ status: 200 }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling image upload:", error);
|
console.error("Error fetching posts:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Internal server error" },
|
{ message: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
125
src/lib/components/Post.tsx
Normal file
125
src/lib/components/Post.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface PostProps {
|
||||||
|
post: {
|
||||||
|
id: string;
|
||||||
|
timeStamp: string;
|
||||||
|
reactions: { liked: boolean; warned: boolean }[];
|
||||||
|
userId: string;
|
||||||
|
image: string; // Assuming the image is served as a base64 string or URL
|
||||||
|
imageDes: string; // New field for the post description
|
||||||
|
};
|
||||||
|
onLike: () => void;
|
||||||
|
onWarning: () => void;
|
||||||
|
onDelete: () => void; // Callback for deleting the post
|
||||||
|
userReactions: { liked: boolean; warned: boolean };
|
||||||
|
allowReactions: boolean; // New property to toggle reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Post({
|
||||||
|
post,
|
||||||
|
onLike,
|
||||||
|
onWarning,
|
||||||
|
onDelete,
|
||||||
|
userReactions,
|
||||||
|
allowReactions,
|
||||||
|
}: PostProps) {
|
||||||
|
const [userData, setUserData] = useState<{ username: string; avatar: number } | null>({username: "Loading...", avatar: 1});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch the username and avatar based on the userId
|
||||||
|
fetch(`/api/user/${post.userId}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.user) {
|
||||||
|
setUserData({
|
||||||
|
username: data.user.username,
|
||||||
|
avatar: data.user.avatar || "/default-avatar.png", // Fallback to a default avatar
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUserData({
|
||||||
|
username: "Unknown User",
|
||||||
|
avatar: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error fetching user data:", err);
|
||||||
|
setUserData({
|
||||||
|
username: "Unknown User",
|
||||||
|
avatar: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [post.userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[color:var(--color-surface-600)]/80 rounded-xl px-6 py-5 shadow-md">
|
||||||
|
<div className="flex items-center gap-4 mb-2">
|
||||||
|
{/* User Avatar */}
|
||||||
|
<img
|
||||||
|
src={"/avatar/p" + userData?.avatar + ".png"}
|
||||||
|
alt="User Avatar"
|
||||||
|
className="w-10 h-10 rounded-full object-cover border border-gray-300"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{/* Username */}
|
||||||
|
<p className="font-semibold text-[color:var(--color-warning-300)]">
|
||||||
|
{userData?.username || "Loading..."}
|
||||||
|
</p>
|
||||||
|
{/* Timestamp */}
|
||||||
|
<p className="text-sm text-gray-400">{new Date(post.timeStamp).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Post Image */}
|
||||||
|
{post.image && (
|
||||||
|
<img
|
||||||
|
src={"data:image/png;base64," + post.image}
|
||||||
|
alt="Post related"
|
||||||
|
className="w-full max-h-64 object-cover rounded mb-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Post Description */}
|
||||||
|
{post.imageDes && (
|
||||||
|
<p className="text-neutral-100 mb-4">{post.imageDes}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Post Actions */}
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
{allowReactions && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={onLike}
|
||||||
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
|
userReactions.liked
|
||||||
|
? "bg-success-800"
|
||||||
|
: "bg-success-600 hover:bg-primary-600"
|
||||||
|
} text-white`}
|
||||||
|
>
|
||||||
|
👍 Like ({post.reactions.filter((reaction) => reaction.liked).length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onWarning}
|
||||||
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
|
userReactions.warned
|
||||||
|
? "bg-red-800"
|
||||||
|
: "bg-primary-500 hover:bg-red-600"
|
||||||
|
} text-white`}
|
||||||
|
>
|
||||||
|
😭 Stop drinking that ({post.reactions.filter((reaction) => reaction.warned).length})
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
className="px-3 py-1 rounded text-sm bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ const postSchema = new mongoose.Schema({
|
|||||||
timeStamp: Date,
|
timeStamp: Date,
|
||||||
reactions: Array,
|
reactions: Array,
|
||||||
userId: reqString,
|
userId: reqString,
|
||||||
image: Buffer
|
image: String
|
||||||
});
|
});
|
||||||
|
|
||||||
export class Post {
|
export class Post {
|
||||||
@@ -32,7 +32,7 @@ export class Post {
|
|||||||
return result.join('');
|
return result.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(userId:string, imageDes: string, image: Buffer) {
|
async create(userId:string, imageDes: string, image: string) {
|
||||||
const newEntry = new this.model({
|
const newEntry = new this.model({
|
||||||
id: this.makeId(5),
|
id: this.makeId(5),
|
||||||
imageDes: imageDes,
|
imageDes: imageDes,
|
||||||
@@ -57,6 +57,13 @@ export class Post {
|
|||||||
return await this.model.find({ userId: userId });
|
return await this.model.find({ userId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addReaction(id: string, reaction: { liked: boolean; warned: boolean }) {
|
||||||
|
return await this.model.updateOne(
|
||||||
|
{ id: id },
|
||||||
|
{ $push: { reactions: reaction } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async update(id: string, imageDes: string) {
|
async update(id: string, imageDes: string) {
|
||||||
return await this.model.updateOne({ id: id }, { imageDes: imageDes });
|
return await this.model.updateOne({ id: id }, { imageDes: imageDes });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user