diff --git a/src/app/[handle]/page.tsx b/src/app/[handle]/page.tsx index 3676a50..ffed044 100644 --- a/src/app/[handle]/page.tsx +++ b/src/app/[handle]/page.tsx @@ -59,6 +59,7 @@ export default function ProfilePage() { bio: '', avatarUrl: '', headerUrl: '', + website: '', }); const [saveError, setSaveError] = useState(null); const [isSaving, setIsSaving] = useState(false); @@ -112,6 +113,7 @@ export default function ProfilePage() { bio: user.bio || '', avatarUrl: user.avatarUrl || '', headerUrl: user.headerUrl || '', + website: user.website || '', }); } }, [user, currentUser]); @@ -353,6 +355,26 @@ export default function ProfilePage() { Joined {formatDate(user.createdAt || new Date().toISOString())} + {user.website && ( +
+ + {user.website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')} + +
+ )} +
+
+ + setProfileForm({ ...profileForm, website: e.target.value })} + maxLength={100} + /> +
diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index ecf43d2..a31e637 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -9,6 +9,7 @@ const updateProfileSchema = z.object({ bio: z.string().max(160).optional().nullable(), avatarUrl: z.string().url().optional().nullable(), headerUrl: z.string().url().optional().nullable(), + website: z.string().url().or(z.string().length(0)).optional().nullable(), }); export async function GET() { @@ -31,6 +32,7 @@ export async function GET() { displayName: session.user.displayName, avatarUrl: session.user.avatarUrl, bio: session.user.bio, + website: session.user.website, }, }); } catch (error) { @@ -54,6 +56,7 @@ export async function PATCH(request: Request) { bio?: string | null; avatarUrl?: string | null; headerUrl?: string | null; + website?: string | null; updatedAt?: Date; } = {}; @@ -61,6 +64,7 @@ export async function PATCH(request: Request) { if (data.bio !== undefined) updateData.bio = data.bio === '' ? null : data.bio; if (data.avatarUrl !== undefined) updateData.avatarUrl = data.avatarUrl === '' ? null : data.avatarUrl; if (data.headerUrl !== undefined) updateData.headerUrl = data.headerUrl === '' ? null : data.headerUrl; + if (data.website !== undefined) updateData.website = data.website === '' ? null : data.website; if (Object.keys(updateData).length === 0) { return NextResponse.json({ @@ -71,6 +75,7 @@ export async function PATCH(request: Request) { avatarUrl: currentUser.avatarUrl, bio: currentUser.bio, headerUrl: currentUser.headerUrl, + website: currentUser.website, followersCount: currentUser.followersCount, followingCount: currentUser.followingCount, postsCount: currentUser.postsCount, @@ -94,6 +99,7 @@ export async function PATCH(request: Request) { avatarUrl: updatedUser.avatarUrl, bio: updatedUser.bio, headerUrl: updatedUser.headerUrl, + website: updatedUser.website, followersCount: updatedUser.followersCount, followingCount: updatedUser.followingCount, postsCount: updatedUser.postsCount, diff --git a/src/app/api/posts/[id]/route.ts b/src/app/api/posts/[id]/route.ts index b3ca352..69d2a24 100644 --- a/src/app/api/posts/[id]/route.ts +++ b/src/app/api/posts/[id]/route.ts @@ -94,3 +94,59 @@ export async function GET( ); } } + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { requireAuth } = await import('@/lib/auth'); + const user = await requireAuth(); + const { id } = await params; + + const post = await db.query.posts.findFirst({ + where: eq(posts.id, id), + }); + + if (!post) { + return NextResponse.json({ error: 'Post not found' }, { status: 404 }); + } + + if (post.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }); + } + + // 1. If it's a reply, decrement parent's repliesCount + if (post.replyToId) { + const parentPost = await db.query.posts.findFirst({ + where: eq(posts.id, post.replyToId), + }); + if (parentPost && parentPost.repliesCount > 0) { + await db.update(posts) + .set({ repliesCount: parentPost.repliesCount - 1 }) + .where(eq(posts.id, post.replyToId)); + } + } + + // 2. Delete the post (cascades to media, likes, notifications) + await db.delete(posts).where(eq(posts.id, id)); + + // 3. Decrement user's postsCount + if (user.postsCount > 0) { + await db.update(users) + .set({ postsCount: user.postsCount - 1 }) + .where(eq(users.id, user.id)); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Delete post error:', error); + if (error instanceof Error && error.message === 'Authentication required') { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + return NextResponse.json( + { error: 'Failed to delete post' }, + { status: 500 } + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 1641cd5..a5dc3d7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -373,6 +373,10 @@ a.btn-primary:visited { color: var(--accent); } +.post-action.delete-action:hover { + color: #ef4444; +} + /* User rows (followers/following) */ .user-row { display: flex; diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index f45a742..cd6510f 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -128,3 +128,12 @@ export const SettingsIcon = () => ( ); + +export const TrashIcon = () => ( + + + + + + +); diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 1aec5be..42023e0 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -2,21 +2,25 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; -import { HeartIcon, RepeatIcon, MessageIcon, FlagIcon } from '@/components/Icons'; +import { HeartIcon, RepeatIcon, MessageIcon, FlagIcon, TrashIcon } from '@/components/Icons'; import { Post } from '@/lib/types'; +import { useAuth } from '@/lib/contexts/AuthContext'; interface PostCardProps { post: Post; onLike?: (id: string, currentLiked: boolean) => void; onRepost?: (id: string, currentReposted: boolean) => void; onComment?: (post: Post) => void; + onDelete?: (id: string) => void; isDetail?: boolean; } -export function PostCard({ post, onLike, onRepost, onComment, isDetail }: PostCardProps) { +export function PostCard({ post, onLike, onRepost, onComment, onDelete, isDetail }: PostCardProps) { + const { user: currentUser } = useAuth(); const [liked, setLiked] = useState(post.isLiked || false); const [reposted, setReposted] = useState(post.isReposted || false); const [reporting, setReporting] = useState(false); + const [deleting, setDeleting] = useState(false); // Sync state if post changes (e.g. after a re-render from parent) useEffect(() => { @@ -89,6 +93,28 @@ export function PostCard({ post, onLike, onRepost, onComment, isDetail }: PostCa setReporting(false); } }; + const handleDelete = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (deleting) return; + if (!window.confirm('Are you sure you want to delete this post? This action cannot be undone.')) return; + setDeleting(true); + try { + const res = await fetch(`/api/posts/${post.id}`, { + method: 'DELETE', + }); + if (res.ok) { + onDelete?.(post.id); + } else { + const data = await res.json(); + alert(data.error || 'Failed to delete post'); + } + } catch { + alert('Failed to delete post'); + } finally { + setDeleting(false); + } + }; const postUrl = `/${post.author.handle}/posts/${post.id}`; @@ -174,6 +200,12 @@ export function PostCard({ post, onLike, onRepost, onComment, isDetail }: PostCa {reporting ? '...' : ''} + {currentUser?.id === post.author.id && ( + + )}
); diff --git a/src/db/schema.ts b/src/db/schema.ts index d794193..a486f36 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -49,6 +49,7 @@ export const users = pgTable('users', { followersCount: integer('followers_count').default(0).notNull(), followingCount: integer('following_count').default(0).notNull(), postsCount: integer('posts_count').default(0).notNull(), + website: text('website'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => [ diff --git a/src/lib/types.ts b/src/lib/types.ts index 0281818..c76a665 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -8,6 +8,7 @@ export interface User { followersCount?: number; followingCount?: number; postsCount?: number; + website?: string | null; createdAt?: string; movedTo?: string | null; }