-
+
{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 ?? (
+
+ Time to shine ✨ Tell the world about yourself
+
+ )}
+
+
+
+
+
+ {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.
-
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}
-
-
-
-
-
- {skillTags.map((skill: string) => (
-
- {skill}
-
- ))}
+
+ <>
+
+
-
-
-
-
Interests
-
-
- {interests.map((interest: string) => (
-
- {interest}
-
- ))}
-
-
-
+ >
)
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 (
-
-