Post deletion, profile link

This commit is contained in:
Christopher
2026-01-22 14:57:42 -08:00
parent 205ec5b5cf
commit 7bc2b1c7e6
8 changed files with 143 additions and 2 deletions
+32
View File
@@ -59,6 +59,7 @@ export default function ProfilePage() {
bio: '',
avatarUrl: '',
headerUrl: '',
website: '',
});
const [saveError, setSaveError] = useState<string | null>(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() {
<span>Joined {formatDate(user.createdAt || new Date().toISOString())}</span>
</div>
{user.website && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '4px',
color: 'var(--accent)',
fontSize: '14px',
}}>
<Link
href={user.website.startsWith('http') ? user.website : `https://${user.website}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit', textDecoration: 'none' }}
>
{user.website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')}
</Link>
</div>
)}
<div style={{ display: 'flex', gap: '16px', marginTop: '12px' }}>
<button
onClick={() => setActiveTab('following')}
@@ -406,6 +428,16 @@ export default function ProfilePage() {
style={{ minHeight: '80px', resize: 'vertical' }}
/>
</div>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Website</label>
<input
className="input"
placeholder="https://example.com"
value={profileForm.website}
onChange={(e) => setProfileForm({ ...profileForm, website: e.target.value })}
maxLength={100}
/>
</div>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Avatar</label>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
+6
View File
@@ -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,
+56
View File
@@ -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 }
);
}
}
+4
View File
@@ -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;