diff --git a/enjoy/src/api/client.ts b/enjoy/src/api/client.ts index 3c1b41ff..3184810b 100644 --- a/enjoy/src/api/client.ts +++ b/enjoy/src/api/client.ts @@ -92,7 +92,71 @@ export class Client { return this.api.get("/api/users/rankings", { params: { range } }); } - posts(params?: { page?: number; items?: number }): Promise< + users(filter: "following" | "followers" = "followers"): Promise< + { + users: UserType[]; + } & PagyResponseType + > { + return this.api.get("/api/users", { params: { filter } }); + } + + user(id: string): Promise { + return this.api.get(`/api/users/${id}`); + } + + userFollowing( + id: string, + options: { page: number } + ): Promise< + { + users: UserType[]; + } & PagyResponseType + > { + return this.api.get(`/api/users/${id}/following`, { + params: decamelizeKeys(options), + }); + } + + userFollowers( + id: string, + options: { page: number } + ): Promise< + { + users: UserType[]; + } & PagyResponseType + > { + return this.api.get(`/api/users/${id}/followers`, { + params: decamelizeKeys(options), + }); + } + + follow(id: string): Promise< + { + user: UserType; + } & { + following: boolean; + } + > { + return this.api.post(`/api/users/${id}/follow`); + } + + unfollow(id: string): Promise< + { + user: UserType; + } & { + following: boolean; + } + > { + return this.api.post(`/api/users/${id}/unfollow`); + } + + posts(params?: { + page?: number; + items?: number; + userId?: string; + type?: "all" | "recording" | "medium" | "story" | "prompt" | "text" | "gpt"; + by?: "following" | "all"; + }): Promise< { posts: PostType[]; } & PagyResponseType diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 85944cec..f2010637 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -514,5 +514,18 @@ "reAnalyze": "re-analyze", "AiDictionary": "AI dictionary", "AiTranslate": "AI translate", - "cambridgeDictionary": "Cambridge dictionary" + "cambridgeDictionary": "Cambridge dictionary", + "following": "following", + "followers": "followers", + "allUsers": "all users", + "allTypes": "all types", + "recordingType": "Recording", + "mediumType": "Audio/Video", + "storyType": "Story", + "promptType": "Prompt", + "gptType": "GPT", + "follow": "follow", + "unfollow": "unfollow", + "noFollowersYet": "No followers yet", + "notFollowingAnyoneYet": "Not following anyone yet" } diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index 6fb6f325..598c0f9a 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -513,5 +513,18 @@ "reAnalyze": "重新分析", "AiDictionary": "智能词典", "AiTranslate": "智能翻译", - "cambridgeDictionary": "剑桥词典" + "cambridgeDictionary": "剑桥词典", + "following": "关注中", + "followers": "被关注", + "allUsers": "所有用户", + "allTypes": "所有类型", + "recordingType": "录音", + "mediumType": "音视频", + "storyType": "文章", + "promptType": "提示语", + "gptType": "智能助手", + "follow": "关注", + "unfollow": "取消关注", + "noFollowersYet": "还没有人关注", + "notFollowingAnyoneYet": "还没有关注任何人" } diff --git a/enjoy/src/renderer/components/posts/post-card.tsx b/enjoy/src/renderer/components/posts/post-card.tsx index 3e44427b..2eb499e6 100644 --- a/enjoy/src/renderer/components/posts/post-card.tsx +++ b/enjoy/src/renderer/components/posts/post-card.tsx @@ -11,6 +11,7 @@ import { Avatar, AvatarImage, AvatarFallback } from "@renderer/components/ui"; import { formatDateTime } from "@renderer/lib/utils"; import { t } from "i18next"; import Markdown from "react-markdown"; +import { Link } from "react-router-dom"; export const PostCard = (props: { post: PostType; @@ -23,12 +24,14 @@ export const PostCard = (props: {
- - - - {post.user.name[0].toUpperCase()} - - + + + + + {post.user.name[0].toUpperCase()} + + +
{post.user.name}
diff --git a/enjoy/src/renderer/components/posts/posts.tsx b/enjoy/src/renderer/components/posts/posts.tsx index a7fa8b5d..f2c2a395 100644 --- a/enjoy/src/renderer/components/posts/posts.tsx +++ b/enjoy/src/renderer/components/posts/posts.tsx @@ -1,12 +1,15 @@ import { useContext, useEffect, useState } from "react"; import { AppSettingsProviderContext } from "@renderer/context"; import { PostCard, LoaderSpin } from "@renderer/components"; -import { toast, Button } from "@renderer/components//ui"; +import { toast, Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@renderer/components//ui"; import { t } from "i18next"; -export const Posts = () => { +export const Posts = (props: { userId?: string }) => { + const { userId } = props; const { webApi } = useContext(AppSettingsProviderContext); const [loading, setLoading] = useState(true); + const [type, setType] = useState<'all' | 'recording' | 'medium' | 'story' | 'prompt' | 'gpt'>("all"); + const [by, setBy] = useState<'all' | 'following'>("following"); const [posts, setPosts] = useState([]); const [nextPage, setNextPage] = useState(1); @@ -29,9 +32,16 @@ export const Posts = () => { .posts({ page, items: 10, + userId, + by, + type }) .then((res) => { - setPosts([...posts, ...res.posts]); + if (page === 1) { + setPosts(res.posts); + } else { + setPosts([...posts, ...res.posts]); + } setNextPage(res.next); }) .catch((err) => { @@ -43,8 +53,8 @@ export const Posts = () => { }; useEffect(() => { - fetchPosts(); - }, []); + fetchPosts(1); + }, [type, by]); if (loading) { return ; @@ -52,8 +62,36 @@ export const Posts = () => { return (
+
+ { + !userId && + } + + +
+ {posts.length === 0 && ( -
{t("noOneSharedYet")}
+
{t("noOneSharedYet")}
)}
diff --git a/enjoy/src/renderer/pages/user.tsx b/enjoy/src/renderer/pages/user.tsx new file mode 100644 index 00000000..bd35eb60 --- /dev/null +++ b/enjoy/src/renderer/pages/user.tsx @@ -0,0 +1,218 @@ +import { useContext, useEffect, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { AppSettingsProviderContext } from "../context"; +import { LoaderSpin, Posts } from "@renderer/components"; +import { ChevronLeftIcon } from "lucide-react"; +import { t } from "i18next"; +import { Avatar, AvatarFallback, AvatarImage, Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@renderer/components/ui"; + +export default () => { + const { id } = useParams<{ id: string }>(); + const [user, setUser] = useState(null); + const { webApi, user: currentUser } = useContext(AppSettingsProviderContext); + const navigate = useNavigate(); + + const fetchUser = async () => { + if (!id) return; + + webApi.user(id).then((user) => { + setUser(user); + }); + } + + const follow = () => { + webApi.follow(id).then(() => { + setUser({ ...user, following: true }); + }); + } + + const unfollow = () => { + webApi.unfollow(id).then(() => { + setUser({ ...user, following: false }); + }); + } + + useEffect(() => { + fetchUser(); + }, [id]); + + if (!user) return ; + + return ( +
+
+
+ + {user.name} +
+ +
+
+ + + + {user.name[0].toUpperCase()} + + +
+ + { + currentUser.id != user.id &&
{ + user.following ? : + }
+ } +
+ +
+ +
+ + {t('activities')} + {t('followers')} + {t('following')} + +
+ + + + + + +
+
+
+
+ ) +} + +const UserFollowers = (props: { id: string }) => { + const { id } = props; + const [users, setUsers] = useState([]); + const { webApi } = useContext(AppSettingsProviderContext); + const [page, setPage] = useState(1); + + const fetchFollowers = async () => { + if (!page) return; + + webApi.userFollowers(id, { page }).then((res) => { + setUsers(res.users); + setPage(res.next); + }); + } + + useEffect(() => { + fetchFollowers(); + return () => { + setUsers([]); + setPage(1); + } + }, [id]); + + if (users.length === 0) return
{t("noFollowersYet")}
; + + return ( + <> +
+ {users.map((user) => ( + + ))} +
+ { + page && (
+ +
) + } + + ) +} + +const UserFollowing = (props: { id: string }) => { + const { id } = props; + const [users, setUsers] = useState([]); + const { webApi } = useContext(AppSettingsProviderContext); + const [page, setPage] = useState(1); + + const fetchFollowing = () => { + if (!page) return; + + webApi.userFollowing(id, { page }).then((res) => { + setUsers(res.users); + setPage(res.next); + }); + } + + useEffect(() => { + fetchFollowing(); + return () => { + setUsers([]); + setPage(1); + } + }, [id]); + + if (users.length === 0) return
{t("notFollowingAnyoneYet")}
; + + return ( + <> +
+ {users.map((user) => ( + + ))} +
+ { + page && (
+ +
) + } + + ) +} + +const UserCard = ({ user }: { user: UserType }) => { + const { webApi, user: currentUser } = useContext(AppSettingsProviderContext); + const [following, setFollowing] = useState(user.following); + + const handleFollow = () => { + if (following) { + webApi.unfollow(user.id).then(() => { + setFollowing(false); + }); + } else { + webApi.follow(user.id).then(() => { + setFollowing(true); + }); + } + }; + + return ( +
+
+ + + + + {user.name[0].toUpperCase()} + + + +
+
{user.name}
+
@{user.id}
+
+
+ +
+ { + currentUser.id != user.id && + } +
+
+ ); +} \ No newline at end of file diff --git a/enjoy/src/renderer/router.tsx b/enjoy/src/renderer/router.tsx index de9e900a..d6078a51 100644 --- a/enjoy/src/renderer/router.tsx +++ b/enjoy/src/renderer/router.tsx @@ -13,6 +13,7 @@ import Stories from "./pages/stories"; import Story from "./pages/story"; import Books from "./pages/books"; import Profile from "./pages/profile"; +import User from "./pages/user"; import Home from "./pages/home"; import Community from "./pages/community"; import StoryPreview from "./pages/story-preview"; @@ -28,6 +29,10 @@ export default createHashRouter([ path: "/community", element: , }, + { + path: "/users/:id", + element: , + }, { path: "/profile", element: , diff --git a/enjoy/src/types/user.d.ts b/enjoy/src/types/user.d.ts index 4ebca7d8..5c24cfd2 100644 --- a/enjoy/src/types/user.d.ts +++ b/enjoy/src/types/user.d.ts @@ -7,5 +7,6 @@ type UserType = { recordingsCount?: number; recordingsDuration?: number; hasMixin?: boolean; + following?: boolean; createdAt?: string; };