Skip to content

Commit 01695ad

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 56f77ba + 717ff35 commit 01695ad

File tree

6 files changed

+224
-46
lines changed

6 files changed

+224
-46
lines changed

app/profile/ProfileForm.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useForm } from 'react-hook-form'
66
import { zodResolver } from '@hookform/resolvers/zod'
77
import { userSchema } from './userSchema'
88
import { updateUser } from './profileActions'
9-
import { User } from '@prisma/client'
9+
import {Skill, User} from '@prisma/client'
1010
import { Button } from "@/components/ui/button"
1111
import { Input } from "@/components/ui/input"
1212
import { Textarea } from "@/components/ui/textarea"
@@ -15,12 +15,19 @@ import { Label } from "@/components/ui/label"
1515
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
1616
import { toast } from "@/components/ui/use-toast"
1717
import { ChevronDown, ChevronUp, User as UserIcon, Mail, Phone, MapPin, Globe, Github, Twitter, Cake, UserCircle, Languages, Lock } from 'lucide-react' // Import icons
18+
import UserSkills from './UserSkills'
1819

1920
type FormData = Omit<User, 'id' | 'createdAt' | 'updatedAt' | 'emailVerified' | 'lastSignInAt' | 'createdAtTwitter' | 'deletedAt' | 'favouritesCount' | 'followersCount' | 'followingCount' | 'likeCount' | 'listedCount' | 'points' | 'statusesCount' | 'tweetCount' | 'privateMetadata' | 'publicMetadata' | 'unsafeMetadata' | 'ipAddress' | 'signatureTimestamp' | 'referrerUserId'>
2021

21-
type FormSection = 'personal' | 'contact' | 'online' | 'preferences'
22+
type FormSection = 'personal' | 'contact' | 'online' | 'preferences' | 'skills'
2223

23-
export default function ProfileForm({ user }: { user: User }) {
24+
type UserSkillWithSkill = {
25+
id: string;
26+
skillId: string;
27+
skill: Skill;
28+
}
29+
30+
export default function ProfileForm({ user }: { user: User & { userSkills: UserSkillWithSkill[] } }) {
2431
const [isSubmitting, setIsSubmitting] = useState(false)
2532
const [openSections, setOpenSections] = useState<FormSection[]>([])
2633

@@ -323,6 +330,10 @@ export default function ProfileForm({ user }: { user: User }) {
323330
</div>
324331
</div>
325332
))}
333+
334+
{renderSection("Skills", "skills", (
335+
<UserSkills user={user} />
336+
))}
326337
</form>
327338
)
328339
}

