Skip to content

Commit 6f76ebb

Browse files
authored
Implement contribution page (boundlessfi#43)
* fix: callback urls for google auth and github auth * feat: implement voting mechanism * feat: implement voting mechanism * feat: implement voting mechanism * refactor: refactor dashboard * refactor: refactor dashboard * refactor: refactor dashboard * refactor: refactor dashboard * implement voting mechanism * Implement contribution page * fix conflict
1 parent df2d23d commit 6f76ebb

26 files changed

Lines changed: 2540 additions & 21 deletions

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ GITHUB_ID="YOUR_GITHUB_CLIENT_ID" #neccesary
3232
GITHUB_SECRET="YOUR_GITHUB_CLIENT_SECRET" #neccesary
3333

3434

35-
PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
36-
PUBLIC_STELLAR_RPC_URL="https://soroban-testnet.stellar.org:443"
35+
STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
36+
STELLAR_RPC_URL="https://soroban-testnet.stellar.org:443"
3737
STELLAR_ACCOUNT="collins"
3838
STELLAR_NETWORK="testnet"
3939

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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

Comments
 (0)