Skip to content

Commit f0be063

Browse files
authored
Ft article implementation 109 (#113)
#### What does this PR do? This PR implements the article management and display features on our platform. It includes fetching articles from Supabase, filtering by category, displaying detailed article views, implementing bookmarking and tracking visited articles, sorting by newest and popularity, and displaying trending articles. #### Description of Task to be completed? - Implemented API calls to fetch articles from Supabase. - Created components for displaying all articles with pagination. - Developed filters for categories to allow seamless filtering. - Implemented detailed view of single articles with related content. - Enabled users to bookmark articles and tracked visited articles. - Added sorting options by newest and popularity metrics. - Implemented a view for displaying trending articles dynamically. #### How should this be manually tested? 1. Ensure articles are fetched correctly from Supabase API. 2. Test pagination by loading multiple pages of articles. 3. Use category filters to verify articles are displayed correctly. 4. Check detailed article view for accuracy and related content. 5. Bookmark articles and verify persistence across sessions. 6. Track visited articles and ensure the list updates as expected. 7. Test sorting options by verifying order of articles. 8. Verify the display of trending articles based on engagement metrics. #### Any background context you want to provide? This PR builds upon our existing content management system, enhancing the user experience by providing robust article management features. It integrates closely with our backend services and ensures data consistency and reliability. #### What are the relevant pivotal tracker/Trello stories? - #37 - #81 #### Questions: Any feedback or questions regarding the implementation are welcome!
2 parents cc328fa + ce99ab3 commit f0be063

File tree

7 files changed

+788
-254
lines changed

7 files changed

+788
-254
lines changed

app/articles/ArticlesDetails.tsx

Lines changed: 252 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,230 @@
11
import { useNavigation, useLocalSearchParams, router } from "expo-router";
2-
import React from "react";
2+
import React, { useEffect, useState } from "react";
33
import {
44
View,
55
Text,
66
Image,
77
SafeAreaView,
88
ScrollView,
99
TouchableOpacity,
10+
Share,
1011
} from "react-native";
12+
import { supabase } from "../supabase";
1113

12-
import { articles } from "@/app/constants/articlesDummy";
14+
interface Article {
15+
id: string;
16+
title: string;
17+
image: string;
18+
category: string;
19+
createdAt: string;
20+
content: string[];
21+
author: string;
22+
views: number;
23+
}
1324

1425
export default function ArticlesDetails() {
1526
const navigation = useNavigation();
16-
const { id } = useLocalSearchParams();
27+
const { id } = useLocalSearchParams<{ id: string }>();
28+
const [loading, setLoading] = useState<boolean>(true);
29+
const [article, setArticle] = useState<Article | null>(null);
30+
const [error, setError] = useState<string | null>(null);
31+
const [bookmarked, setBookmarked] = useState<boolean>(false);
32+
const [userId, setUserId] = useState<string | null>(null);
33+
34+
useEffect(() => {
35+
fetchUser();
36+
}, []);
37+
38+
useEffect(() => {
39+
if (article && userId) {
40+
incrementViews(article, userId);
41+
}
42+
}, [article, userId]);
43+
44+
useEffect(() => {
45+
if (id && userId) {
46+
fetchArticle();
47+
} else if (!id) {
48+
setLoading(false);
49+
setError("No article ID provided");
50+
}
51+
}, [id, userId]);
52+
53+
const fetchUser = async () => {
54+
const {
55+
data: { user },
56+
} = await supabase.auth.getUser();
57+
if (user) {
58+
setUserId(user.id);
59+
} else {
60+
setError("User not logged in");
61+
setLoading(false);
62+
}
63+
};
64+
65+
const fetchArticle = async () => {
66+
setLoading(true);
67+
setError(null);
68+
69+
const { data: article, error } = await supabase
70+
.from("articles")
71+
.select("*")
72+
.eq("id", id)
73+
.single();
74+
75+
if (error) {
76+
console.error("Error fetching article:", error);
77+
setError("Failed to load article.");
78+
setArticle(null);
79+
} else if (article) {
80+
setArticle({
81+
...article,
82+
content: [
83+
article.content__001,
84+
article.content__002,
85+
article.content__003,
86+
].filter(Boolean),
87+
});
88+
checkIfBookmarked(article.id);
89+
} else {
90+
setArticle(null);
91+
}
92+
setLoading(false);
93+
};
94+
95+
const checkIfBookmarked = async (articleId: string) => {
96+
const { data, error } = await supabase
97+
.from("bookmarks")
98+
.select("*")
99+
.eq("user_id", userId)
100+
.eq("article_id", articleId)
101+
.single();
102+
103+
if (error) {
104+
if (error.code === "PGRST116") {
105+
setBookmarked(false);
106+
} else {
107+
console.log("Error checking bookmark:", error);
108+
}
109+
} else if (data) {
110+
setBookmarked(true);
111+
} else {
112+
setBookmarked(false);
113+
}
114+
};
115+
116+
const toggleBookmark = async () => {
117+
if (article) {
118+
const isBookmarked = !bookmarked;
119+
console.log(
120+
`Toggling bookmark for article ID: ${article.id}, new status: ${isBookmarked}`.toUpperCase()
121+
);
122+
123+
if (isBookmarked) {
124+
const { data, error } = await supabase
125+
.from("bookmarks")
126+
.insert([{ user_id: userId, article_id: article.id }]);
127+
128+
if (error) {
129+
console.error("Error adding bookmark:", error);
130+
} else {
131+
console.log("Bookmark added successfully", data);
132+
}
133+
} else {
134+
const { data, error } = await supabase
135+
.from("bookmarks")
136+
.delete()
137+
.eq("user_id", userId)
138+
.eq("article_id", article.id);
139+
140+
if (error) {
141+
console.error("Error removing bookmark:", error);
142+
} else {
143+
console.log("Bookmark removed successfully", data);
144+
}
145+
}
17146

18-
const article = articles.find((article) => article.id === Number(id));
147+
setBookmarked(isBookmarked);
148+
}
149+
};
150+
151+
const incrementViews = async (
152+
article: Article,
153+
userId: string
154+
): Promise<void> => {
155+
if (!article?.id || !userId) {
156+
throw new Error("User ID or Article ID is undefined");
157+
}
158+
159+
try {
160+
// Check if the user has already viewed the article
161+
const { data: existingView, error: viewError } = await supabase
162+
.from("views")
163+
.select("*")
164+
.eq("user_id", userId)
165+
.eq("article_id", article.id)
166+
.single();
167+
168+
if (viewError && viewError.code !== "PGRST116") {
169+
// PGRST116: No rows returned
170+
throw viewError;
171+
}
172+
173+
if (!existingView) {
174+
// User has not viewed the article, so increment the views
175+
const { data, error } = await supabase.rpc("increment_views", {
176+
article_id: article.id,
177+
user_id: userId,
178+
});
179+
180+
if (error) {
181+
console.error("Error incrementing views:", error.message);
182+
} else {
183+
console.log("Views incremented successfully", data);
184+
}
185+
} else {
186+
console.log("User already viewed this article");
187+
}
188+
} catch (error) {
189+
console.error("Error incrementing views:", error);
190+
}
191+
};
192+
193+
const shareArticle = () => {
194+
if (article) {
195+
Share.share({
196+
message: `Check out this article: ${article.title}`,
197+
url: article.image,
198+
title: article.title,
199+
})
200+
.then((result) => console.log(result))
201+
.catch((error) => console.log(error));
202+
}
203+
};
204+
205+
const more = () => {
206+
console.log("more is clicked");
207+
};
208+
209+
if (loading) {
210+
return (
211+
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
212+
<Text
213+
style={{ fontSize: 20, fontFamily: "UrbanistBold", color: "#212121" }}
214+
>
215+
Loading...
216+
</Text>
217+
</View>
218+
);
219+
}
19220

20221
return (
21-
<SafeAreaView style={{ paddingTop: 50, backgroundColor: "white",flex:1 }}>
222+
<SafeAreaView style={{ paddingTop: 50, backgroundColor: "white", flex: 1 }}>
22223
<View
23224
style={{
24225
display: "flex",
25226
flexDirection: "row",
26227
justifyContent: "space-between",
27-
28228
}}
29229
>
30230
<View
@@ -35,9 +235,7 @@ export default function ArticlesDetails() {
35235
padding: 15,
36236
}}
37237
>
38-
<TouchableOpacity
39-
onPress={() => router.back()}
40-
>
238+
<TouchableOpacity onPress={() => router.back()}>
41239
<Image
42240
style={{ marginTop: 10 }}
43241
source={require("../../assets/articlesImages/vuuu.png")}
@@ -52,19 +250,23 @@ export default function ArticlesDetails() {
52250
padding: 15,
53251
}}
54252
>
55-
<TouchableOpacity>
253+
<TouchableOpacity onPress={toggleBookmark}>
56254
<Image
57255
style={{ height: 22.57, width: 18.13, padding: 10 }}
58-
source={require("../../assets/articlesImages/rwanda.png")}
256+
source={
257+
bookmarked
258+
? require("../../assets/articlesImages/Bookmarked.png")
259+
: require("../../assets/articlesImages/rwanda.png")
260+
}
59261
/>
60262
</TouchableOpacity>
61-
<TouchableOpacity>
263+
<TouchableOpacity onPress={shareArticle}>
62264
<Image
63265
style={{ height: 11.07, width: 11.07, padding: 10 }}
64266
source={require("../../assets/articlesImages/rwiza.png")}
65267
/>
66268
</TouchableOpacity>
67-
<TouchableOpacity>
269+
<TouchableOpacity onPress={more}>
68270
<Image
69271
style={{ height: 1.17, width: 1.17, padding: 10 }}
70272
source={require("../../assets/articlesImages/Group.png")}
@@ -82,19 +284,17 @@ export default function ArticlesDetails() {
82284
gap: 5,
83285
}}
84286
>
85-
<TouchableOpacity>
86-
{article?.image && (
87-
<Image
88-
style={{
89-
height: 240,
90-
width: "100%",
91-
borderRadius: 24,
92-
justifyContent: "center",
93-
}}
94-
source={article?.image}
95-
/>
96-
)}
97-
</TouchableOpacity>
287+
{article?.image && (
288+
<Image
289+
style={{
290+
height: 240,
291+
width: "100%",
292+
borderRadius: 24,
293+
justifyContent: "center",
294+
}}
295+
source={{ uri: article.image }}
296+
/>
297+
)}
98298
<View>
99299
<Text
100300
style={{
@@ -115,17 +315,24 @@ export default function ArticlesDetails() {
115315
padding: 15,
116316
gap: 10,
117317
marginTop: 5,
318+
justifyContent: "space-between",
118319
}}
119320
>
120321
<Text
121322
style={{
122-
color: "#424242",
323+
color: "#246BFD",
123324
fontSize: 10,
124-
marginTop: 10,
325+
backgroundColor: "#E0E7FF",
326+
borderRadius: 6,
327+
height: 24,
328+
width: 59,
329+
textAlign: "center",
330+
padding: 5,
331+
marginTop: 5,
125332
fontFamily: "UrbanistRegular",
126333
}}
127334
>
128-
{article?.date}
335+
{article?.category}
129336
</Text>
130337
<Text
131338
style={{
@@ -134,31 +341,31 @@ export default function ArticlesDetails() {
134341
backgroundColor: "#E0E7FF",
135342
borderRadius: 6,
136343
height: 24,
137-
width: 59,
344+
width: "auto",
138345
textAlign: "center",
139346
padding: 5,
140347
marginTop: 5,
141348
fontFamily: "UrbanistRegular",
142349
}}
143350
>
144-
{article?.category}
351+
{article?.author}
145352
</Text>
146353
</View>
147354
</View>
148-
{article?.content.map((content, index) => (
149-
<Text
150-
key={index}
151-
style={{
152-
fontSize: 16,
153-
color: "#424242",
154-
fontFamily: "UrbanistRegular",
155-
marginBottom: 20,
156-
paddingHorizontal: 10,
157-
}}
158-
>
159-
{content}
160-
</Text>
161-
))}
355+
{article?.content.map((section, index) => (
356+
<Text
357+
key={index}
358+
style={{
359+
fontSize: 16,
360+
color: "#424242",
361+
fontFamily: "UrbanistRegular",
362+
marginBottom: 20,
363+
paddingHorizontal: 10,
364+
}}
365+
>
366+
{section}
367+
</Text>
368+
))}
162369
</View>
163370
</ScrollView>
164371
</SafeAreaView>

0 commit comments

Comments
 (0)