18 |
19 | setInput(e.target.value)}
23 | />
24 |
32 |
33 | {!query ? (
34 |
35 | Start typing to get some results
36 |
37 | ) : (
38 |
39 | )}
40 |
41 | );
42 | };
43 |
44 | export default SearchSection;
45 |
--------------------------------------------------------------------------------
/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import {
5 | AiOutlineHome,
6 | AiFillHome,
7 | AiFillBell,
8 | AiOutlineBell,
9 | } from "react-icons/ai";
10 | import { IoSearchOutline, IoSearchSharp } from "react-icons/io5";
11 | import { RiUser3Fill, RiUser3Line } from "react-icons/ri";
12 | import { BiMessageSquareAdd } from "react-icons/bi";
13 | import { usePathname, useRouter } from "next/navigation";
14 | import { useEffect, useMemo } from "react";
15 | import { UserButton, useUser } from "@clerk/nextjs";
16 | import { dark } from "@clerk/themes";
17 | import { usePostModalContext } from "@/providers/PostModalProvider";
18 |
19 | export default function Sidebar() {
20 | const { user } = useUser();
21 | const { handleOpen: handleOpenPostModal } = usePostModalContext();
22 |
23 | const location = usePathname();
24 | const router = useRouter();
25 |
26 | const handleRoute = (route: string) => router.push(route);
27 |
28 | const navs = useMemo(() => {
29 | const basePath = location?.split("/")?.[1];
30 |
31 | const routes = [
32 | {
33 | Active: AiFillHome,
34 | InActive: AiOutlineHome,
35 | title: "Home",
36 | path: "/",
37 | },
38 | {
39 | Active: IoSearchSharp,
40 | InActive: IoSearchOutline,
41 | title: "Search",
42 | path: "/search",
43 | },
44 | {
45 | Active: AiFillBell,
46 | InActive: AiOutlineBell,
47 | title: "Notifications",
48 | path: "/notifications",
49 | },
50 | {
51 | Active: RiUser3Fill,
52 | InActive: RiUser3Line,
53 | title: "Profile",
54 | path: "/profile",
55 | },
56 | ];
57 | if (location === "/") {
58 | return [
59 | {
60 | Icon: routes[0].Active,
61 | title: routes[0].title,
62 | path: routes[0].path,
63 | isActive: true,
64 | },
65 | ...routes.slice(1).map((route) => ({
66 | Icon: route.InActive,
67 | title: route.title,
68 | path: route.path,
69 | isActive: false,
70 | })),
71 | ];
72 | } else {
73 | return routes.map((route, idx) => {
74 | const isActive = idx !== 0 && `/${basePath}`.includes(route.path);
75 | return {
76 | Icon: isActive ? route.Active : route.InActive,
77 | title: route.title,
78 | path: route.path,
79 | isActive,
80 | };
81 | });
82 | }
83 | }, [location]);
84 |
85 | /**
86 | * Prefetch routes when first-time rendering this component
87 | */
88 | useEffect(() => {
89 | navs.map((nav) => {
90 | if (!nav.isActive) router.prefetch(nav.path);
91 | });
92 | // eslint-disable-next-line react-hooks/exhaustive-deps
93 | }, []);
94 |
95 | return (
96 |
74 | {displayLoaderProfile &&
}
75 | {commentOwner && (
76 |
77 |
78 |
82 |
83 | {commentOwner.login.slice(0, 1).toUpperCase()}
84 |
85 |
86 |
87 |
88 | {commentOwner.name}
89 |
90 |
@{commentOwner.login}
91 |
92 |
93 | {" "}
94 | | Replied {formatTimeAgo(comment.createdAt)}
95 |
96 |
97 | )}
98 |
105 | {comment.content}
106 |
107 | {user?.username === comment.ownerId && (
108 |
109 |
110 |
114 |
115 |
116 |
117 |
118 | Are you sure you want to delete this comment?
119 |
120 |
121 | This action cannot be undone.
122 |
123 |
124 |
125 | Cancel
126 |
127 | Continue
128 |
129 |
130 |
131 |
132 | )}
133 |
134 | );
135 | };
136 |
137 | export default CommentCard;
138 |
--------------------------------------------------------------------------------
/src/components/cards/NotificationCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { RouterOutputs, api } from "@/lib/api/client";
4 | import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
5 | import { useRouter } from "next/navigation";
6 | import AvatarSkeleton from "../skeletons/AvatarSkeleton";
7 | import { formatTimeAgo } from "@/helpers/formatTimeAgo";
8 | import CardSkeleton from "../skeletons/CardSkeleton";
9 | import RepoCard from "./RepoCard";
10 | import PostCard from "./PostCard";
11 | import CommentCard from "./CommentCard";
12 | import { convertToRepoId } from "@/helpers/repoId";
13 |
14 | interface Props {
15 | notification: RouterOutputs["notification"]["getRecents"][number];
16 | }
17 | const NotificationCard = ({ notification }: Props) => {
18 | const router = useRouter();
19 |
20 | const { isLoading: isLoadingProfile, data: profile } =
21 | api.github.otherProfile.useQuery({
22 | username: notification.originId,
23 | });
24 |
25 | /**
26 | * Post action
27 | */
28 | const displayPost = !!(
29 | notification.postAction &&
30 | notification.postAction !== "comment" &&
31 | notification.postId
32 | );
33 | const { isLoading: isLoadingPost, data: post } = api.post.postById.useQuery(
34 | {
35 | id: notification.postId!!,
36 | },
37 | {
38 | enabled: displayPost,
39 | }
40 | );
41 | const displayLoaderPost = displayPost ? isLoadingPost : false;
42 |
43 | /**
44 | * Comment action
45 | */
46 | const displayComment = !!(
47 | notification.postAction === "comment" && notification.commentId
48 | );
49 | const { isLoading: isLoadingComment, data: comment } =
50 | api.comment.commentById.useQuery(
51 | {
52 | id: notification.commentId!,
53 | },
54 | {
55 | enabled: displayComment,
56 | }
57 | );
58 | const displayLoaderComment = displayComment ? isLoadingComment : false;
59 |
60 | /**
61 | * Repos action
62 | */
63 | const displayRepo = !!(
64 | notification.githubAction &&
65 | notification.githubAction !== "follow" &&
66 | notification.repoName
67 | );
68 | const { isLoading: isLoadingRepo, data: repoShared } =
69 | api.github.getARepo.useQuery(
70 | {
71 | repoName: notification.repoName!,
72 | },
73 | {
74 | enabled: displayRepo,
75 | }
76 | );
77 | const displayLoaderRepo = displayRepo ? isLoadingRepo : false;
78 |
79 | const readProfile = () => router.push(`/users/${notification.originId}`);
80 |
81 | const goToReference = () => {
82 | const { githubAction, repoName, postAction, postId, originId } =
83 | notification;
84 | let link = "";
85 | if (githubAction) {
86 | switch (githubAction) {
87 | case "follow":
88 | link = `/users/${originId}`;
89 | break;
90 | case "share":
91 | if (postId) {
92 | link = `/posts/${postId}`;
93 | } else {
94 | link = `/repos/${convertToRepoId(repoName!)}`;
95 | }
96 | break;
97 | case "star":
98 | link = `/repos/${convertToRepoId(repoName!)}`;
99 | break;
100 | default:
101 | break;
102 | }
103 | } else if (postAction) {
104 | switch (postAction) {
105 | case "comment":
106 | link = `/posts/${postId}#comments`;
107 | break;
108 | case "like":
109 | link = `/posts/${postId}`;
110 | break;
111 | }
112 | }
113 | router.push(link);
114 | };
115 |
116 | const notificationInfo = (() => {
117 | const { githubAction, postAction } = notification;
118 | let action = "";
119 | if (githubAction === "follow") action = "started following you";
120 | if (githubAction === "share")
121 | action = "shared one of your repository in a post";
122 | if (githubAction === "star") action = "has starred one of your repository";
123 | if (postAction === "comment") action = "commented in one of your post";
124 | if (postAction === "like") action = "liked one of your post";
125 | return action;
126 | })();
127 |
128 | return (
129 |
130 | {isLoadingProfile || !profile ? (
131 |
132 | ) : (
133 |
134 |
135 |
136 |
137 | {profile.login.slice(0, 1).toUpperCase()}
138 |
139 |
140 |
141 |
145 |
@{profile.login}
146 |
147 |
151 | {notificationInfo}
152 |
153 |
154 | | {formatTimeAgo(notification.createdAt)}
155 |
156 |
157 |
158 | )}
159 | {(displayLoaderRepo || displayLoaderComment || displayLoaderPost) && (
160 |
161 | )}
162 | {displayRepo && repoShared &&
}
163 | {displayRepo && !displayLoaderRepo && !repoShared && (
164 |
165 | Repository doesn't exist
166 |
167 | )}
168 | {displayPost && post &&
}
169 | {displayPost && !displayLoaderPost && !post && (
170 |
171 | Post doesn't exist
172 |
173 | )}
174 | {displayComment && comment &&
}
175 | {displayComment && !displayLoaderComment && !comment && (
176 |
177 | Comment doesn't exist
178 |
179 | )}
180 |
181 | Posted {formatTimeAgo(notification.createdAt)}
182 |
183 |
184 | );
185 | };
186 |
187 | export default NotificationCard;
188 |
--------------------------------------------------------------------------------
/src/components/cards/RepoCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { TrimmedGitHubRepo } from "@/types/github";
4 | import Link from "next/link";
5 | import { Separator } from "../ui/separator";
6 | import {
7 | AiOutlineRetweet,
8 | AiFillStar,
9 | AiOutlineStar,
10 | AiOutlineFork,
11 | } from "react-icons/ai";
12 | import { api } from "@/lib/api/client";
13 | import { useToast } from "../ui/use-toast";
14 | import { useState } from "react";
15 | import { Badge } from "../ui/badge";
16 | import { displayNumbers } from "@/helpers/displayNumbers";
17 | import StarSkeleton from "../skeletons/StarSkeleton";
18 | import {
19 | Tooltip,
20 | TooltipContent,
21 | TooltipProvider,
22 | TooltipTrigger,
23 | } from "../ui/tooltip";
24 | import { usePostModalContext } from "@/providers/PostModalProvider";
25 | import { cn } from "@/lib/utils";
26 | import { convertToRepoId } from "@/helpers/repoId";
27 |
28 | interface Props {
29 | repo: TrimmedGitHubRepo;
30 | hideCounts?: boolean;
31 | border?: boolean;
32 | navigateToGitHub?: boolean;
33 | }
34 | const RepoCard = ({
35 | repo,
36 | hideCounts = false,
37 | border = true,
38 | navigateToGitHub = false,
39 | }: Props) => {
40 | const { handleOpen: handleOpenPostModal } = usePostModalContext();
41 |
42 | const { isLoading: isLoadingHasStarred, data: hasStarred } =
43 | api.github.hasStarredTheRepo.useQuery(
44 | {
45 | repoName: repo.full_name,
46 | },
47 | {
48 | enabled: !hideCounts,
49 | }
50 | );
51 | const { isLoading: isLoadingRepoSharedCounts, data: totalShared } =
52 | api.post.repoSharedCounts.useQuery(
53 | {
54 | repoName: repo.full_name,
55 | },
56 | {
57 | enabled: !hideCounts,
58 | }
59 | );
60 | const [starCount, setStarCount] = useState(repo.stargazers_count);
61 |
62 | const { toast } = useToast();
63 | const utils = api.useContext();
64 |
65 | const { mutate } = api.github.starAction.useMutation({
66 | onError: (err) =>
67 | toast({
68 | title: "Oh uh..",
69 | description: err.message,
70 | variant: "destructive",
71 | }),
72 | onSuccess: (res) => {
73 | setStarCount((count) => {
74 | if (hasStarred) {
75 | return count - 1;
76 | } else {
77 | return count + 1;
78 | }
79 | });
80 | utils.github.hasStarredTheRepo.invalidate({
81 | repoName: repo.full_name,
82 | });
83 | utils.github.myRepos.invalidate();
84 | utils.github.otherUserRepos.invalidate();
85 |
86 | toast({
87 | title: res.success ? "Success!" : "Oh uh..",
88 | description: res.message,
89 | variant: res.success ? "default" : "destructive",
90 | });
91 | },
92 | });
93 |
94 | const handleStar = () => {
95 | if (isLoadingHasStarred) return;
96 | const action = hasStarred ? "unstar" : "star";
97 | mutate({
98 | action,
99 | repoName: repo.full_name,
100 | });
101 | };
102 |
103 | const handleShareRepo = () => handleOpenPostModal(repo);
104 |
105 | return (
106 |
114 |
115 |
123 |
124 | {repo.full_name}
125 |
126 |
127 | {repo.fork && (
128 |
129 |
130 | This is a forked repository
131 |
132 | )}
133 |
{repo.description}
134 | {repo.topics && (
135 |
136 | {repo.topics.map((topic) => (
137 |
142 | {topic}
143 |
144 | ))}
145 |
146 | )}
147 |
148 | {!hideCounts && (
149 | <>
150 |
151 |
152 | {isLoadingRepoSharedCounts ? (
153 |
154 | ) : (
155 |
156 |
157 |
158 |
162 |
163 | {displayNumbers(totalShared ?? 0)}
164 |
165 |
166 |
167 | Share repo in a post
168 |
169 |
170 |
171 | )}
172 |
173 |
174 |
175 | {isLoadingHasStarred ? (
176 |
177 | ) : (
178 |
179 |
180 |
181 |
185 | {hasStarred ? (
186 |
187 | ) : (
188 |
189 | )}
190 | {displayNumbers(starCount)}
191 |
192 |
193 |
194 | Star/Unstar the repo
195 |
196 |
197 |
198 | )}
199 |
200 |
201 |
202 |
203 |
204 |
205 |
208 | window.open(`https://github.com/${repo.full_name}/fork`)
209 | }
210 | >
211 |
{displayNumbers(repo.forks_count)}
212 |
213 |
214 |
215 | Fork the repo
216 |
217 |
218 |
219 |
220 | >
221 | )}
222 |
223 | );
224 | };
225 |
226 | export default RepoCard;
227 |
--------------------------------------------------------------------------------
/src/components/heads/TitleHead.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { AiOutlineArrowLeft } from "react-icons/ai";
5 |
6 | interface Props {
7 | title: string;
8 | disableBackButton?: boolean;
9 | }
10 | const TitleHead = ({ title, disableBackButton = false }: Props) => {
11 | const router = useRouter();
12 | const navigateBack = () => router.back();
13 |
14 | return (
15 |
37 | Uh oh.. no comment here.
38 |
39 | )}
40 | {comments &&
41 | comments.length > 0 &&
42 | comments.map((data) => (
43 |