Post Update

This commit is contained in:
2025-04-13 07:20:16 -04:00
parent 8f1dac4937
commit 2719927792
5 changed files with 442 additions and 114 deletions

View File

@@ -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);
} }
}, [isAuthenticated, session?.username]); })
.catch((err) => {
console.error("Error fetching posts:", err);
});
}
}, [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,27 +50,36 @@ 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 };
try {
const response = await fetch(`/api/post/${post.id}`, {
method: "POST",
body: new URLSearchParams({ like: "true" }),
});
if (response.ok) {
const updatedPosts = [...posts];
const alreadyLiked = reactions[index]?.liked; const alreadyLiked = reactions[index]?.liked;
updatedPosts[index].likes += alreadyLiked ? -1 : 1; // Update reactions in the post
updatedPosts[index].reactions.push({ liked: !alreadyLiked, warned: false });
// Update local state
reactions[index] = { reactions[index] = {
...reactions[index], ...reactions[index],
liked: !alreadyLiked, liked: !alreadyLiked,
@@ -66,16 +87,34 @@ export default function PostsPage() {
setPosts(updatedPosts); setPosts(updatedPosts);
setUserReactions(reactions); 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 };
try {
const response = await fetch(`/api/post/${post.id}`, {
method: "POST",
body: new URLSearchParams({ warn: "true" }),
});
if (response.ok) {
const updatedPosts = [...posts];
const alreadyWarned = reactions[index]?.warned; const alreadyWarned = reactions[index]?.warned;
updatedPosts[index].warnings += alreadyWarned ? -1 : 1; // Update reactions in the post
updatedPosts[index].reactions.push({ liked: false, warned: !alreadyWarned });
// Update local state
reactions[index] = { reactions[index] = {
...reactions[index], ...reactions[index],
warned: !alreadyWarned, warned: !alreadyWarned,
@@ -83,6 +122,37 @@ export default function PostsPage() {
setPosts(updatedPosts); setPosts(updatedPosts);
setUserReactions(reactions); 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,57 +187,28 @@ export default function PostsPage() {
</form> </form>
</div> </div>
{/* Post List Card */}
<div className="bg-[color:var(--color-surface-800)] rounded-xl px-6 py-5 shadow-md">
<h2 className="text-2xl font-bold text-[color:var(--color-warning-300)] mb-4">
Post List
</h2>
<div className="space-y-6"> <div className="space-y-6">
{posts.map((post, index) => ( {posts
<div .slice() // Create a shallow copy of the posts array
.sort((a, b) => new Date(b.timeStamp).getTime() - new Date(a.timeStamp).getTime()) // Sort by timeStamp in descending order
.map((post, index) => (
<Post
allowReactions={false}
key={index} key={index}
className="bg-[color:var(--color-surface-600)]/80 rounded-xl px-6 py-5 shadow-md" post={post}
> userReactions={userReactions[index] || { liked: false, warned: false }}
<div className="flex items-center gap-4 mb-2"> onLike={() => handleLike(index)}
<div className="w-10 h-10 rounded-full bg-neutral-800 border border-gray-300" /> onWarning={() => handleWarning(index)}
<div> onDelete={() => handleDelete(index)} // Pass the delete handler
<p className="font-semibold text-[color:var(--color-warning-300)]">
{post.author || "Anonymous"}
</p>
<p className="text-sm text-gray-400">{post.date}</p>
</div>
</div>
{post.imageUrl && (
<img
src={post.imageUrl}
alt="Post related"
className="w-full max-h-64 object-cover rounded mb-4"
/> />
)}
<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>
<div className="h-10" /> <div className="h-10" />
</div> </div>

View File

@@ -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 });
}
}

View File

@@ -19,9 +19,66 @@ async function authenticateUser() {
return userData; return userData;
} }
/*
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) { export async function POST(req: Request) {
try { 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 });
@@ -41,7 +98,15 @@ export async function POST(req: Request) {
const prompt = `Generate a 1-3 sentence description for this image.`; const prompt = `Generate a 1-3 sentence description for this image.`;
const data = await gemini.generateDescription(prompt, buffer); const data = await gemini.generateDescription(prompt, buffer);
let postData = await db.posts.create(userData.id, data?.description, 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) { if (!postData) {
return NextResponse.json( return NextResponse.json(
@@ -51,7 +116,7 @@ export async function POST(req: Request) {
} }
return NextResponse.json( return NextResponse.json(
{ message: "Image uploaded successfully", postData }, { message: "Image uploaded successfully", postData, points },
{ status: 200 } { status: 200 }
); );
} catch (error) { } catch (error) {
@@ -62,3 +127,30 @@ export async function POST(req: Request) {
); );
} }
} }
export async function GET(req: Request) {
try {
let userData = await authenticateUser();
if (!userData) return NextResponse.json({ message: "User not found" }, { status: 404 });
const posts = await db.posts.getAllByUserId(userData.id);
if (!posts) {
return NextResponse.json(
{ message: "Failed to fetch posts" },
{ status: 500 }
);
}
return NextResponse.json(
{ message: "Posts fetched successfully", posts },
{ status: 200 }
);
} catch (error) {
console.error("Error fetching posts:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 }
);
}
}

125
src/lib/components/Post.tsx Normal file
View 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>
);
}

View File

@@ -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 });
} }