diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2fa1304 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "trailingComma": "none", + "semi": false, + "tabWidth": 2, + "singleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed" +} diff --git a/package.json b/package.json index f2fab82..a149d6d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "react-day-picker": "^8.10.1", "react-dom": "^18", "react-element-to-jsx-string": "^15.0.0", - "react-tagsinput": "^3.20.3", "recharts": "^2.14.1", "svix": "^1.43.0", "tailwind-merge": "^2.5.5", diff --git a/src/app/(dashboard)/posts/page.tsx b/src/app/(dashboard)/posts/page.tsx index aa5e5e0..5919576 100644 --- a/src/app/(dashboard)/posts/page.tsx +++ b/src/app/(dashboard)/posts/page.tsx @@ -3,7 +3,11 @@ import { useState } from "react" import PostFeed from "@/src/components/Dashboard/post-feed" import CreatePostForm from "@/src/components/Dashboard/create-post-form" -import { Post, PostFile, PostPoll } from "@/src/components/Dashboard/posts/types/posts-types" +import { + Post, + PostFile, + PostPoll +} from "@/src/components/Dashboard/posts/types/posts-types.d" const samplePosts: (Post | PostFile | PostPoll)[] = [ { diff --git a/src/app/(dashboard)/profile/[id]/page.tsx b/src/app/(dashboard)/profile/[id]/page.tsx index 7e3a036..ccd3b6c 100644 --- a/src/app/(dashboard)/profile/[id]/page.tsx +++ b/src/app/(dashboard)/profile/[id]/page.tsx @@ -6,48 +6,16 @@ import ProfileCalendar from "@/src/components/Dashboard/profile/profile-calendar import ProfileRewards from "@/src/components/Dashboard/profile/profile-rewards" import ProfileFollowActions from "@/src/components/Dashboard/profile/user/ProfileFollowActions" import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar" -import { Button } from "@/src/components/ui/button" import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/src/components/ui/tabs" -import { SelectUser } from "@/src/db/schema" import { FindUserByUniqueIdAction } from "@/src/server-actions/User/FindUserByUniqueIdAction" import { CalendarIcon, StarIcon, TrophyIcon, UserIcon } from "lucide-react" import Link from "next/link" -const skillTags = ["React", "Next.js", "TypeScript", "UI/UX", "Node.js"] -const interests = ["Web Development", "AI", "Open Source", "Tech Writing"] -const recommendations = [ - { - name: "Jane Doe", - text: "An exceptional developer with a keen eye for detail.", - }, - { name: "John Smith", text: "Always delivers high-quality work on time." }, -] -const rewards = [ - { - title: "Top Contributor", - description: "Awarded for outstanding contributions to the team", - }, - { - title: "Innovation Champion", - description: "Recognized for implementing creative solutions", - }, -] -const activities = [ - { - date: "2023-04-01", - description: "Completed the 'Advanced React Patterns' course", - }, - { - date: "2023-03-15", - description: "Contributed to open-source project 'awesome-ui-components'", - }, -] - interface ProfileScreenProps { params:{ id: string @@ -82,56 +50,49 @@ export default async function ProfileScreen({ params: { id }, searchParams:{tab} - + - - - - Bio/Basic - - - - - - Rewards - - - - - - - Activity - - - - - - - Calendar - - + + + + Bio/Basic + + + + + + Rewards + + + + + + Activity + + + {/* + + + Calendar + + */} - + - + - + {/* - + */} ) diff --git a/src/app/(dashboard)/profile/page.tsx b/src/app/(dashboard)/profile/page.tsx index 6fff4d4..6d798fc 100644 --- a/src/app/(dashboard)/profile/page.tsx +++ b/src/app/(dashboard)/profile/page.tsx @@ -1,12 +1,18 @@ -import ProfileScreen from '@/src/components/Dashboard/profile/ProfileScreen' -import React, { Suspense } from 'react' +import ProfileScreen from "@/src/components/Dashboard/profile/ProfileScreen" +import { Suspense } from "react" -const page = () => { +interface ProfilePageProps { + searchParams: { + tab?: string + } +} + +const ProfilePage: React.FC = ({ searchParams: { tab } }) => { return ( - + ) } -export default page \ No newline at end of file +export default ProfilePage diff --git a/src/app/(dashboard)/spaces/page.tsx b/src/app/(dashboard)/spaces/page.tsx index d9f4069..a90d05d 100644 --- a/src/app/(dashboard)/spaces/page.tsx +++ b/src/app/(dashboard)/spaces/page.tsx @@ -6,7 +6,7 @@ import { Post, PostFile, PostPoll -} from "@/src/components/Dashboard/posts/types/posts-types" +} from "@/src/components/Dashboard/posts/types/posts-types.d" import { Card, CardContent, diff --git a/src/components/Dashboard/Spaces/CommunityStatsCard.tsx b/src/components/Dashboard/Spaces/CommunityStatsCard.tsx index 4c14089..98516a3 100644 --- a/src/components/Dashboard/Spaces/CommunityStatsCard.tsx +++ b/src/components/Dashboard/Spaces/CommunityStatsCard.tsx @@ -1,5 +1,5 @@ import { Card, CardTitle, CardHeader, CardContent } from "../../ui/card" -import { Stat } from "./types/spaces-types" +import { Stat } from "./types/spaces-types.d" type CommunityStatsProps = { stats: Stat[] diff --git a/src/components/Dashboard/Spaces/SpacesStats.tsx b/src/components/Dashboard/Spaces/SpacesStats.tsx index 7058c09..3ccdac8 100644 --- a/src/components/Dashboard/Spaces/SpacesStats.tsx +++ b/src/components/Dashboard/Spaces/SpacesStats.tsx @@ -5,7 +5,7 @@ import TrendingTopicsCard from "./TrendingTopicsCard" import UpcomingEventsCard from "./UpcomingEventsCard" import { useDetectBreakpoint } from "@/src/hooks/useBreakpoint" import { MessageCircle, Users } from "lucide-react" -import { Event, Topic, Stat } from "./types/spaces-types" +import { Event, Topic, Stat } from "./types/spaces-types.d" const upcomingEvents: Event[] = [ { name: "TechConf 2023", date: "2023-09-15" }, @@ -45,9 +45,9 @@ const SpacesStats = () => { return ( !isMobileOrTab && ( ) ) diff --git a/src/components/Dashboard/Spaces/TrendingTopicsCard.tsx b/src/components/Dashboard/Spaces/TrendingTopicsCard.tsx index 9bacdcc..31fcb40 100644 --- a/src/components/Dashboard/Spaces/TrendingTopicsCard.tsx +++ b/src/components/Dashboard/Spaces/TrendingTopicsCard.tsx @@ -4,7 +4,7 @@ import { CardTitle } from "../../ui/card" import { CardContent } from "../../ui/card" import { Badge } from "../../ui/badge" import { TrendingUp } from "lucide-react" -import { Topic } from "./types/spaces-types" +import { Topic } from "./types/spaces-types.d" type TendingTopicProps = { topics: Topic[] diff --git a/src/components/Dashboard/Spaces/UpcomingEventsCard.tsx b/src/components/Dashboard/Spaces/UpcomingEventsCard.tsx index fc99cd1..9b06fe1 100644 --- a/src/components/Dashboard/Spaces/UpcomingEventsCard.tsx +++ b/src/components/Dashboard/Spaces/UpcomingEventsCard.tsx @@ -1,7 +1,7 @@ import { Badge } from "../../ui/badge" import { Calendar } from "../../ui/calendar" import { Card, CardHeader, CardTitle, CardContent } from "../../ui/card" -import { Event } from "./types/spaces-types" +import { Event } from "./types/spaces-types.d" type UpcomingEventsProps = { events: Event[] diff --git a/src/components/Dashboard/create-post-form.tsx b/src/components/Dashboard/create-post-form.tsx index 91b4f70..94d96c8 100644 --- a/src/components/Dashboard/create-post-form.tsx +++ b/src/components/Dashboard/create-post-form.tsx @@ -24,7 +24,13 @@ import { SelectValue } from "@/src/components/ui/select" import { Textarea } from "../ui/textarea" -import { NewPost, Post, PostFile, PostPoll, PostType } from "./posts/types/posts-types" +import { + NewPost, + Post, + PostFile, + PostPoll, + PostType +} from "./posts/types/posts-types.d" import CreatePostInput from "./posts/create-post-input" type Props = { diff --git a/src/components/Dashboard/post-feed.tsx b/src/components/Dashboard/post-feed.tsx index 755d597..5662eff 100644 --- a/src/components/Dashboard/post-feed.tsx +++ b/src/components/Dashboard/post-feed.tsx @@ -4,8 +4,7 @@ import FilePost from "./posts/post-file" import ImagePost from "./posts/post-image" import PollPost from "./posts/post-poll" import TextPost from "./posts/post-text" -import { Post, PostFile, PostPoll } from "./posts/types/posts-types" - +import { Post, PostFile, PostPoll } from "./posts/types/posts-types.d" type Props = { posts: (Post | PostFile | PostPoll)[] diff --git a/src/components/Dashboard/posts/create-post-input.tsx b/src/components/Dashboard/posts/create-post-input.tsx index 83496e9..0d71b4f 100644 --- a/src/components/Dashboard/posts/create-post-input.tsx +++ b/src/components/Dashboard/posts/create-post-input.tsx @@ -1,6 +1,6 @@ import { Textarea } from "@/src/components/ui/textarea" import { Input } from "@/src/components/ui/input" -import { NewPost, PostType } from "./types/posts-types" +import { NewPost, PostType } from "./types/posts-types.d" type Props = { type: PostType diff --git a/src/components/Dashboard/posts/post-comment-form.tsx b/src/components/Dashboard/posts/post-comment-form.tsx index 73d9346..a3840ab 100644 --- a/src/components/Dashboard/posts/post-comment-form.tsx +++ b/src/components/Dashboard/posts/post-comment-form.tsx @@ -4,7 +4,7 @@ import { Button } from "@/src/components/ui/button" import { useState } from "react" import { Input } from "@/src/components/ui/input" import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar" -import { Post, PostFile, PostPoll } from "./types/posts-types" +import { Post, PostFile, PostPoll } from "./types/posts-types.d" type Props = { posts: (Post | PostFile | PostPoll)[] @@ -12,7 +12,7 @@ type Props = { postId: string } -const PostCommentForm: React.FC = ({posts, setPosts, postId}) => { +const PostCommentForm: React.FC = ({ posts, setPosts, postId }) => { const [newComment, setNewComment] = useState<{ [key: string]: string }>({}) const handleAddComment = (postId: string) => { @@ -33,7 +33,12 @@ const PostCommentForm: React.FC = ({posts, setPosts, postId}) => { } return post }) - setPosts(updatedPosts.filter((post): post is Post => 'content' in post && typeof post.content === 'string')) + setPosts( + updatedPosts.filter( + (post): post is Post => + "content" in post && typeof post.content === "string" + ) + ) setNewComment({ ...newComment, [postId]: "" }) } diff --git a/src/components/Dashboard/posts/post-comments.tsx b/src/components/Dashboard/posts/post-comments.tsx index 6654e76..36f44e3 100644 --- a/src/components/Dashboard/posts/post-comments.tsx +++ b/src/components/Dashboard/posts/post-comments.tsx @@ -1,5 +1,5 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar" -import { Comment } from "./types/posts-types" +import { Comment } from "./types/posts-types.d" type Props = { comment: Comment diff --git a/src/components/Dashboard/posts/post-file.tsx b/src/components/Dashboard/posts/post-file.tsx index e1b6179..9156900 100644 --- a/src/components/Dashboard/posts/post-file.tsx +++ b/src/components/Dashboard/posts/post-file.tsx @@ -1,10 +1,10 @@ -import { Post, Comment, PostFile, PostPoll } from "./types/posts-types" +import { Post, Comment, PostFile, PostPoll } from "./types/posts-types.d" import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar" import { Card, CardContent, CardFooter, - CardHeader, + CardHeader } from "@/src/components/ui/card" import { Badge } from "@/src/components/ui/badge" import { Separator } from "@/src/components/ui/separator" @@ -19,16 +19,13 @@ type Props = { setPosts: (posts: (Post | PostFile | PostPoll)[]) => void } -const FilePost: React.FC = ({post, posts, setPosts}) => { +const FilePost: React.FC = ({ post, posts, setPosts }) => { return (
- + {post.author.name[0]}
@@ -53,21 +50,14 @@ const FilePost: React.FC = ({post, posts, setPosts}) => {
- +
{post.comments.map((comment: Comment) => ( ))}
- +
) diff --git a/src/components/Dashboard/posts/post-image.tsx b/src/components/Dashboard/posts/post-image.tsx index 7103432..660af3c 100644 --- a/src/components/Dashboard/posts/post-image.tsx +++ b/src/components/Dashboard/posts/post-image.tsx @@ -1,10 +1,10 @@ -import { Post, Comment, PostFile, PostPoll } from "./types/posts-types" +import { Post, Comment, PostFile, PostPoll } from "./types/posts-types.d" import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar" import { Card, CardContent, CardFooter, - CardHeader, + CardHeader } from "@/src/components/ui/card" import { Badge } from "@/src/components/ui/badge" import { Separator } from "@/src/components/ui/separator" diff --git a/src/components/Dashboard/posts/post-poll.tsx b/src/components/Dashboard/posts/post-poll.tsx index d095792..bf8b824 100644 --- a/src/components/Dashboard/posts/post-poll.tsx +++ b/src/components/Dashboard/posts/post-poll.tsx @@ -3,7 +3,7 @@ import { Card, CardContent, CardFooter, - CardHeader, + CardHeader } from "@/src/components/ui/card" import { Badge } from "@/src/components/ui/badge" import { Separator } from "@/src/components/ui/separator" @@ -13,7 +13,7 @@ import PostCommentForm from "./post-comment-form" import { RadioGroup } from "../../ui/radio-group" import { Label } from "../../ui/label" import { RadioGroupItem } from "../../ui/radio-group" -import { Comment, Post, PostFile, PostPoll } from "./types/posts-types" +import { Comment, Post, PostFile, PostPoll } from "./types/posts-types.d" type Props = { post: PostPoll diff --git a/src/components/Dashboard/posts/post-text.tsx b/src/components/Dashboard/posts/post-text.tsx index f511eda..494b7f5 100644 --- a/src/components/Dashboard/posts/post-text.tsx +++ b/src/components/Dashboard/posts/post-text.tsx @@ -1,10 +1,10 @@ -import { Post, Comment, PostFile, PostPoll } from "./types/posts-types" +import { Post, Comment, PostFile, PostPoll } from "./types/posts-types.d" import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar" import { Card, CardContent, CardFooter, - CardHeader, + CardHeader } from "@/src/components/ui/card" import { Badge } from "@/src/components/ui/badge" import { Separator } from "@/src/components/ui/separator" diff --git a/src/components/Dashboard/profile/ProfileBioClient.tsx b/src/components/Dashboard/profile/ProfileBioClient.tsx new file mode 100644 index 0000000..a52b23f --- /dev/null +++ b/src/components/Dashboard/profile/ProfileBioClient.tsx @@ -0,0 +1,125 @@ +"use client" + +import EditProfileModal from "./edit-profile-modal" +import { Badge } from "../../ui/badge" +import { useAtomValue, useSetAtom } from "jotai" +import { profileStore } from "@/src/store/profile/profileStore" +import { useServerAction } from "@/src/hooks/useServerAction" +import { GetTagsForUserAction } from "@/src/server-actions/Tag/Tag" +import { useEffect } from "react" +import { Tag, TagStatus } from "./types/profile-types.d" +import Loader from "../../common/Loader/Loader" +import { LoaderSizes } from "../../common/Loader/types/loader-types.d" + +type ProfileBioClientProps = { + editable?: boolean + userId: string + userBio: string +} + +const ProfileBioClient: React.FC = ({ + editable = true, + userId, + userBio +}) => { + const setUserBio = useSetAtom(profileStore.bio) + const setUserSkills = useSetAtom(profileStore.skills) + const setUserInterests = useSetAtom(profileStore.interests) + const skills = useAtomValue(profileStore.skills) + const interests = useAtomValue(profileStore.interests) + const bio = useAtomValue(profileStore.bio) + + const [getTagsLoading, tagsData, getTagsError, getTags] = + useServerAction(GetTagsForUserAction) + + useEffect(() => { + if (tagsData && tagsData.data) { + const skillTags = tagsData?.data + .filter((tag) => tag.type === "skill") + .map((tag) => ({ + id: tag.id, + name: tag.name, + status: TagStatus.saved as const + })) + const interestTags = tagsData?.data + .filter((tag) => tag.type === "interest") + .map((tag) => ({ + id: tag.id, + name: tag.name, + status: TagStatus.saved as const + })) + setUserInterests(interestTags) + setUserSkills(skillTags) + } + }, [tagsData]) + + useEffect(() => { + if (userId) { + getTags(userId) + } + }, [userId]) + + useEffect(() => { + setUserBio(userBio) + }, [userBio]) + + return getTagsLoading ? ( +
+ +
+ ) : ( + <> +
+
+

Bio

+ {editable && } +
+

+ {bio ?? ( + + Time to shine ✨ Tell the world about yourself + + )} +

+
+
+
+

Skills

+
+
+ {skills.length ? ( + skills.map((skill: Tag) => ( + + {skill.name} + + )) + ) : ( + + HTML ninja? 🥷 Python wizard? 🪄 Show off your superpowers! + + )} +
+
+
+
+

Interests

+
+
+ {interests.length ? ( + interests.map((interest: Tag) => ( + + {interest.name} + + )) + ) : ( + + 💿 Share your passions, hobbies, and guilty coding pleasures 💾 + + )} +
+
+ + ) +} + +export default ProfileBioClient diff --git a/src/components/Dashboard/profile/ProfileScreen.tsx b/src/components/Dashboard/profile/ProfileScreen.tsx index e1e68ac..1a7ef29 100644 --- a/src/components/Dashboard/profile/ProfileScreen.tsx +++ b/src/components/Dashboard/profile/ProfileScreen.tsx @@ -1,5 +1,3 @@ -"use client" - import ProfileActivities from "@/src/components/Dashboard/profile/profile-activities" import ProfileBio from "@/src/components/Dashboard/profile/profile-bio" import ProfileCalendar from "@/src/components/Dashboard/profile/profile-calendar" @@ -12,121 +10,76 @@ import { TabsTrigger } from "@/src/components/ui/tabs" import { CalendarIcon, StarIcon, TrophyIcon, UserIcon } from "lucide-react" -import { useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { Recommendation } from "@/src/components/Dashboard/profile/types/profile-types" -import { useUser } from "@clerk/nextjs" +import { AuthUserAction } from "@/src/server-actions/User/AuthUserAction" +import NotFound from "@/src/components/Dashboard/NotFound/NotFound" +import Link from "next/link" -const rewards = [ - { - title: "Top Contributor", - description: "Awarded for outstanding contributions to the team" - }, - { - title: "Innovation Champion", - description: "Recognized for implementing creative solutions" - } -] -const activities = [ - { - date: "2023-04-01", - description: "Completed the 'Advanced React Patterns' course" - }, - { - date: "2023-03-15", - description: "Contributed to open-source project 'awesome-ui-components'" - } -] +type ProfileScreenProps = { tab?: string } -export default function ProfileScreen() { - const searchParams = useSearchParams() - const [activeTab, setActiveTab] = useState("basic") - const [skillTags, setSkillTags] = useState([ - "Web Development", - "AI", - "Open Source", - "Tech Writing" - ]) - const [interests, setInterests] = useState([ - "React", - "Next.js", - "TypeScript", - "UI/UX", - "Node.js" - ]) - const [recommendations, setRecommendations] = useState([ - { - name: "Jane Doe", - text: "An exceptional developer with a keen eye for detail." - }, - { name: "John Smith", text: "Always delivers high-quality work on time." } - ]) - const { user } = useUser() +export default async function ProfileScreen({ tab }: ProfileScreenProps) { + const user = await AuthUserAction() - useEffect(() => { - const tab = searchParams.get("tab") - if (tab) { - setActiveTab(tab) - } - }, [searchParams]) + if (!user) { + return + } return (
- - JD + + Profile Image
-

{user?.fullName}

-

- {user?.emailAddresses[0].emailAddress} -

+

{user?.first_name}

+

{user?.email}

- + - setActiveTab("basic")}> - - Bio/Basic - - setActiveTab("rewards")}> - - Rewards - - setActiveTab("activity")} - > - - Activity - - setActiveTab("calendar")} - > - - Calendar - + + + + Bio/Basic + + + + + + Rewards + + + + + + Activity + + + {/* + + + Calendar + + */} - + - + - + {/* - + */}
) diff --git a/src/components/Dashboard/profile/edit-profile-modal.tsx b/src/components/Dashboard/profile/edit-profile-modal.tsx index 71232d6..6776221 100644 --- a/src/components/Dashboard/profile/edit-profile-modal.tsx +++ b/src/components/Dashboard/profile/edit-profile-modal.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react" +import { useEffect, useState } from "react" import { Button } from "../../ui/button" import { Dialog, @@ -9,34 +9,158 @@ import { DialogTitle, DialogTrigger } from "../../ui/dialog" -import { Input } from "../../ui/input" import { Label } from "../../ui/label" import { Textarea } from "@/src/components/ui/textarea" -import ChipsInput from "@/src/components/chips-input" +import TagsInput from "@/src/components/TagsInput/TagsInput" +import { SaveUserProfileAction } from "@/src/server-actions/User/User" +import { useServerAction } from "@/src/hooks/useServerAction" +import { useAtomValue, useSetAtom } from "jotai" +import { userStore } from "@/src/store/user/userStore" +import { profileStore } from "@/src/store/profile/profileStore" +import useUserSkills from "./hooks/useUserSkills" +import useUserInterests from "./hooks/useUserInterests" +import { ProfileData, Tag, TagStatus } from "./types/profile-types.d" +import { useToast } from "@/src/hooks/use-toast" -type EditProfileModalProps = { - bio: string - setBio: (value: string) => void - interests: string[] - setInterests: (value: string[]) => void - skills: string[] - setSkills: (value: string[]) => void -} +const EditProfileModal: React.FC = () => { + const bio = useAtomValue(profileStore.bio) + const user = useAtomValue(userStore.AuthUser) + const setBio = useSetAtom(profileStore.bio) + const { toast } = useToast() + + const [isOpen, setIsOpen] = useState(false) + const [editedBio, setEditedBio] = useState(bio) + + const [ + updateProfileLoading, + updatedProfileData, + updateProfileError, + updateProfile + ] = useServerAction(SaveUserProfileAction) + + const [ + skills, + setSkills, + skillSuggestions, + searchSkills, + searchSkillsLoading + ] = useUserSkills() + + const [ + interests, + setInterests, + interestSuggestions, + searchInterests, + searchInterestsLoading + ] = useUserInterests() -const EditProfileModal: React.FC = ({setBio, setSkills, setInterests,skills, bio, interests }) => { - const [isOpen, setIsOpen] = useState(false) - const [skillsCopy, setSkillsCopy] = useState([...skills]) - const [interestsCopy, setinterestsCopy] = useState([ - ...interests - ]) - const editedBio = useRef(bio) + useEffect(() => { + if (updateProfileError) { + toast({ + variant: "destructive", + title: "Error updating profile", + description: "Something went wrong. Please try again.", + duration: 3000 + }) + } + }, [updateProfileError]) - const updateProfileValue = (e: React.FormEvent) => { + const updatedSkillsLength: number = skills.filter( + (tag) => !tag.deleted + ).length + const updatedInterestsLength: number = interests.filter( + (tag) => !tag.deleted + ).length + const skillsError: string = + updatedSkillsLength > 20 ? "You can only add a maximum of 20 skills" : "" + const interestsError: string = + updatedInterestsLength > 20 + ? "You can only add a maximum of 20 interests" + : "" + const bioError: string = + editedBio && editedBio?.length > 2000 + ? "Bio cannot exceed 2000 characters" + : "" + const saveProfileChanges = async (e: React.FormEvent) => { e.preventDefault() - setBio(editedBio.current) - setSkills([...skillsCopy]) - setInterests([...interestsCopy]) - setIsOpen(false) + try { + const deletedSkillsIds: number[] = skills + .filter((skill) => skill.deleted && skill.status === TagStatus.saved) + .map((skill) => skill.id as number) + const deletedInterestsIds: number[] = interests + .filter( + (interest) => interest.deleted && interest.status === TagStatus.saved + ) + .map((interest) => interest.id as number) + const updatedProfileData: ProfileData = { + userId: user?.unique_id as string, + bio: editedBio ? editedBio : bio, + newTags: [ + ...skills + .filter((tag) => tag.status === TagStatus.new && !tag.deleted) + .map((tag) => { + return { name: tag.name, type: "skill" } + }), + ...interests + .filter((tag) => tag.status === TagStatus.new && !tag.deleted) + .map((tag) => { + return { name: tag.name, type: "interest" } + }) + ], + existingTags: [ + ...skills + .filter((tag) => tag.status === TagStatus.selected && !tag.deleted) + .map((tag) => { + return { name: tag.name, id: tag.id, type: "skill" } + }), + ...interests + .filter((tag) => tag.status === TagStatus.selected && !tag.deleted) + .map((tag) => { + return { name: tag.name, id: tag.id, type: "interest" } + }) + ], + deletedTagsIds: [...deletedSkillsIds, ...deletedInterestsIds] + } + const res = await updateProfile(updatedProfileData) + if (res?.success) { + // remove deleted skills and update skills val in store + setSkills((skills: Tag[]) => + skills + .filter( + (tag) => + !deletedSkillsIds.includes(tag.id as number) && !tag.deleted + ) + .map((tag) => ({ ...tag, status: TagStatus.saved })) + ) + // remove deleted Interests and update interests val in store + setInterests((interests: Tag[]) => + interests + .filter( + (tag) => + !deletedInterestsIds.includes(tag.id as number) && !tag.deleted + ) + .map((tag) => ({ ...tag, status: TagStatus.saved })) + ) + editedBio && setBio(editedBio) + setIsOpen(false) + setEditedBio("") + toast({ + title: "Profile updated", + description: "Your changes have been saved successfully.", + duration: 3000 + }) + } else { + throw res?.error + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error updating profile", + description: + error instanceof Error ? error.message : "Something went wrong", + duration: 3000 + }) + } } return ( @@ -53,7 +177,7 @@ const EditProfileModal: React.FC = ({setBio, setSkills, s Make changes to your profile here. Click save when you're done. -
+
@@ -64,35 +188,95 @@ const EditProfileModal: React.FC = ({setBio, setSkills, s id={"bio"} defaultValue={bio} className="min-h-[100px] w-full" - onChange={(e: React.ChangeEvent) => - (editedBio.current = e.target.value) - } + onChange={(e) => setEditedBio(e.target.value)} /> +
+

2000 + ? "text-red-500" + : "text-gray-500" + }`} + > + {editedBio?.length + ? editedBio?.length + : bio?.length + ? bio.length + : 0} + /2000 characters +

+ {bioError && ( +

{bioError}

+ )} +
- setSkillsCopy([...skills])} + +
+

20 + ? "text-red-500" + : "text-gray-500" + }`} + > + {`${updatedSkillsLength}/20 skills`} +

+ {skillsError && ( +

{skillsError}

+ )} +
- - setinterestsCopy([...interests]) - } + +
+

20 + ? "text-red-500" + : "text-gray-500" + }`} + > + {`${updatedInterestsLength}/20 skills`} +

+ {interestsError && ( +

{interestsError}

+ )} +
- +
diff --git a/src/components/Dashboard/profile/hooks/useUserInterests.ts b/src/components/Dashboard/profile/hooks/useUserInterests.ts new file mode 100644 index 0000000..8a6e245 --- /dev/null +++ b/src/components/Dashboard/profile/hooks/useUserInterests.ts @@ -0,0 +1,51 @@ +import { Tag, TagStatus } from "../types/profile-types.d" +import { useServerAction } from "@/src/hooks/useServerAction" +import { SearchTagsForSuggestionsAction } from "@/src/server-actions/Tag/Tag" +import { SetStateAction, useAtomValue, useSetAtom } from "jotai" +import { profileStore } from "@/src/store/profile/profileStore" + +type UseUserInterestsReturn = [ + interests: Tag[], // Current skills + setInterests: (value: SetStateAction) => void, // Interests setter + suggestions: Tag[], // Search suggestions + searchInterestsForUserInput: (name: string) => void, // Search function + searchInterestsLoading: boolean // Loading state +] + +const useUserInterests = (): UseUserInterestsReturn => { + const interests = useAtomValue(profileStore.interests) + const setInterests = useSetAtom(profileStore.interests) + + const [ + searchInterestsLoading, + searchedInterests, + searchInterestsError, + searchInterests + ] = useServerAction(SearchTagsForSuggestionsAction) + + const suggestions: Tag[] = searchedInterests?.data + ? searchedInterests.data.map((tag) => ({ + name: tag.name, + id: tag.id, + status: TagStatus.selected as const + })) + : [] + + const searchInterestsForUserInput = (name: string) => { + try { + searchInterests(name, "interest") + } catch (error) { + console.error(error) + } + } + + return [ + interests, + setInterests, + suggestions, + searchInterestsForUserInput, + searchInterestsLoading + ] +} + +export default useUserInterests diff --git a/src/components/Dashboard/profile/hooks/useUserSkills.ts b/src/components/Dashboard/profile/hooks/useUserSkills.ts new file mode 100644 index 0000000..ab3581b --- /dev/null +++ b/src/components/Dashboard/profile/hooks/useUserSkills.ts @@ -0,0 +1,47 @@ +import { Tag, TagStatus } from "../types/profile-types.d" +import { useServerAction } from "@/src/hooks/useServerAction" +import { SearchTagsForSuggestionsAction } from "@/src/server-actions/Tag/Tag" +import { SetStateAction, useAtomValue, useSetAtom } from "jotai" +import { profileStore } from "@/src/store/profile/profileStore" + +type UseUserSkillsReturn = [ + skills: Tag[], // Current skills + setSkills: (value: SetStateAction) => void, // Skills setter + suggestions: Tag[], // Search suggestions + searchSkillsForUserInput: (name: string) => void, // Search function + searchSkillsLoading: boolean // Loading state +] + +const useUserSkills = (): UseUserSkillsReturn => { + const skills = useAtomValue(profileStore.skills) + const setSkills = useSetAtom(profileStore.skills) + + const [searchSkillsLoading, searchedSkills, searchSkillsError, searchSkills] = + useServerAction(SearchTagsForSuggestionsAction) + + const suggestions: Tag[] = searchedSkills?.data + ? searchedSkills.data.map((tag) => ({ + name: tag.name, + id: tag.id, + status: TagStatus.selected as const + })) + : [] + + const searchSkillsForUserInput = (name: string) => { + try { + searchSkills(name, "skill") + } catch (error) { + console.error(error) + } + } + + return [ + skills, + setSkills, + suggestions, + searchSkillsForUserInput, + searchSkillsLoading + ] +} + +export default useUserSkills diff --git a/src/components/Dashboard/profile/profile-activities.tsx b/src/components/Dashboard/profile/profile-activities.tsx index f5279cd..088badc 100644 --- a/src/components/Dashboard/profile/profile-activities.tsx +++ b/src/components/Dashboard/profile/profile-activities.tsx @@ -1,18 +1,33 @@ -import { Activity } from "./types/profile-types" import { Card, CardTitle, CardDescription, CardContent, - CardHeader, + CardHeader } from "../../ui/card" import { StarIcon } from "lucide-react" +import { GetActivitiessForUserAction } from "@/src/server-actions/Activity/Activity" -type Props = { - activities: Activity[] +type ProfileActivitiesProps = { + userId: string } -const ProfileActivities: React.FC = (props) => { +const ProfileActivities: React.FC = async ({ + userId +}) => { + let activities + + try { + const res = await GetActivitiessForUserAction(userId) + if (res.success) { + activities = res.data + } else { + throw res.error + } + } catch (error) { + console.error(error) + } + return ( @@ -23,8 +38,8 @@ const ProfileActivities: React.FC = (props) => {
    - {props.activities.map((activity, i) => ( -
  • + {activities?.map((activity) => ( +
  • {activity.description}

    diff --git a/src/components/Dashboard/profile/profile-bio.tsx b/src/components/Dashboard/profile/profile-bio.tsx index 1bec7c0..61610df 100644 --- a/src/components/Dashboard/profile/profile-bio.tsx +++ b/src/components/Dashboard/profile/profile-bio.tsx @@ -1,86 +1,37 @@ -'use client' -import { Badge } from "../../ui/badge" -import { - Card, - CardTitle, - CardDescription, - CardContent, - CardHeader, -} from "../../ui/card" -import { Recommendation } from "./types/profile-types" -import EditProfileModal from "./edit-profile-modal" -import { useState } from "react" +import { GetUserRecommendationsAction } from "@/src/server-actions/Recommendations/Recommendations" +import { Card, CardContent } from "../../ui/card" +import ProfileBioClient from "./ProfileBioClient" -type Props = { - recommendations: Recommendation[] - skillTags: string[] - setSkillTags?: (tags: string[]) => void - interests: string[] - setInterests?: (tags: string[]) => void +type ProfileBioProps = { + userId: string + userBio: string editable?: boolean } -const ProfileBio: React.FC = ({recommendations, skillTags, setSkillTags, interests, setInterests, editable=true}) => { - const [bio, setBio] = useState("hello world!") +const ProfileBio: React.FC = async ({ userId, userBio, editable }) => { + const recommendations = await GetUserRecommendationsAction(userId) return ( - -
    - Bio - { - editable && setSkillTags && setInterests && ( - - ) - } -
    - {bio} -
    - -
    -
    -

    Skills

    -
    -
    - {skillTags.map((skill: string) => ( - - {skill} - - ))} + + <> + +
    +

    Recommendations

    +
      + {recommendations.map((recommendation, i) => ( +
    • +

      + "{recommendation.content}" +

      +

      + - {recommendation.recommender_full_name} +

      +
    • + ))} +
    -
    -
    -
    -

    Interests

    -
    -
    - {interests.map((interest: string) => ( - - {interest} - - ))} -
    -
    -
    -

    Recommendations

    -
      - {recommendations.map((recommendation: Recommendation,i) => ( -
    • -

      "{recommendation.text}"

      -

      - - {recommendation.name} -

      -
    • - ))} -
    -
    + ) diff --git a/src/components/Dashboard/profile/profile-rewards.tsx b/src/components/Dashboard/profile/profile-rewards.tsx index d8a8e16..f19a543 100644 --- a/src/components/Dashboard/profile/profile-rewards.tsx +++ b/src/components/Dashboard/profile/profile-rewards.tsx @@ -1,18 +1,31 @@ -import { Reward } from "./types/profile-types" import { Card, CardTitle, CardDescription, CardContent, - CardHeader, + CardHeader } from "../../ui/card" import { TrophyIcon } from "lucide-react" +import { GetRewardsForUserAction } from "@/src/server-actions/Reward/Reward" -type Props = { - rewards: Reward[] +type ProfileActivitiesProps = { + userId: string } -const ProfileRewards: React.FC = (props) => { +const ProfileRewards: React.FC = async ({ userId }) => { + let rewards + + try { + const res = await GetRewardsForUserAction(userId) + if (res.success) { + rewards = res.data + } else { + throw res.error + } + } catch (error) { + console.error(error) + } + return ( @@ -21,8 +34,8 @@ const ProfileRewards: React.FC = (props) => {
      - {props.rewards.map((reward: Reward,i) => ( -
    • + {rewards?.map((reward) => ( +
    • {reward.title}

      diff --git a/src/components/Dashboard/profile/types/profile-types.d.ts b/src/components/Dashboard/profile/types/profile-types.d.ts index 43f8870..cd0cba8 100644 --- a/src/components/Dashboard/profile/types/profile-types.d.ts +++ b/src/components/Dashboard/profile/types/profile-types.d.ts @@ -1,3 +1,5 @@ +import { InsertTag } from "@/src/db/schema" + export type Recommendation = { name: string text: string @@ -12,3 +14,24 @@ export type Activity = { date: string description: string } + +export type Tag = { + name: string + id?: number + status: TagStatus + deleted?: boolean +} + +export enum TagStatus { + saved = "saved", + selected = "selected", + new = "new" +} + +export type ProfileData = { + userId: string + bio: string + newTags: InsertTag[] + existingTags: InsertTag[] + deletedTagsIds: number[] +} diff --git a/src/components/TagsInput/TagsInput.tsx b/src/components/TagsInput/TagsInput.tsx new file mode 100644 index 0000000..0034a5a --- /dev/null +++ b/src/components/TagsInput/TagsInput.tsx @@ -0,0 +1,165 @@ +import { useState, useEffect, useRef } from "react" +import { X } from "lucide-react" +import { Tag, TagStatus } from "../Dashboard/profile/types/profile-types.d" + +type TagsInputProps = { + tags: Tag[] + updateTags: (tags: Tag[] | ((tags: Tag[]) => Tag[])) => void + suggestions: Tag[] + onChange: (tagName: string) => void + loadingSuggestions: boolean + autocomplete?: boolean +} + +const TagsInput: React.FC = ({ + tags, + updateTags, + suggestions, + loadingSuggestions, + onChange, + autocomplete = true +}) => { + const [showSuggestions, setShowSuggestions] = useState(false) + const timer = useRef() + + const tagInput = useRef(null) + + useEffect(() => { + return () => { + timer && clearTimeout(timer.current) + } + }, []) + + const handleInputChange = (e: React.ChangeEvent) => { + if (autocomplete) { + // Clear existing timer + timer && clearTimeout(timer.current) + // Set new timer for debouncing + if (e.target.value.length >= 2) { + timer.current = setTimeout(() => { + try { + setShowSuggestions(true) + onChange(e.target.value) + } catch (error) { + console.error("Error fetching suggestions:", error) + } + }, 800) + } else { + onChange(e.target.value) + } + } else { + setShowSuggestions(false) + } + } + + const handleNewTag = () => { + if ( + !tags.some( + (tag) => + tag?.name.toLowerCase() === + (tagInput.current as HTMLInputElement).value.toLowerCase() && + !tag.deleted + ) + ) { + updateTags((tags: Tag[]) => [ + ...tags, + { + name: + (tagInput.current as HTMLInputElement).value + .trim()[0] + .toUpperCase() + + (tagInput.current as HTMLInputElement).value + .substring(1) + .toLowerCase(), + status: TagStatus.new + } + ]) + setShowSuggestions(false) + ;(tagInput.current as HTMLInputElement).value = "" + } + } + + const removeTag = (indexToRemove: number) => { + updateTags((tags) => + tags.with(indexToRemove, { ...tags[indexToRemove], deleted: true }) + ) + } + + const selectSuggestion = (suggestion: Tag) => { + if ( + !tags.some( + (tag) => + tag?.name.toLowerCase() === suggestion.name.toLowerCase() && + !tag.deleted + ) + ) { + updateTags((tags: Tag[]) => [...tags, suggestion]) + } + ;(tagInput.current as HTMLInputElement).value = "" + setShowSuggestions(false) + } + + return ( +
      +
      + {tags.map( + (tag, i) => + !tag.deleted && ( + + {tag?.name} + + + ) + )} + +
      + {showSuggestions && (tagInput.current as HTMLInputElement).value && ( +
      + {loadingSuggestions ? ( +
      Loading...
      + ) : suggestions.length === 0 ? ( +
      + +
      + ) : ( +
      + {suggestions.map((suggestion) => ( + + ))} +
      + )} +
      + )} +
      + ) +} + +export default TagsInput diff --git a/src/components/chips-input/chips-input.css b/src/components/chips-input/chips-input.css deleted file mode 100644 index e949e2c..0000000 --- a/src/components/chips-input/chips-input.css +++ /dev/null @@ -1,37 +0,0 @@ -.react-tagsinput { - background-color: transparent; - border: none; - height: fit-content; -} - -.react-tagsinput--focused { - border-color: none; -} - -.react-tagsinput-tag { - background-color: #262626; - border-radius: 2px; - border: none; - color: hsl(0, 0%, 98%); - display: inline-block; - font-family: sans-serif; - font-size: 13px; - font-weight: 400; - margin-bottom: 5px; - margin-right: 5px; - padding: 5px; -} - -.react-tagsinput-input { - background: transparent; - border: 0; - color: white; - font-family: sans-serif; - font-size: 13px; - font-weight: 400; - margin-bottom: 6px; - margin-top: 1px; - outline: none; - padding: 5px; - width: 80px; -} diff --git a/src/components/chips-input/index.tsx b/src/components/chips-input/index.tsx deleted file mode 100644 index 69303b7..0000000 --- a/src/components/chips-input/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import TagsInput from "react-tagsinput" -import "./chips-input.css" - -type ChipsInputProps = { - tags: string[] - updateTags: (tags: string[]) => void -} - -const ChipsInput: React.FC = (props) => { - return ( -
      - props.updateTags(tags)} - addOnPaste - inputProps={{ - className: "react-tagsinput-input", - placeholder: "" - }} - /> -
      - ) -} - -export default ChipsInput diff --git a/src/components/common/Loader/Loader.tsx b/src/components/common/Loader/Loader.tsx index c8d3e5c..976589f 100644 --- a/src/components/common/Loader/Loader.tsx +++ b/src/components/common/Loader/Loader.tsx @@ -1,13 +1,28 @@ -import React from 'react' +import { LoaderSizes } from "./types/loader-types.d" -const Loader = () => { +type LoaderProps = { + size?: LoaderSizes +} + +const Loader: React.FC = ({ size = LoaderSizes.sm }) => { return ( - -