app/profile/UserSkills.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use client'
2+
3+
import React, { useState, useEffect } from 'react'
4+
import { User, Skill } from '@prisma/client'
5+
import { Input } from "@/components/ui/input"
6+
import { Button } from "@/components/ui/button"
7+
import { X } from 'lucide-react'
8+
import { addUserSkill, removeUserSkill, searchSkills } from './skillActions'
9+
import { useDebounce } from 'use-debounce'
10+
11+
type UserSkillWithSkill = {
12+
id: string;
13+
skillId: string;
14+
skill: Skill;
15+
}
16+
17+
type UserSkillsProps = {
18+
user: User & { userSkills: UserSkillWithSkill[] };
19+
}
20+
21+
export default function UserSkills({ user }: UserSkillsProps) {
22+
const [searchTerm, setSearchTerm] = useState('')
23+
const [debouncedSearchTerm] = useDebounce(searchTerm, 300)
24+
const [filteredSkills, setFilteredSkills] = useState<Skill[]>([])
25+
26+
useEffect(() => {
27+
const fetchSkills = async () => {
28+
if (debouncedSearchTerm) {
29+
const skills = await searchSkills(debouncedSearchTerm)
30+
setFilteredSkills(skills.filter(skill =>
31+
!user.userSkills.some(userSkill => userSkill.skillId === skill.id)
32+
))
33+
} else {
34+
setFilteredSkills([])
35+
}
36+
}
37+
fetchSkills()
38+
}, [debouncedSearchTerm, user.userSkills])
39+
40+
const handleAddSkill = async (skillName: string) => {
41+
if (skillName.trim()) {
42+
await addUserSkill(user.id, skillName.trim())
43+
setSearchTerm('')
44+
}
45+
}
46+
47+
const handleRemoveSkill = async (skillId: string) => {
48+
await removeUserSkill(user.id, skillId)
49+
}
50+
51+
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
52+
if (e.key === 'Enter') {
53+
e.preventDefault() // Prevent form submission
54+
handleAddSkill(searchTerm)
55+
}
56+
}
57+
58+
return (
59+
<div className="space-y-4">
60+
<div className="relative">
61+
<Input
62+
type="text"
63+
placeholder="Search for a skill..."
64+
value={searchTerm}
65+
onChange={(e) => setSearchTerm(e.target.value)}
66+
onKeyPress={handleKeyPress}
67+
/>
68+
{searchTerm && filteredSkills.length > 0 && (
69+
<div className="absolute z-10 w-full border mt-1 rounded-md shadow-lg bg-background">
70+
{filteredSkills.map((skill) => (
71+
<div
72+
key={skill.id}
73+
className="p-2 hover:bg-accent hover:text-accent-foreground cursor-pointer"
74+
onClick={() => handleAddSkill(skill.name)}
75+
>
76+
{skill.name}
77+
</div>
78+
))}
79+
</div>
80+
)}
81+
</div>
82+
<div className="flex flex-wrap gap-2">
83+
{user.userSkills.map((userSkill) => (
84+
<div key={userSkill.id} className="flex items-center bg-opacity-10 rounded-full px-3 py-1">
85+
<span>{userSkill.skill.name}</span>
86+
<Button
87+
variant="ghost"
88+
size="sm"
89+
className="ml-2 p-0"
90+
onClick={() => handleRemoveSkill(userSkill.skillId)}
91+
>
92+
<X size={16} />
93+
</Button>
94+
</div>
95+
))}
96+
</div>
97+
</div>
98+
)
99+
}

app/profile/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ export default async function ProfilePage() {
1212

1313
const user = await prisma.user.findUnique({
1414
where: { email: session.user.email },
15+
include: {
16+
userSkills: {
17+
include: {
18+
skill: true
19+
}
20+
}
21+
}
1522
})
1623

1724
if (!user) {

app/profile/skillActions.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use server'
2+
3+
import prisma from "@/lib/prisma"
4+
import { revalidatePath } from "next/cache"
5+
6+
export async function addUserSkill(userId: string, skillName: string) {
7+
const skill = await prisma.skill.upsert({
8+
where: { name: skillName },
9+
update: {},
10+
create: { name: skillName },
11+
})
12+
13+
await prisma.userSkill.create({
14+
data: {
15+
userId: userId,
16+
skillId: skill.id,
17+
},
18+
})
19+
20+
revalidatePath('/profile')
21+
}
22+
23+
export async function removeUserSkill(userId: string, skillId: string) {
24+
await prisma.userSkill.delete({
25+
where: {
26+
userId_skillId: {
27+
userId: userId,
28+
skillId: skillId,
29+
},
30+
},
31+
})
32+
33+
revalidatePath('/profile')
34+
}
35+
36+
export async function searchSkills(searchTerm: string) {
37+
const skills = await prisma.skill.findMany({
38+
where: {
39+
name: {
40+
contains: searchTerm,
41+
mode: 'insensitive',
42+
},
43+
},
44+
take: 5,
45+
})
46+
return skills
47+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
"undici": "^6.18.2",
190190
"unist-builder": "^3.0.1",
191191
"unist-util-filter": "^5.0.1",
192+
"use-debounce": "^10.0.3",
192193
"uuid": "^9.0.1",
193194
"vaul": "^0.9.0",
194195
"wavefile": "^11.0.0",

0 commit comments

Comments
 (0)