|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useState, useEffect } from "react"; |
| 4 | +import { useRouter } from "next/navigation"; |
| 5 | +import { toast } from "sonner"; |
| 6 | +import { ContributionFilters } from "@/components/contributions/contribution-filters"; |
| 7 | +import { ActiveContributions } from "@/components/contributions/active-contribution"; |
| 8 | +import { PastContributions } from "@/components/contributions/past-contributions"; |
| 9 | +import { UserComments } from "@/components/contributions/user-comments"; |
| 10 | +import { CallToAction } from "@/components/contributions/call-to-action"; |
| 11 | +import { CommentEditModal } from "@/components/contributions/comment-edit-modal"; |
| 12 | +import { DeleteConfirmationDialog } from "@/components/contributions/delete-confirmation-dialog"; |
| 13 | +import { LoadingState } from "@/components/contributions/loading-state"; |
| 14 | +import type { |
| 15 | + ActiveProject, |
| 16 | + ContributionStats, |
| 17 | + PastProject, |
| 18 | + SortOption, |
| 19 | + TabOption, |
| 20 | + UserComment, |
| 21 | +} from "@/types/contributions"; |
| 22 | +import { |
| 23 | + fetchActiveProjects, |
| 24 | + fetchCategories, |
| 25 | + fetchContributionStats, |
| 26 | + fetchPastProjects, |
| 27 | + fetchUserComments, |
| 28 | + editComment as apiEditComment, |
| 29 | + deleteComment as apiDeleteComment, |
| 30 | +} from "@/lib/actions/services"; |
| 31 | +import { |
| 32 | + sortActiveProjects, |
| 33 | + sortComments, |
| 34 | + sortPastProjects, |
| 35 | +} from "@/lib/utils"; |
| 36 | +import { ContributionStats as ContributionStatsComponent } from "@/components/contributions/contribution-stats"; |
| 37 | + |
| 38 | +export default function MyContributionsPage() { |
| 39 | + const router = useRouter(); |
| 40 | + const [activeTab, setActiveTab] = useState<TabOption>("all"); |
| 41 | + const [searchQuery, setSearchQuery] = useState(""); |
| 42 | + const [sortOption, setSortOption] = useState<SortOption>("newest"); |
| 43 | + const [categoryFilter, setCategoryFilter] = useState("all"); |
| 44 | + const [categories, setCategories] = useState<string[]>([]); |
| 45 | + |
| 46 | + // Data states - Fix type definitions |
| 47 | + const [stats, setStats] = useState<ContributionStats | null>(null); |
| 48 | + const [activeProjects, setActiveProjects] = useState<ActiveProject[]>([]); |
| 49 | + const [pastProjects, setPastProjects] = useState<PastProject[]>([]); |
| 50 | + const [comments, setComments] = useState<UserComment[]>([]); |
| 51 | + |
| 52 | + // Loading states |
| 53 | + const [isLoadingStats, setIsLoadingStats] = useState(true); |
| 54 | + const [isLoadingActive, setIsLoadingActive] = useState(true); |
| 55 | + const [isLoadingPast, setIsLoadingPast] = useState(true); |
| 56 | + const [isLoadingComments, setIsLoadingComments] = useState(true); |
| 57 | + |
| 58 | + // Modal states |
| 59 | + const [commentToEdit, setCommentToEdit] = useState<UserComment | null>(null); |
| 60 | + const [commentToDelete, setCommentToDelete] = useState<string | null>(null); |
| 61 | + |
| 62 | + // Fetch data on initial load |
| 63 | + useEffect(() => { |
| 64 | + const fetchData = async () => { |
| 65 | + try { |
| 66 | + // Fetch categories |
| 67 | + const categoriesData = await fetchCategories(); |
| 68 | + setCategories(categoriesData); |
| 69 | + |
| 70 | + // Fetch stats |
| 71 | + setIsLoadingStats(true); |
| 72 | + const statsData = await fetchContributionStats(); |
| 73 | + setStats(statsData); |
| 74 | + setIsLoadingStats(false); |
| 75 | + |
| 76 | + // Fetch active projects |
| 77 | + setIsLoadingActive(true); |
| 78 | + const activeData = await fetchActiveProjects(); |
| 79 | + setActiveProjects(activeData); |
| 80 | + setIsLoadingActive(false); |
| 81 | + |
| 82 | + // Fetch past projects |
| 83 | + setIsLoadingPast(true); |
| 84 | + const pastData = await fetchPastProjects(); |
| 85 | + setPastProjects(pastData); |
| 86 | + setIsLoadingPast(false); |
| 87 | + |
| 88 | + // Fetch comments |
| 89 | + setIsLoadingComments(true); |
| 90 | + const commentsData = await fetchUserComments(); |
| 91 | + setComments(commentsData); |
| 92 | + setIsLoadingComments(false); |
| 93 | + } catch (error) { |
| 94 | + console.error("Error fetching data:", error); |
| 95 | + toast.error("Error", { |
| 96 | + description: "Failed to load your contributions. Please try again.", |
| 97 | + }); |
| 98 | + } |
| 99 | + }; |
| 100 | + |
| 101 | + fetchData(); |
| 102 | + }, []); |
| 103 | + |
| 104 | + // Fetch data when category filter changes |
| 105 | + useEffect(() => { |
| 106 | + const fetchFilteredData = async () => { |
| 107 | + try { |
| 108 | + if (activeTab === "all" || activeTab === "votes") { |
| 109 | + // Fetch active projects with category filter |
| 110 | + setIsLoadingActive(true); |
| 111 | + const activeData = await fetchActiveProjects(categoryFilter); |
| 112 | + setActiveProjects(activeData); |
| 113 | + setIsLoadingActive(false); |
| 114 | + |
| 115 | + // Fetch past projects with category filter |
| 116 | + setIsLoadingPast(true); |
| 117 | + const pastData = await fetchPastProjects(categoryFilter); |
| 118 | + setPastProjects(pastData); |
| 119 | + setIsLoadingPast(false); |
| 120 | + } |
| 121 | + } catch (error) { |
| 122 | + console.error("Error fetching filtered data:", error); |
| 123 | + toast.error("Error", { |
| 124 | + description: "Failed to load filtered data. Please try again.", |
| 125 | + }); |
| 126 | + } |
| 127 | + }; |
| 128 | + |
| 129 | + fetchFilteredData(); |
| 130 | + }, [categoryFilter, activeTab]); |
| 131 | + |
| 132 | + // Fetch comments when search query changes |
| 133 | + useEffect(() => { |
| 134 | + const fetchFilteredComments = async () => { |
| 135 | + if (activeTab === "all" || activeTab === "comments") { |
| 136 | + try { |
| 137 | + setIsLoadingComments(true); |
| 138 | + const commentsData = await fetchUserComments(searchQuery); |
| 139 | + setComments(commentsData); |
| 140 | + setIsLoadingComments(false); |
| 141 | + } catch (error) { |
| 142 | + console.error("Error fetching comments:", error); |
| 143 | + toast.error("Error", { |
| 144 | + description: "Failed to load comments. Please try again.", |
| 145 | + }); |
| 146 | + } |
| 147 | + } |
| 148 | + }; |
| 149 | + |
| 150 | + // Debounce search to avoid too many requests |
| 151 | + const debounceTimeout = setTimeout(() => { |
| 152 | + fetchFilteredComments(); |
| 153 | + }, 500); |
| 154 | + |
| 155 | + return () => clearTimeout(debounceTimeout); |
| 156 | + }, [searchQuery, activeTab]); |
| 157 | + |
| 158 | + // Sort data based on selected option |
| 159 | + const sortedActiveProjects = activeProjects |
| 160 | + ? sortActiveProjects(activeProjects, sortOption) |
| 161 | + : []; |
| 162 | + const sortedPastProjects = pastProjects |
| 163 | + ? sortPastProjects(pastProjects, sortOption) |
| 164 | + : []; |
| 165 | + const sortedComments = comments ? sortComments(comments, sortOption) : []; |
| 166 | + |
| 167 | + // Navigation and action handlers |
| 168 | + const navigateToProject = (projectId: string) => { |
| 169 | + router.push(`/projects/${projectId}`); |
| 170 | + }; |
| 171 | + |
| 172 | + // Fix the type to match what UserComments component expects |
| 173 | + const handleEditComment = (commentId: string) => { |
| 174 | + const comment = comments.find((c) => c.id === commentId); |
| 175 | + if (comment) { |
| 176 | + setCommentToEdit(comment); |
| 177 | + } |
| 178 | + }; |
| 179 | + |
| 180 | + const handleDeleteComment = (commentId: string) => { |
| 181 | + setCommentToDelete(commentId); |
| 182 | + }; |
| 183 | + |
| 184 | + const saveEditedComment = async (id: string, content: string) => { |
| 185 | + try { |
| 186 | + await apiEditComment(id, content); |
| 187 | + |
| 188 | + // Update local state |
| 189 | + setComments((prevComments) => |
| 190 | + prevComments.map((comment) => |
| 191 | + comment.id === id ? { ...comment, content } : comment, |
| 192 | + ), |
| 193 | + ); |
| 194 | + |
| 195 | + toast.success("Success", { |
| 196 | + description: "Comment updated successfully", |
| 197 | + }); |
| 198 | + } catch (error) { |
| 199 | + console.error("Error updating comment:", error); |
| 200 | + toast.error("Error", { |
| 201 | + description: "Failed to update comment. Please try again.", |
| 202 | + }); |
| 203 | + throw error; // Re-throw to handle in the modal |
| 204 | + } |
| 205 | + }; |
| 206 | + |
| 207 | + const confirmDeleteComment = async () => { |
| 208 | + if (!commentToDelete) return; |
| 209 | + |
| 210 | + try { |
| 211 | + await apiDeleteComment(commentToDelete); |
| 212 | + |
| 213 | + // Update local state |
| 214 | + setComments((prevComments) => |
| 215 | + prevComments.filter((comment) => comment.id !== commentToDelete), |
| 216 | + ); |
| 217 | + |
| 218 | + toast.success("Success", { |
| 219 | + description: "Comment deleted successfully", |
| 220 | + }); |
| 221 | + } catch (error) { |
| 222 | + console.error("Error deleting comment:", error); |
| 223 | + toast.error("Error", { |
| 224 | + description: "Failed to delete comment. Please try again.", |
| 225 | + }); |
| 226 | + throw error; // Re-throw to handle in the dialog |
| 227 | + } |
| 228 | + }; |
| 229 | + |
| 230 | + return ( |
| 231 | + <div className="container mx-auto py-8 max-w-7xl"> |
| 232 | + <h1 className="text-3xl font-bold mb-6">My Contributions</h1> |
| 233 | + |
| 234 | + {/* Summary Section */} |
| 235 | + {isLoadingStats ? ( |
| 236 | + <LoadingState type="stats" /> |
| 237 | + ) : stats ? ( |
| 238 | + <ContributionStatsComponent stats={stats} /> |
| 239 | + ) : null} |
| 240 | + |
| 241 | + {/* Tabs and Filters */} |
| 242 | + <ContributionFilters |
| 243 | + activeTab={activeTab} |
| 244 | + setActiveTab={setActiveTab} |
| 245 | + searchQuery={searchQuery} |
| 246 | + setSearchQuery={setSearchQuery} |
| 247 | + sortOption={sortOption} |
| 248 | + setSortOption={setSortOption} |
| 249 | + categoryFilter={categoryFilter} |
| 250 | + setCategoryFilter={setCategoryFilter} |
| 251 | + categories={categories} |
| 252 | + /> |
| 253 | + |
| 254 | + {/* Active Contributions Section */} |
| 255 | + {(activeTab === "all" || activeTab === "votes") && ( |
| 256 | + <> |
| 257 | + <h2 className="text-2xl font-semibold mb-4">Ongoing Contributions</h2> |
| 258 | + {isLoadingActive ? ( |
| 259 | + <LoadingState type="cards" count={3} /> |
| 260 | + ) : ( |
| 261 | + <ActiveContributions |
| 262 | + projects={sortedActiveProjects} |
| 263 | + navigateToProject={navigateToProject} |
| 264 | + /> |
| 265 | + )} |
| 266 | + </> |
| 267 | + )} |
| 268 | + |
| 269 | + {/* Past Contributions Section */} |
| 270 | + {(activeTab === "all" || activeTab === "votes") && ( |
| 271 | + <> |
| 272 | + <h2 className="text-2xl font-semibold mb-4">Past Contributions</h2> |
| 273 | + {isLoadingPast ? ( |
| 274 | + <LoadingState type="table" count={4} /> |
| 275 | + ) : ( |
| 276 | + <PastContributions |
| 277 | + projects={sortedPastProjects} |
| 278 | + navigateToProject={navigateToProject} |
| 279 | + /> |
| 280 | + )} |
| 281 | + </> |
| 282 | + )} |
| 283 | + |
| 284 | + {/* Comments Section */} |
| 285 | + {(activeTab === "all" || activeTab === "comments") && ( |
| 286 | + <> |
| 287 | + <h2 className="text-2xl font-semibold mb-4">My Comments</h2> |
| 288 | + {isLoadingComments ? ( |
| 289 | + <LoadingState type="comments" count={3} /> |
| 290 | + ) : ( |
| 291 | + <UserComments |
| 292 | + comments={sortedComments} |
| 293 | + navigateToProject={navigateToProject} |
| 294 | + handleEditComment={handleEditComment} |
| 295 | + handleDeleteComment={handleDeleteComment} |
| 296 | + /> |
| 297 | + )} |
| 298 | + </> |
| 299 | + )} |
| 300 | + |
| 301 | + {/* Call-to-Action Section */} |
| 302 | + <CallToAction /> |
| 303 | + |
| 304 | + {/* Modals */} |
| 305 | + <CommentEditModal |
| 306 | + comment={commentToEdit} |
| 307 | + isOpen={!!commentToEdit} |
| 308 | + onClose={() => setCommentToEdit(null)} |
| 309 | + onSave={saveEditedComment} |
| 310 | + /> |
| 311 | + |
| 312 | + <DeleteConfirmationDialog |
| 313 | + isOpen={!!commentToDelete} |
| 314 | + onClose={() => setCommentToDelete(null)} |
| 315 | + onConfirm={confirmDeleteComment} |
| 316 | + /> |
| 317 | + </div> |
| 318 | + ); |
| 319 | +} |
0 commit comments