AI, API, Profile Page Update
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth0/nextjs-auth0": "^4.4.2",
|
"@auth0/nextjs-auth0": "^4.4.2",
|
||||||
|
"@google/genai": "^0.8.0",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.487.0",
|
||||||
"mongoose": "^8.13.2",
|
"mongoose": "^8.13.2",
|
||||||
|
|||||||
@@ -1,64 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// const useDevice = () => ({
|
import React from "react";
|
||||||
// isAuthenticated: true,
|
|
||||||
// session: { username: "demo_user" },
|
|
||||||
// });
|
|
||||||
|
|
||||||
import { useDevice } from "@/lib/context/DeviceContext";
|
import { useDevice } from "@/lib/context/DeviceContext";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { isAuthenticated, session } = useDevice();
|
const { isAuthenticated, session } = useDevice();
|
||||||
const [bio, setBio] = useState("");
|
|
||||||
const [saved, setSaved] = useState(false);
|
|
||||||
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
||||||
const [uploadMessage, setUploadMessage] = useState<string>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isAuthenticated && session?.username) {
|
|
||||||
fetch(`/api/user/${session.username}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.bio) setBio(data.bio);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, session?.username]);
|
|
||||||
|
|
||||||
const handleBioSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const res = await fetch(`/api/user/${session.username}/bio`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ bio }),
|
|
||||||
});
|
|
||||||
if (res.ok) setSaved(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 py-10 max-w-full lg:max-w-1/2 mx-auto font-sans text-neutral-100">
|
<div className="px-6 py-10 max-w-full lg:max-w-1/2 mx-auto font-sans text-neutral-100">
|
||||||
<div className="bg-[color:var(--color-surface-600)]/70 backdrop-blur-md rounded-xl px-6 py-5 my-6 shadow-sm">
|
<div className="bg-[color:var(--color-surface-600)]/70 backdrop-blur-md rounded-xl px-6 py-5 my-6 shadow-sm">
|
||||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-[-.01em] text-center sm:text-left">
|
<h1 className="text-2xl sm:text-3xl font-bold tracking-[-.01em] text-center sm:text-left">
|
||||||
Hi, {isAuthenticated ? session.username : ""}!!
|
Hi, {session?.username || ""}!!
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="flex flex-col items-center mt-6">
|
<div className="flex flex-col items-center mt-6">
|
||||||
{previewUrl ? (
|
{isAuthenticated && (
|
||||||
<img
|
<img
|
||||||
src={previewUrl}
|
src={"/p" + session?.avatar + ".png"}
|
||||||
alt="Profile Preview"
|
alt="Profile Preview"
|
||||||
className="w-32 h-32 rounded-full object-cover border border-gray-300"
|
className="w-42 h-42 rounded-full object-cover bg-surface-700"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className="w-32 h-32 rounded-full bg-neutral-800 border border-gray-300" />
|
|
||||||
)}
|
)}
|
||||||
{uploadMessage && <p className="text-sm text-gray-400 mt-2">{uploadMessage}</p>}
|
{/* {uploadMessage && (
|
||||||
|
<p className="text-sm text-gray-400 mt-2">{uploadMessage}</p>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-6">{bio || "No bio yet..."}</p>
|
{/* <p className="mb-6">{bio || "No bio yet..."}</p>
|
||||||
|
|
||||||
<form onSubmit={handleBioSubmit} className="mb-6 space-y-2">
|
<form onSubmit={handleBioSubmit} className="mb-6 space-y-2">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -75,7 +45,7 @@ export default function ProfilePage() {
|
|||||||
Save Bio
|
Save Bio
|
||||||
</button>
|
</button>
|
||||||
{saved && <p className="text-green-400 text-sm">Bio saved!</p>}
|
{saved && <p className="text-green-400 text-sm">Bio saved!</p>}
|
||||||
</form>
|
</form> */}
|
||||||
|
|
||||||
{/* Friends */}
|
{/* Friends */}
|
||||||
<div className="bg-[color:var(--color-surface-600)] rounded-xl px-6 py-5 my-6 shadow-md">
|
<div className="bg-[color:var(--color-surface-600)] rounded-xl px-6 py-5 my-6 shadow-md">
|
||||||
@@ -96,7 +66,7 @@ export default function ProfilePage() {
|
|||||||
{/* Daily Stats */}
|
{/* Daily Stats */}
|
||||||
<div className="bg-[color:var(--color-surface-600)]/70 backdrop-blur-md rounded-xl px-6 py-5 my-6 shadow-sm">
|
<div className="bg-[color:var(--color-surface-600)]/70 backdrop-blur-md rounded-xl px-6 py-5 my-6 shadow-sm">
|
||||||
<h2 className="text-xl font-semibold text-[color:var(--color-warning-300)] mt-0 mb-2">
|
<h2 className="text-xl font-semibold text-[color:var(--color-warning-300)] mt-0 mb-2">
|
||||||
--------------------Daily Stats---------------------
|
--------------------Daily Stats---------------------
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="list-disc pl-5 text-neutral-200 space-y-1">
|
<ul className="list-disc pl-5 text-neutral-200 space-y-1">
|
||||||
<li>Points Logged: [ dailyPoints variable ]</li>
|
<li>Points Logged: [ dailyPoints variable ]</li>
|
||||||
@@ -104,8 +74,8 @@ export default function ProfilePage() {
|
|||||||
<li>Sugar Logged (g): [ daily sugar variable ]</li>
|
<li>Sugar Logged (g): [ daily sugar variable ]</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="font-semibold italic text-[color:var(--color-success-300)] mt-1">
|
<p className="font-semibold italic text-[color:var(--color-success-300)] mt-1">
|
||||||
Don't forget you have a 400mg caffeine limit and 30.5g sugar limit!
|
Don't forget you have a 400mg caffeine limit and 30.5g sugar limit!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-6" />
|
<div className="h-6" />
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { db } from "../../lib/scripts/db";
|
|
||||||
|
|
||||||
export default function handler(req, res) {
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
|
|
||||||
// Handle GET request
|
|
||||||
res.status(200).json({ message: 'Hello, this is a GET request!' });
|
|
||||||
} else {
|
|
||||||
// Handle unsupported methods
|
|
||||||
res.setHeader('Allow', ['GET']);
|
|
||||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
src/app/api/post/[id]/route.ts
Normal file
36
src/app/api/post/[id]/route.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth0 } from "../../../../lib/scripts/auth0";
|
||||||
|
import { db } from "@/lib/scripts/db";
|
||||||
|
|
||||||
|
async function authenticateUser() {
|
||||||
|
const session = await auth0.getSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ message: "No session found" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionUser = session.user;
|
||||||
|
let userData = await db.users.findByEmail(sessionUser.email as string);
|
||||||
|
if (!userData) return NextResponse.json({ message: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
|
return userData != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ post }, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error finding post by ID:", error);
|
||||||
|
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/app/api/post/route.ts
Normal file
55
src/app/api/post/route.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth0 } from "@/lib/scripts/auth0";
|
||||||
|
import { db } from "@/lib/scripts/db";
|
||||||
|
|
||||||
|
import { gemini } from "@/lib/scripts/gemini";
|
||||||
|
|
||||||
|
async function authenticateUser() {
|
||||||
|
const session = await auth0.getSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ message: "No session found" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionUser = session.user;
|
||||||
|
let userData = await db.users.findByEmail(sessionUser.email as string);
|
||||||
|
if (!userData)
|
||||||
|
return NextResponse.json({ message: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
// if (!(await authenticateUser())) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
let postData = await db.posts.create("6gi1f", data?.description);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Image uploaded successfully", postData },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling image upload:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/app/api/user/[id]/route.ts
Normal file
36
src/app/api/user/[id]/route.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth0 } from "../../../../lib/scripts/auth0";
|
||||||
|
import { db } from "@/lib/scripts/db";
|
||||||
|
|
||||||
|
async function authenticateUser() {
|
||||||
|
const session = await auth0.getSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ message: "No session found" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionUser = session.user;
|
||||||
|
let userData = await db.users.findByEmail(sessionUser.email as string);
|
||||||
|
if (!userData) return NextResponse.json({ message: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
|
return userData != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: Request, { params }: any) {
|
||||||
|
try {
|
||||||
|
if (!(await authenticateUser())) return;
|
||||||
|
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
const user = await db.users.findById(id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ message: "User not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ user }, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error finding user by ID:", error);
|
||||||
|
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,12 @@ import { Navigation } from "@skeletonlabs/skeleton-react";
|
|||||||
import {
|
import {
|
||||||
Home as IconHome,
|
Home as IconHome,
|
||||||
BookText as BookImage,
|
BookText as BookImage,
|
||||||
Coins as CoinsImage,
|
|
||||||
User as UserImage,
|
User as UserImage,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useDevice } from "@/lib/context/DeviceContext";
|
import { useDevice } from "@/lib/context/DeviceContext";
|
||||||
|
|
||||||
const MobileNav = () => {
|
const MobileNav = () => {
|
||||||
|
|
||||||
const { isAuthenticated } = useDevice();
|
const { isAuthenticated } = useDevice();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const userSchema = new mongoose.Schema({
|
|||||||
id: reqString,
|
id: reqString,
|
||||||
email: reqString,
|
email: reqString,
|
||||||
username: reqString,
|
username: reqString,
|
||||||
avatar: String,
|
bio: String,
|
||||||
|
avatar: Number,
|
||||||
points: Number,
|
points: Number,
|
||||||
inventory: Array,
|
inventory: Array,
|
||||||
friends: Array<String>,
|
friends: Array<String>,
|
||||||
@@ -39,11 +40,15 @@ export class User {
|
|||||||
username: string
|
username: string
|
||||||
) {
|
) {
|
||||||
const id = this.makeId(5);
|
const id = this.makeId(5);
|
||||||
|
|
||||||
|
// random number from 1 to 5
|
||||||
|
const randomNumber = Math.floor(Math.random() * 5) + 1;
|
||||||
|
|
||||||
const newEntry = new this.model({
|
const newEntry = new this.model({
|
||||||
id: id,
|
id: id,
|
||||||
email: email,
|
email: email,
|
||||||
username: username,
|
username: username,
|
||||||
avatar: null,
|
avatar: randomNumber,
|
||||||
points: 0,
|
points: 0,
|
||||||
inventory: [],
|
inventory: [],
|
||||||
friends: [],
|
friends: [],
|
||||||
|
|||||||
71
src/lib/scripts/gemini.ts
Normal file
71
src/lib/scripts/gemini.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { GoogleGenAI, Type } from "@google/genai";
|
||||||
|
|
||||||
|
class Gemini {
|
||||||
|
ai: GoogleGenAI | null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.ai = null;
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
||||||
|
}
|
||||||
|
|
||||||
|
fileToGenerativePart(buffer: Buffer) {
|
||||||
|
return {
|
||||||
|
mimeType: "image/png",
|
||||||
|
data: buffer.toString("base64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateDescription(
|
||||||
|
prompt: string,
|
||||||
|
imageBuffer: Buffer
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const imagePart = {
|
||||||
|
inlineData: {
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
data: imageBuffer.toString("base64"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.ai?.models.generateContent({
|
||||||
|
model: "gemini-2.0-flash",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
text: prompt,
|
||||||
|
},
|
||||||
|
imagePart,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
responseMimeType: "application/json",
|
||||||
|
responseSchema: {
|
||||||
|
type: Type.OBJECT,
|
||||||
|
properties: {
|
||||||
|
description: {
|
||||||
|
type: Type.STRING,
|
||||||
|
description: "A concise description of the image.",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["description"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
if(!result) return null;
|
||||||
|
else return JSON.parse(result.candidates[0].content.parts[0].text);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in generateDescription:", error);
|
||||||
|
throw new Error("Failed to generate image description");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gemini = new Gemini();
|
||||||
Reference in New Issue
Block a user