Skip to content

Commit

Permalink
Merge pull request #668 from bounswe/feature/650-follow-tag
Browse files Browse the repository at this point in the history
[Mobile] Follow/Unfollow Tag
  • Loading branch information
atakanyasar authored Dec 16, 2024
2 parents 9e0cf7a + b1b4606 commit 9b4009d
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 40 deletions.
4 changes: 2 additions & 2 deletions mobile/app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ErrorAlert from "@/components/ErrorAlert";
import FollowButton from "@/components/FollowButton";
import FollowUserButton from "@/components/FollowUserButton";
import { FullscreenLoading } from "@/components/FullscreenLoading";
import { QuestionCard } from "@/components/QuestionCard";
import {
Expand Down Expand Up @@ -254,7 +254,7 @@ export function UserProfile({ userId }: { userId: string }) {
</Button>
)
) : (
data?.data && <FollowButton profile={data?.data} />
data?.data && <FollowUserButton profile={data?.data} />
)}
</VStack>
{/*
Expand Down
4 changes: 2 additions & 2 deletions mobile/app/question/[questionId].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Answers } from "@/components/Answers";

import ErrorAlert from "@/components/ErrorAlert";
import FollowButton from "@/components/FollowButton";
import FollowUserButton from "@/components/FollowUserButton";
import { FullscreenLoading } from "@/components/FullscreenLoading";
import {
Button,
Expand Down Expand Up @@ -271,7 +271,7 @@ export default function QuestionPage() {
</Link>
</HStack>
{token && selfProfile?.id !== question.author.id && (
<FollowButton profile={question.author} />
<FollowUserButton profile={question.author} />
)}
</HStack>

Expand Down
39 changes: 25 additions & 14 deletions mobile/app/tags/[tagId].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ErrorAlert from "@/components/ErrorAlert";
import FollowTagButton from "@/components/FollowTagButton";
import { FullscreenLoading } from "@/components/FullscreenLoading";
import { QuestionList } from "@/components/QuestionsList";
import {
Expand Down Expand Up @@ -30,13 +31,14 @@ import useAuthStore from "@/services/auth";
import { Link, router, useLocalSearchParams } from "expo-router";
import { ArrowLeftIcon, ChevronDownIcon, Plus } from "lucide-react-native";
import { useState } from "react";
import { SvgUri } from "react-native-svg";

export default function TagPage() {
const { tagId } = useLocalSearchParams<{ tagId: string }>();

const { data, isLoading, error } = useGetTagDetails(
{
pathParams: { tagId: tagId! },
pathParams: { tagId: Number(tagId) },
},
{
enabled: !!tagId,
Expand Down Expand Up @@ -68,34 +70,43 @@ export default function TagPage() {
}

return (
<VStack className="flex-1 px-2 my-8 mt-8">
<VStack className="flex-1 px-2 my-8">
<HStack className="flex items-center justify-between">
<Button
onPress={() => router.back()}
className="self-start mt-4 ml-2"
className="self-start mt-12 ml-2"
variant={"outline"}
size="sm"
>
<Icon as={ArrowLeftIcon} />
</Button>
</HStack>
<ScrollView contentContainerStyle={{ paddingVertical: 32, flexGrow: 1 }}>
<View className="flex items-start px-4">
{tag?.logoImage && tag.logoImage.endsWith(".svg") && (
<SvgUri
uri={tag?.logoImage}
viewBox="0 0 480 240"
width="120px"
height= "90px"
/>
)}

{tag?.logoImage && tag.logoImage.endsWith(".png") && (
<Image
source={{ uri: tag?.logoImage || "https://placehold.co/400x300" }}
style={{ width: 400, height: 300 }}
/>
)}
</View>

<View style={{ paddingHorizontal: 16, gap: 16 }}>
<HStack
style={{ alignItems: "center", justifyContent: "space-between" }}
>
<Text className="text-2xl font-bold">{tag.name}</Text>
<FollowTagButton tag={{id: Number(tagId), selfFollowing: tag.following}} />
</HStack>
<Image
source={{ uri: tag?.logoImage || "https://placehold.co/400x300" }}
alt={tag.name}
style={{
height: 192,
width: "100%",
borderRadius: 24,
resizeMode: "contain",
}}
/>
<Text>{tag.description}</Text>

<VStack style={{ marginTop: 16, gap: 16 }}>
Expand Down Expand Up @@ -155,7 +166,7 @@ export default function TagPage() {

<QuestionList
searchQueryParams=""
tagFilter={tag.tagId}
tagFilter={tag.tagId.toString()}
{...(difficultyFilter ? { difficultyFilter } : {})}
sortBy={tab === "top-rated" ? "TOP_RATED" : "RECENT"}
/>
Expand Down
94 changes: 94 additions & 0 deletions mobile/components/FollowTagButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
useFollowTag,
useGetTagDetails,
useUnfollowTag,
} from "@/services/api/programmingForumComponents";
import { useEffect, useState } from "react";
import { Button, ButtonText } from "./ui/button";

export default function FollowTagButton({
tag,
}: {
tag: { id?: number; selfFollowing?: boolean };
}) {
const { isLoading, data, error, refetch } = useGetTagDetails(
{
pathParams: {
tagId: tag.id!,
},
},
);


const [optimisticFollowing, setOptimisticFollowing] = useState(
false
);

useEffect(() => {
setOptimisticFollowing(tag.selfFollowing ?? false);
}, [tag.selfFollowing]);

const { mutateAsync: follow } = useFollowTag({
onMutate: async () => {
setOptimisticFollowing(true);
},
onSuccess: (data) => {
refetch().then(() => {
setOptimisticFollowing(true);
});
console.log(data);
},
onError: (error) => {
console.error(error);
setOptimisticFollowing(false);
},
});

const { mutateAsync: unfollow } = useUnfollowTag({
onMutate: async () => {
setOptimisticFollowing(false);
},
onSuccess: () => {
refetch().then(() => {
setOptimisticFollowing(false);
});
},
onError: () => {
setOptimisticFollowing(true);
},
});

const following = optimisticFollowing ?? data?.data?.following;

return (
<Button
disabled={!!error || isLoading}
variant={following && !isLoading ? "outline" : "solid"}
onPress={() => {
if (following) {
unfollow({
pathParams: {
tagId: tag.id!,
},
});
} else {
follow({
pathParams: {
tagId: tag.id!,
},
});
}
}}
>
<ButtonText>
{isLoading
? "Loading..."
: error
? "Error"
: following
? "Following"
: "Follow"}
</ButtonText>
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { useState } from "react";
import { Button, ButtonText } from "./ui/button";

export default function FollowButton({
export default function FollowUserButton({
profile,
}: {
profile: { id?: number; selfFollowing?: boolean };
Expand Down
4 changes: 2 additions & 2 deletions mobile/components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserSummary } from "@/services/api/programmingForumSchemas";
import useAuthStore from "@/services/auth";
import { Link } from "expo-router";
import FollowButton from "./FollowButton";
import FollowUserButton from "./FollowUserButton";
import { Text, View } from "./ui";

interface ProfileProps {
Expand All @@ -18,7 +18,7 @@ export const Profile = ({ profile }: ProfileProps) => {
{profile.username}
</Text>
</Link>
{profile.id !== selfProfile?.id && <FollowButton profile={profile} />}
{profile.id !== selfProfile?.id && <FollowUserButton profile={profile} />}
</View>
);
};
6 changes: 3 additions & 3 deletions mobile/services/api/programmingForumComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1787,7 +1787,7 @@ export const useCreateTag = (
};

export type GetTagDetailsPathParams = {
tagId: string;
tagId: number;
};

export type GetTagDetailsError = Fetcher.ErrorWrapper<{
Expand Down Expand Up @@ -1850,7 +1850,7 @@ export const useGetTagDetails = <TData = GetTagDetailsResponse,>(
};

export type FollowTagPathParams = {
tagId: string;
tagId: number;
};

export type FollowTagError = Fetcher.ErrorWrapper<
Expand Down Expand Up @@ -1900,7 +1900,7 @@ export const useFollowTag = (
};

export type UnfollowTagPathParams = {
tagId: string;
tagId: number;
};

export type UnfollowTagError = Fetcher.ErrorWrapper<
Expand Down
2 changes: 1 addition & 1 deletion mobile/services/api/programmingForumFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const baseUrl =
process.env.NODE_ENV === "development"
? `http://${new URL(Constants.default.experienceUrl).hostname}:5173/api/v1`
:
"https://programming-languages-forum-ahwzj.ondigitalocean.app/api/v1";
"https://programming-languages-forum-psrb6.ondigitalocean.app/api/v1";

console.log(baseUrl);

Expand Down
12 changes: 6 additions & 6 deletions mobile/services/api/programmingForumSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,13 @@ export type AnswerDetails = {
};

/**
* @example {"tagId":"python","name":"Python","description":"Python is a programming language.","questionCount":100,"followersCount":1000,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png","authors":["Guido van Rossum"],"inceptionYear":"1991","fileExtension":".py","officialWebsite":"https://www.python.org","stackExchangeTag":"python"}
* @example {"tagId":"java","name":"Java","type":"PROGRAMMING_LANGUAGE","description":"Java is a class-based, object-oriented programming language.","questionCount":200,"followersCount":800,"following":true,"photo":"https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/182px-Java_programming_language_logo.svg.png","authors":["James Gosling"],"inceptionYear":"1995","fileExtension":".java","officialWebsite":"https://www.java.com","stackExchangeTag":"java"}
* @example {"tagId":"react","name":"React","type":"SOFTWARE_LIBRARY","description":"React is a JavaScript library for building user interfaces.","questionCount":150,"followersCount":600,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/180px-React-icon.svg.png","officialWebsite":"https://reactjs.org","stackExchangeTag":"reactjs"}
* @example {"tagId":"oop","name":"Object-Oriented Programming","type":"PROGRAMMING_PARADIGM","description":"OOP is a programming paradigm based on objects containing data and code.","questionCount":80,"followersCount":400,"following":true,"photo":"https://example.com/oop-icon.png","stackExchangeTag":"oop"}
* @example {"tagId":1,"name":"Python","description":"Python is a programming language.","questionCount":100,"followersCount":1000,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png","authors":["Guido van Rossum"],"inceptionYear":"1991","fileExtension":".py","officialWebsite":"https://www.python.org","stackExchangeTag":"python"}
* @example {"tagId":2,"name":"Java","type":"PROGRAMMING_LANGUAGE","description":"Java is a class-based, object-oriented programming language.","questionCount":200,"followersCount":800,"following":true,"photo":"https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/182px-Java_programming_language_logo.svg.png","authors":["James Gosling"],"inceptionYear":"1995","fileExtension":".java","officialWebsite":"https://www.java.com","stackExchangeTag":"java"}
* @example {"tagId":3,"name":"React","type":"SOFTWARE_LIBRARY","description":"React is a JavaScript library for building user interfaces.","questionCount":150,"followersCount":600,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/180px-React-icon.svg.png","officialWebsite":"https://reactjs.org","stackExchangeTag":"reactjs"}
* @example {"tagId":4,"name":"Object-Oriented Programming","type":"PROGRAMMING_PARADIGM","description":"OOP is a programming paradigm based on objects containing data and code.","questionCount":80,"followersCount":400,"following":true,"photo":"https://example.com/oop-icon.png","stackExchangeTag":"oop"}
*/
export type TagDetails = {
tagId: string;
tagId: number;
name: string;
tagType?: TagType;
description: string;
Expand Down Expand Up @@ -258,7 +258,7 @@ export type TagDetails = {
};

/**
* @example {"tagId":"python","name":"Python","questionCount":100,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png"}
* @example {"tagId":1,"name":"Python","questionCount":100,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png"}
*/
export type TagSummary = {
id?: string;
Expand Down
18 changes: 9 additions & 9 deletions swagger/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,7 @@ paths:
in: path
required: true
schema:
type: string
type: integer
responses:
"200":
description: Successful response
Expand Down Expand Up @@ -873,7 +873,7 @@ paths:
in: path
required: true
schema:
type: string
type: integer
responses:
"200":
description: Successfully followed the tag
Expand All @@ -894,7 +894,7 @@ paths:
in: path
required: true
schema:
type: string
type: integer
responses:
"200":
description: Successfully unfollowed the tag
Expand Down Expand Up @@ -1622,7 +1622,7 @@ components:
- officialWebsite
properties:
tagId:
type: string
type: integer
name:
type: string
tagType:
Expand Down Expand Up @@ -1665,7 +1665,7 @@ components:
items:
$ref: "#/components/schemas/QuestionSummary"
examples:
- tagId: "python"
- tagId: 1
name: "Python"
description: "Python is a programming language."
questionCount: 100
Expand All @@ -1678,7 +1678,7 @@ components:
fileExtension: ".py"
officialWebsite: "https://www.python.org"
stackExchangeTag: "python"
- tagId: "java"
- tagId: 2
name: "Java"
type: "PROGRAMMING_LANGUAGE"
description: "Java is a class-based, object-oriented programming language."
Expand All @@ -1692,7 +1692,7 @@ components:
fileExtension: ".java"
officialWebsite: "https://www.java.com"
stackExchangeTag: "java"
- tagId: "react"
- tagId: 3
name: "React"
type: "SOFTWARE_LIBRARY"
description: "React is a JavaScript library for building user interfaces."
Expand All @@ -1702,7 +1702,7 @@ components:
photo: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/180px-React-icon.svg.png"
officialWebsite: "https://reactjs.org"
stackExchangeTag: "reactjs"
- tagId: "oop"
- tagId: 4
name: "Object-Oriented Programming"
type: "PROGRAMMING_PARADIGM"
description: "OOP is a programming paradigm based on objects containing data and code."
Expand All @@ -1727,7 +1727,7 @@ components:
type: string
format: url
examples:
- tagId: "python"
- tagId: 1
name: "Python"
questionCount: 100
photo: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png"
Expand Down

0 comments on commit 9b4009d

Please sign in to comment.