diff --git a/bun.lockb b/bun.lockb index d89b9d2..61fe6a7 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index fbb3105..d8a2688 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "NODE_ENV=development next dev -p 3002 --turbo & local-ssl-proxy --config ./ssl-proxy.json", + "dev:plain": "next dev --turbo", "dev:ssl": "NODE_ENV=development next dev -p 3002 --turbo & local-ssl-proxy --config ./ssl-proxy.json", "build": "next build", "start": "next start", diff --git a/public/uploads/1751655970353-598daxi8ps7.png b/public/uploads/1751655970353-598daxi8ps7.png new file mode 100644 index 0000000..6c37ccb Binary files /dev/null and b/public/uploads/1751655970353-598daxi8ps7.png differ diff --git a/public/uploads/1751655973928-ypc5n04gq6.png b/public/uploads/1751655973928-ypc5n04gq6.png new file mode 100644 index 0000000..6c37ccb Binary files /dev/null and b/public/uploads/1751655973928-ypc5n04gq6.png differ diff --git a/public/uploads/1751656225977-ldr7sx3ol68.png b/public/uploads/1751656225977-ldr7sx3ol68.png new file mode 100644 index 0000000..60e6126 Binary files /dev/null and b/public/uploads/1751656225977-ldr7sx3ol68.png differ diff --git a/public/uploads/1751657713879-9rw2aztx9zp.png b/public/uploads/1751657713879-9rw2aztx9zp.png new file mode 100644 index 0000000..6c37ccb Binary files /dev/null and b/public/uploads/1751657713879-9rw2aztx9zp.png differ diff --git a/src/app/api/comments/route.ts b/src/app/api/comments/route.ts new file mode 100644 index 0000000..2d8b8d5 --- /dev/null +++ b/src/app/api/comments/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerAuthSession } from '@/lib/server-auth'; +import { db } from '@/lib/db'; +import { comments, users } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; + +export async function POST(request: NextRequest) { + try { + const session = await getServerAuthSession(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + const { imageId, content } = await request.json(); + + if (!imageId || !content?.trim()) { + return NextResponse.json( + { error: 'Image ID and content are required' }, + { status: 400 } + ); + } + + // Insert the comment + const result = await db + .insert(comments) + .values({ + content: content.trim(), + imageId, + authorId: session.user.id, + }) + .returning({ + id: comments.id, + content: comments.content, + createdAt: comments.createdAt, + }); + + // Get the author info for the response + const authorInfo = await db + .select({ + name: users.name, + email: users.email, + }) + .from(users) + .where(eq(users.id, session.user.id)) + .limit(1); + + const commentWithAuthor = { + ...result[0], + authorName: authorInfo[0]?.name || null, + authorEmail: authorInfo[0]?.email || '', + }; + + return NextResponse.json(commentWithAuthor); + } catch (error) { + console.error('Error creating comment:', error); + return NextResponse.json( + { error: 'Failed to create comment' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/images/update-title/route.ts b/src/app/api/images/update-title/route.ts new file mode 100644 index 0000000..0d10987 --- /dev/null +++ b/src/app/api/images/update-title/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerAuthSession } from '@/lib/server-auth'; +import { db } from '@/lib/db'; +import { images } from '@/lib/db/schema'; +import { eq, and } from 'drizzle-orm'; + +export async function PATCH(request: NextRequest) { + try { + const session = await getServerAuthSession(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + const { imageId, title } = await request.json(); + + if (!imageId || !title?.trim()) { + return NextResponse.json( + { error: 'Image ID and title are required' }, + { status: 400 } + ); + } + + // Update the title only if the user owns the image + const result = await db + .update(images) + .set({ + title: title.trim(), + updatedAt: new Date(), + }) + .where( + and( + eq(images.id, imageId), + eq(images.uploadedBy, session.user.id) + ) + ) + .returning({ + id: images.id, + title: images.title, + }); + + if (result.length === 0) { + return NextResponse.json( + { error: 'Image not found or you do not have permission to edit it' }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, title: result[0].title }); + } catch (error) { + console.error('Error updating title:', error); + return NextResponse.json( + { error: 'Failed to update title' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/images/upload/route.ts b/src/app/api/images/upload/route.ts index d996a16..7dce789 100644 --- a/src/app/api/images/upload/route.ts +++ b/src/app/api/images/upload/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { auth } from '@/lib/auth'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; import { db } from '@/lib/db'; import { images, tags, imageTags } from '@/lib/db/schema'; import { writeFile, mkdir } from 'fs/promises'; @@ -8,19 +9,28 @@ import { eq } from 'drizzle-orm'; export async function POST(request: NextRequest) { try { - const session = await auth(); + console.log('Upload API called'); + const session = await getServerSession(authOptions); + console.log('Session:', session); + if (!session?.user?.id) { + console.log('No session or user ID found'); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } + console.log('User ID:', session.user.id); + const formData = await request.formData(); const file = formData.get('file') as File; const title = formData.get('title') as string; const description = formData.get('description') as string; const tagsInput = formData.get('tags') as string; - if (!file || !title) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + console.log('Processing file:', file?.name); + console.log('Form data - Title:', title, 'Description:', description, 'Tags:', tagsInput); + + if (!file) { + return NextResponse.json({ error: 'Missing file' }, { status: 400 }); } // Validate file type @@ -43,11 +53,13 @@ export async function POST(request: NextRequest) { await writeFile(filepath, Buffer.from(bytes)); // Create image record + console.log('Creating image record in database...'); const imageUrl = `/uploads/${filename}`; + const imageTitle = title || file.name.split('.')[0]; // Use filename without extension as fallback const [newImage] = await db .insert(images) .values({ - title, + title: imageTitle, description: description || null, filename, originalName: file.name, @@ -58,6 +70,8 @@ export async function POST(request: NextRequest) { }) .returning(); + console.log('Image created with ID:', newImage.id); + // Process tags if (tagsInput) { const tagNames = tagsInput diff --git a/src/app/image/[id]/page.tsx b/src/app/image/[id]/page.tsx new file mode 100644 index 0000000..e255d6d --- /dev/null +++ b/src/app/image/[id]/page.tsx @@ -0,0 +1,113 @@ +import { notFound } from 'next/navigation'; +import { db } from '@/lib/db'; +import { images, users, comments } from '@/lib/db/schema'; +import { eq, desc } from 'drizzle-orm'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Comments } from '@/components/comments'; +import { EditableTitle } from '@/components/editable-title'; + +interface ImagePageProps { + params: { + id: string; + }; +} + +export default async function ImagePage({ params }: ImagePageProps) { + try { + const imageResult = await db + .select({ + id: images.id, + title: images.title, + description: images.description, + url: images.url, + originalName: images.originalName, + upvotes: images.upvotes, + downvotes: images.downvotes, + createdAt: images.createdAt, + uploadedBy: images.uploadedBy, + uploaderName: users.name, + uploaderEmail: users.email, + }) + .from(images) + .leftJoin(users, eq(images.uploadedBy, users.id)) + .where(eq(images.id, params.id)) + .limit(1); + + if (imageResult.length === 0) { + notFound(); + } + + const image = imageResult[0]; + + // Fetch comments for this image + const imageComments = await db + .select({ + id: comments.id, + content: comments.content, + createdAt: comments.createdAt, + authorName: users.name, + authorEmail: users.email, + }) + .from(comments) + .leftJoin(users, eq(comments.authorId, users.id)) + .where(eq(comments.imageId, params.id)) + .orderBy(desc(comments.createdAt)); + + return ( +
+ + + + + + {/* Image Display */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {image.title +
+ + {/* Description */} + {image.description && ( +
+

Description

+

{image.description}

+
+ )} + + {/* Vote counts */} +
+ 👍 {image.upvotes} upvotes + 👎 {image.downvotes} downvotes +
+ + {/* Comments Section */} +
+ ({ + id: comment.id, + content: comment.content, + authorName: comment.authorName, + authorEmail: comment.authorEmail || '', + createdAt: comment.createdAt.toISOString(), + }))} + /> +
+
+
+
+ ); + } catch (error) { + console.error('Error loading image:', error); + notFound(); + } +} diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx index ddb5b90..7f41fe3 100644 --- a/src/app/upload/page.tsx +++ b/src/app/upload/page.tsx @@ -1,4 +1,4 @@ -import Image from 'next/image'; +'use client'; import { useState } from 'react'; import { useSession } from 'next-auth/react'; @@ -14,7 +14,8 @@ export default function UploadPage() { const [file, setFile] = useState(null); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); - const [tags, setTags] = useState(''); + const [tags, setTags] = useState([]); + const [tagInput, setTagInput] = useState(''); const [uploading, setUploading] = useState(false); const [preview, setPreview] = useState(null); @@ -43,16 +44,40 @@ export default function UploadPage() { } }; + const addTag = (tagText: string) => { + const trimmedTag = tagText.trim().toLowerCase(); + if (trimmedTag && !tags.includes(trimmedTag)) { + setTags([...tags, trimmedTag]); + } + setTagInput(''); + }; + + const removeTag = (tagToRemove: string) => { + setTags(tags.filter(tag => tag !== tagToRemove)); + }; + + const handleTagInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (tagInput.trim()) { + addTag(tagInput); + } + } else if (e.key === 'Backspace' && !tagInput && tags.length > 0) { + // Remove last tag if input is empty and backspace is pressed + removeTag(tags[tags.length - 1]); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!file || !title) return; + if (!file) return; setUploading(true); const formData = new FormData(); formData.append('file', file); formData.append('title', title); formData.append('description', description); - formData.append('tags', tags); + formData.append('tags', tags.join(',')); try { const response = await fetch('/api/images/upload', { @@ -76,21 +101,49 @@ export default function UploadPage() { return (
- - - - - Upload Image - - - -
-
- -
- {preview ? ( + {!preview ? ( + // Image upload only - before file selection +
+
+

Share Your Image

+

Upload an image to get started

+
+ + + +
+ +

Choose an image to upload

+

+ Drag and drop your image here, or click to browse +

+ +
+
+
+
+ ) : ( + // Full form - after file selection + + + + + Upload Image + + + + +
+ +
{/* eslint-disable-next-line @next/next/no-img-element */} { setFile(null); setPreview(null); + setTags([]); + setTagInput(''); }} >
- ) : ( -
- -

- Click to select an image or drag and drop -

-
- )} +
+
+ +
+ setTitle(e.target.value)} + placeholder="Enter image title (optional)" />
-
-
- - setTitle(e.target.value)} - placeholder="Enter image title" - required - /> -
+
+ +