From 3c8c73913109e6d9860b1e9eb5645f47b7516a2a Mon Sep 17 00:00:00 2001 From: ukuku360 <148301060+ukuku360@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:11:09 +1100 Subject: [PATCH] Move feed edit action to header and harden community save flow --- apps/web/src/app/styles.css | 41 +++++++ .../components/CommunityCommentSection.tsx | 114 ++++++++++++++++-- .../community/components/CommunityFeed.tsx | 43 ++++++- .../components/CommunityPostCard.tsx | 99 +++++++++++++-- .../community/lib/__tests__/discovery.test.ts | 1 + apps/web/src/features/feed/pages/FeedPage.tsx | 86 ++++++++++++- .../src/services/comments/comments.service.ts | 5 + .../services/community/community.service.ts | 42 +++++++ apps/web/src/services/posts/posts.service.ts | 5 + apps/web/src/types/domain.ts | 2 + supabase_schema.sql | 44 ++++++- 11 files changed, 453 insertions(+), 29 deletions(-) diff --git a/apps/web/src/app/styles.css b/apps/web/src/app/styles.css index facb3b9..59164f8 100644 --- a/apps/web/src/app/styles.css +++ b/apps/web/src/app/styles.css @@ -1110,6 +1110,30 @@ gap: 6px; } + +.rk-post-quick-edit { + border: 1px solid #cde2f8; + border-radius: 999px; + background: #edf5ff; + color: #355f8d; + padding: 5px 11px; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.01em; + white-space: nowrap; + cursor: pointer; +} + +.rk-post-quick-edit:hover:not(:disabled) { + border-color: #b7d6f5; + background: #e3f0ff; +} + +.rk-post-quick-edit:disabled { + opacity: 0.64; + cursor: not-allowed; +} + .rk-admin-quick-delete { border: 1px solid #efb8c4; border-radius: 999px; @@ -2414,6 +2438,23 @@ white-space: pre-wrap; } + +.rk-community-edit-form { + display: grid; + gap: 0.5rem; +} + +.rk-community-inline-actions { + display: inline-flex; + align-items: center; + gap: 0.6rem; +} + +.rk-community-comment-edit-form { + display: grid; + gap: 0.45rem; +} + .rk-community-actions { display: flex; justify-content: flex-end; diff --git a/apps/web/src/features/community/components/CommunityCommentSection.tsx b/apps/web/src/features/community/components/CommunityCommentSection.tsx index a6d91ac..7481a1a 100644 --- a/apps/web/src/features/community/components/CommunityCommentSection.tsx +++ b/apps/web/src/features/community/components/CommunityCommentSection.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { fetchComments, createComment, deleteComment } from '../../../services/community/community.service' +import { fetchComments, createComment, updateComment, deleteComment } from '../../../services/community/community.service' import { useAuthSession } from '../../../app/providers/auth-session-context' import { formatDateTime } from '../../../lib/formatters' import type { CommunityComment } from '../../../types/domain' @@ -18,7 +18,10 @@ export function CommunityCommentSection({ postId }: Props) { const [content, setContent] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [errorMessage, setErrorMessage] = useState('') + const [editingCommentId, setEditingCommentId] = useState(null) + const [editContent, setEditContent] = useState('') const normalizedContent = content.trim() + const normalizedEditContent = editContent.trim() const isSubmitDisabled = isSubmitting || normalizedContent.length === 0 const { data: comments = [], isLoading } = useQuery({ @@ -33,7 +36,7 @@ export function CommunityCommentSection({ postId }: Props) { }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['community_comments', postId] }) - queryClient.invalidateQueries({ queryKey: ['community_posts'] }) // Update comment count on post + queryClient.invalidateQueries({ queryKey: ['community_posts'] }) setContent('') setIsSubmitting(false) setErrorMessage('') @@ -44,6 +47,20 @@ export function CommunityCommentSection({ postId }: Props) { } }) + const editCommentMutation = useMutation({ + mutationFn: async ({ commentId, text }: { commentId: string; text: string }) => updateComment(commentId, text), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['community_comments', postId] }) + queryClient.invalidateQueries({ queryKey: ['community_posts'] }) + setEditingCommentId(null) + setEditContent('') + setErrorMessage('') + }, + onError: (error) => { + setErrorMessage(error instanceof Error ? error.message : 'Failed to edit comment.') + }, + }) + const deleteCommentMutation = useMutation({ mutationFn: deleteComment, onSuccess: () => { @@ -63,6 +80,24 @@ export function CommunityCommentSection({ postId }: Props) { addCommentMutation.mutate(normalizedContent) } + function startEditing(comment: CommunityComment) { + setEditingCommentId(comment.id) + setEditContent(comment.content) + setErrorMessage('') + } + + function cancelEditing() { + setEditingCommentId(null) + setEditContent('') + } + + async function handleEditSubmit(e: React.FormEvent, comment: CommunityComment) { + e.preventDefault() + if (normalizedEditContent.length === 0 || normalizedEditContent === comment.content) return + + await editCommentMutation.mutateAsync({ commentId: comment.id, text: normalizedEditContent }) + } + function handleDelete(commentId: string) { if (confirm('Delete this comment?')) { deleteCommentMutation.mutate(commentId) @@ -73,7 +108,6 @@ export function CommunityCommentSection({ postId }: Props) { return (
- {/* List */}
{comments.length === 0 ? (

No replies yet. Start the conversation.

@@ -81,29 +115,83 @@ export function CommunityCommentSection({ postId }: Props) { comments.map((comment) => { const isOwner = user?.id === comment.user_id const canDelete = isOwner + const canEdit = isOwner + const formattedUpdatedAt = formatDateTime(comment.updated_at) + const isEdited = comment.updated_at !== comment.created_at && formattedUpdatedAt !== '-' + const isEditingCurrent = editingCommentId === comment.id return (
{comment.author} - {formatDateTime(comment.created_at)} + + {formatDateTime(comment.created_at)} + {isEdited ? ` (Edited ${formattedUpdatedAt})` : ''} +
-
{comment.content}
- {canDelete && ( - + + {isEditingCurrent ? ( +
void handleEditSubmit(event, comment)} className="rk-community-comment-edit-form"> + setEditContent(event.target.value)} + maxLength={COMMENT_MAX_LENGTH} + disabled={editCommentMutation.isPending} + /> +
+ + +
+
+ ) : ( +
{comment.content}
)} + + {(canEdit || canDelete) && !isEditingCurrent ? ( +
+ {canEdit ? ( + + ) : null} + {canDelete ? ( + + ) : null} +
+ ) : null}
) }) )}
- {/* Form */}
updateCommunityPost(postId, content), + onSuccess: async (updatedPost) => { + queryClient.setQueryData(communityPostsQueryKey, (previous) => { + if (!previous) return [] + return previous.map((post) => { + if (post.id !== updatedPost.id) return post + + return { + ...post, + content: updatedPost.content, + updated_at: updatedPost.updated_at, + } + }) + }) + + await queryClient.invalidateQueries({ queryKey: ['community_posts'] }) + setStatusTone('success') + setStatusMessage('Post updated.') + }, + onError: (error) => { + console.error('Failed to edit post', error) + setStatusTone('error') + setStatusMessage(error instanceof Error ? error.message : 'Failed to update post.') + }, + }) + const deleteMutation = useMutation({ mutationFn: deleteCommunityPost, onSuccess: () => { @@ -301,6 +335,12 @@ export function CommunityFeed() { } }) + + + async function handleEdit(postId: string, content: string) { + await editMutation.mutateAsync({ postId, content }) + } + function handleDelete(id: string) { if (!confirm('Are you sure you want to delete this post?')) return deleteMutation.mutate(id) @@ -523,6 +563,7 @@ export function CommunityFeed() { isReportPending={Boolean(isReportPendingByPostId[post.id])} isAdminDeletePending={Boolean(isAdminDeletePendingByPostId[post.id])} communityPostsQueryKey={communityPostsQueryKey} + onEdit={handleEdit} onDelete={handleDelete} onAdminDelete={handleAdminQuickDelete} onToggleReport={handleReportToggle} diff --git a/apps/web/src/features/community/components/CommunityPostCard.tsx b/apps/web/src/features/community/components/CommunityPostCard.tsx index 682768f..539d9b9 100644 --- a/apps/web/src/features/community/components/CommunityPostCard.tsx +++ b/apps/web/src/features/community/components/CommunityPostCard.tsx @@ -15,6 +15,7 @@ type Props = { isReportPending: boolean isAdminDeletePending: boolean communityPostsQueryKey: readonly ['community_posts'] + onEdit: (id: string, content: string) => void | Promise onDelete: (id: string) => void onAdminDelete: (post: CommunityPost) => void | Promise onToggleReport: (id: string, isReported: boolean) => void | Promise @@ -23,6 +24,8 @@ type Props = { elementId: string } +const COMMUNITY_POST_MAX_LENGTH = 280 + export function CommunityPostCard({ post, currentUserId, @@ -32,6 +35,7 @@ export function CommunityPostCard({ isReportPending, isAdminDeletePending, communityPostsQueryKey, + onEdit, onDelete, onAdminDelete, onToggleReport, @@ -42,8 +46,20 @@ export function CommunityPostCard({ const isOwner = currentUserId === post.user_id const canDelete = isOwner const [showComments, setShowComments] = useState(false) + const [isEditing, setIsEditing] = useState(false) + const [editContent, setEditContent] = useState(post.content) + const [isEditPending, setIsEditPending] = useState(false) const queryClient = useQueryClient() + const normalizedEditContent = editContent.trim() + const formattedUpdatedAt = formatDateTime(post.updated_at) + const isEdited = post.updated_at !== post.created_at && formattedUpdatedAt !== '-' + const isEditSubmitDisabled = + isEditPending || + normalizedEditContent.length === 0 || + normalizedEditContent.length > COMMUNITY_POST_MAX_LENGTH || + normalizedEditContent === post.content + const likeMutation = useMutation({ mutationFn: async () => { if (!currentUserId) return @@ -85,12 +101,31 @@ export function CommunityPostCard({ likeMutation.mutate() } + async function handleEditSubmit(e: React.FormEvent) { + e.preventDefault() + if (isEditSubmitDisabled) return + + try { + setIsEditPending(true) + await onEdit(post.id, normalizedEditContent) + setIsEditing(false) + } finally { + setIsEditPending(false) + } + } + + function handleEditCancel() { + setEditContent(post.content) + setIsEditing(false) + } + return (
{post.author}
{formatDateTime(post.created_at)} + {isEdited ? (Edited {formattedUpdatedAt}) : null} {canAdminDelete ? (
-
{post.content}
+ {isEditing ? ( + +