From e44cf69043831f6f9003390d180d992ad47cac52 Mon Sep 17 00:00:00 2001 From: Dev Sharma Date: Thu, 31 Oct 2024 11:56:14 +0530 Subject: [PATCH] added share button + edge cases --- .../migration.sql | 11 ++ prisma/schema.prisma | 2 - prisma/seed.ts | 5 +- src/actions/auth.actions.ts | 3 +- src/actions/user.profile.actions.ts | 2 - src/components/comboBox.tsx | 5 +- src/components/profile/AboutMe.tsx | 8 +- src/components/profile/ProfileEducation.tsx | 4 +- src/components/profile/ProfileExperience.tsx | 4 +- src/components/profile/ProfileHeroSection.tsx | 20 +-- src/components/profile/ProfileProjects.tsx | 58 ++++----- src/components/profile/ProfileShare.tsx | 88 +++++++++++++ .../ProfileEmptyContainers.tsx | 10 +- .../profile/forms/EditProfileForm.tsx | 21 +-- src/components/profile/forms/ProjectForm.tsx | 17 ++- src/components/profile/forms/ReadMeForm.tsx | 12 +- src/components/profile/forms/SkillsForm.tsx | 15 +-- .../profile/profile-skills-combobox.tsx | 122 ++++++++++++++++++ src/components/profile/profileComboBox.tsx | 109 ++++++++++++++++ src/components/skills-combobox.tsx | 65 +++++----- src/lib/auth.ts | 1 - src/lib/authOptions.ts | 1 - src/lib/validators/user.profile.validator.ts | 12 +- src/types/user.types.ts | 1 - 24 files changed, 452 insertions(+), 144 deletions(-) create mode 100644 prisma/migrations/20241031043344_username_remove/migration.sql create mode 100644 src/components/profile/ProfileShare.tsx create mode 100644 src/components/profile/profile-skills-combobox.tsx create mode 100644 src/components/profile/profileComboBox.tsx diff --git a/prisma/migrations/20241031043344_username_remove/migration.sql b/prisma/migrations/20241031043344_username_remove/migration.sql new file mode 100644 index 00000000..ee88353a --- /dev/null +++ b/prisma/migrations/20241031043344_username_remove/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `username` on the `User` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "User_username_key"; + +-- AlterTable +ALTER TABLE "User" DROP COLUMN "username"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 37185446..9d14ae77 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,8 +38,6 @@ model User { twitterLink String? discordLink String? - username String @unique - contactEmail String? aboutMe String? diff --git a/prisma/seed.ts b/prisma/seed.ts index 1856b278..352d9078 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -12,21 +12,19 @@ import bcrypt from 'bcryptjs'; const prisma = new PrismaClient(); const users = [ - { id: '1', name: 'Jack', email: 'user@gmail.com', username: 'jackcoder', role: Role.USER }, + { id: '1', name: 'Jack', email: 'user@gmail.com', role: Role.USER }, { id: '2', name: 'Admin', email: 'admin@gmail.com', role: Role.ADMIN, onBoard: true, - username: 'admincoder', }, { id: '3', name: 'Hr', email: 'hr@gmail.com', role: Role.HR, - username: 'hrcoder', }, ]; @@ -334,7 +332,6 @@ async function seedUsers() { password: hashedPassword, role: u.role || Role.USER, emailVerified: new Date(), - username: u.username, }, }); console.log(`User created or updated: ${u.email}`); diff --git a/src/actions/auth.actions.ts b/src/actions/auth.actions.ts index 3a156ac7..9db6d30b 100644 --- a/src/actions/auth.actions.ts +++ b/src/actions/auth.actions.ts @@ -42,8 +42,7 @@ export const signUp = withServerActionAsyncCatcher< await prisma.$transaction( async (txn) => { const user = await txn.user.create({ - // todo username - data: { ...data, password: hashedPassword, username: 'asdif' }, + data: { ...data, password: hashedPassword }, }); const verificationToken = await txn.verificationToken.create({ diff --git a/src/actions/user.profile.actions.ts b/src/actions/user.profile.actions.ts index 519c3504..f1a24e9e 100644 --- a/src/actions/user.profile.actions.ts +++ b/src/actions/user.profile.actions.ts @@ -369,7 +369,6 @@ export const getUserDetailsWithId = async (id: string) => { id: id, }, select: { - username: true, name: true, id: true, skills: true, @@ -419,7 +418,6 @@ export const updateUserDetails = withSession< }, data: { name: data.name, - username: data.username, email: data.email, contactEmail: data.contactEmail, aboutMe: data.aboutMe, diff --git a/src/components/comboBox.tsx b/src/components/comboBox.tsx index 97f3a0df..aed07fae 100644 --- a/src/components/comboBox.tsx +++ b/src/components/comboBox.tsx @@ -43,9 +43,10 @@ export function Combobox({ variant="outline" role="combobox" aria-expanded={open} - className="w-full justify-between border border-slate-200 dark:bg-gray-800 text-slate-500 rounded-[8px] dark:text-white" + className="w-full justify-between dark:bg-gray-800 border-none dark:text-white" + aria-label="skillset" > - Enter Skills + Search skillset ... diff --git a/src/components/profile/AboutMe.tsx b/src/components/profile/AboutMe.tsx index 8297d809..4c67013e 100644 --- a/src/components/profile/AboutMe.tsx +++ b/src/components/profile/AboutMe.tsx @@ -1,11 +1,11 @@ 'use client'; -import { FileText, Pencil } from 'lucide-react'; +import { SquareUserRound, Pencil } from 'lucide-react'; import React, { useState } from 'react'; import { Button } from '@/components/ui/button'; import SheetWrapper from './sheets/SheetWrapper'; -import ReadMeForm from './forms/ReadMeForm'; import { SHEETS } from '@/lib/constant/profile.constant'; import ProfileEmptyContainers from './emptycontainers/ProfileEmptyContainers'; +import AboutMeForm from './forms/ReadMeForm'; const ProfileAboutMe = ({ aboutMe, @@ -55,7 +55,7 @@ const ProfileAboutMe = ({ ? 'Share a brief introduction to let companies know who you are.' : '' } - Icon={FileText} + Icon={SquareUserRound} /> )} {aboutMe && ( @@ -70,7 +70,7 @@ const ProfileAboutMe = ({ title={title} description={SHEETS.aboutMe.description} > - + )} diff --git a/src/components/profile/ProfileEducation.tsx b/src/components/profile/ProfileEducation.tsx index 2bbb6a17..c9ddb673 100644 --- a/src/components/profile/ProfileEducation.tsx +++ b/src/components/profile/ProfileEducation.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Circle, Info, Pencil, Plus } from 'lucide-react'; +import { Circle, BookOpenCheck, Pencil, Plus } from 'lucide-react'; import React, { useState } from 'react'; import { Button } from '../ui/button'; import SheetWrapper from './sheets/SheetWrapper'; @@ -68,7 +68,7 @@ const ProfileEducation = ({ ? 'Provide your education background to complete your profile.' : '' } - Icon={Info} + Icon={BookOpenCheck} /> )} {education.length !== 0 && ( diff --git a/src/components/profile/ProfileExperience.tsx b/src/components/profile/ProfileExperience.tsx index 097c9d38..a877d1c2 100644 --- a/src/components/profile/ProfileExperience.tsx +++ b/src/components/profile/ProfileExperience.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Circle, Info, Pencil, Plus } from 'lucide-react'; +import { Circle, Building2, Pencil, Plus } from 'lucide-react'; import React, { useState } from 'react'; import { Button } from '../ui/button'; import SheetWrapper from './sheets/SheetWrapper'; @@ -73,7 +73,7 @@ const ProfileExperience = ({ ? 'Share your experience to attract the right companies.' : '' } - Icon={Info} + Icon={Building2} /> )} diff --git a/src/components/profile/ProfileHeroSection.tsx b/src/components/profile/ProfileHeroSection.tsx index 30d5907d..71e7b6c4 100644 --- a/src/components/profile/ProfileHeroSection.tsx +++ b/src/components/profile/ProfileHeroSection.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; -import { Pencil, Settings, Share2 } from 'lucide-react'; +import { Pencil, Settings, User } from 'lucide-react'; import SheetWrapper from './sheets/SheetWrapper'; import EditProfileForm from './forms/EditProfileForm'; import { SHEETS } from '@/lib/constant/profile.constant'; @@ -10,6 +10,7 @@ import AccountSeetingForm from './forms/AccountSeetingForm'; import { useSession } from 'next-auth/react'; import { UserType } from '@/types/user.types'; import ProfileSocials from './ProfileSocials'; +import { ProfileShareDialog } from './ProfileShare'; const ProfileHeroSection = ({ userdetails }: { userdetails: UserType }) => { const [isSheetOpen, setIsSheetOpen] = useState(false); @@ -31,11 +32,17 @@ const ProfileHeroSection = ({ userdetails }: { userdetails: UserType }) => {
- + {userdetails.avatar && ( )} - {userdetails.name[0]} + + +
{status === 'authenticated' && data.user.id === userdetails.id && ( @@ -54,17 +61,12 @@ const ProfileHeroSection = ({ userdetails }: { userdetails: UserType }) => { > - )} +

{userdetails.name}

- - @{userdetails.username} -
diff --git a/src/components/profile/ProfileProjects.tsx b/src/components/profile/ProfileProjects.tsx index 7a60fb6e..f913238c 100644 --- a/src/components/profile/ProfileProjects.tsx +++ b/src/components/profile/ProfileProjects.tsx @@ -1,5 +1,5 @@ 'use client'; -import { ChevronDown, ChevronUp, Info, Plus } from 'lucide-react'; +import { ChevronDown, ChevronUp, FileStack, Plus } from 'lucide-react'; import React, { useMemo, useState } from 'react'; import { Button } from '@/components/ui/button'; import SheetWrapper from './sheets/SheetWrapper'; @@ -38,13 +38,16 @@ const ProfileProjects = ({ setIsSeeMore(!isSeeMore); }; - const featuredProjects = useMemo(() => { - return projects.filter((project) => project.isFeature === true); - }, [projects]); - - const nonFeaturedProjects = useMemo(() => { - return projects.filter((project) => project.isFeature === false); - }, [projects]); + const allProjects = useMemo(() => { + return projects + .filter((project) => { + if (!isSeeMore) { + return project.isFeature === true; + } + return true; + }) + .sort((a, b) => Number(b.isFeature) - Number(a.isFeature)); + }, [projects, isSeeMore]); const title = selectedProject ? SHEETS.project.title.replace('Add New', 'Edit') @@ -65,7 +68,7 @@ const ProfileProjects = ({ )}
- {featuredProjects.length === 0 && ( + {projects.length === 0 && ( )} - {featuredProjects.length !== 0 && !isSeeMore && ( + {projects.length !== 0 && ( <>
- {featuredProjects.map((project) => ( - - ))} -
- - - )} - {projects.length !== 0 && isSeeMore && ( - <> -
- {[...featuredProjects, ...nonFeaturedProjects].map((project) => ( + {allProjects.map((project) => ( - Hide + {isSeeMore + ? 'Hide' + : allProjects.length === 0 + ? 'Show non featured projects' + : 'See More'} + {isSeeMore ? ( + + ) : ( + + )} )} diff --git a/src/components/profile/ProfileShare.tsx b/src/components/profile/ProfileShare.tsx new file mode 100644 index 00000000..ca1f7103 --- /dev/null +++ b/src/components/profile/ProfileShare.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Twitter, Linkedin, Share2, Copy } from 'lucide-react'; +import { useToast } from '../ui/use-toast'; + +interface ShareOption { + name: string; + icon: React.ReactNode; + shareFunction: () => void; +} + +export const ProfileShareDialog = () => { + const { toast } = useToast(); + + const shareOptions: ShareOption[] = [ + { + name: 'Twitter', + icon: , + shareFunction: () => { + const text = encodeURIComponent( + `Check out my new profile at 100xdevs Job-Board: ${window.location.href}` + ); + window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank'); + }, + }, + { + name: 'LinkedIn', + icon: , + shareFunction: () => { + const url = encodeURIComponent(window.location.href); + const title = encodeURIComponent('My New Profile'); + const summary = encodeURIComponent( + `Excited to share my new profile on 100xdevs Job-Board! Check it out here: ${url} #JobSearch #Hiring #OpenToWork` + ); + window.open( + `https://www.linkedin.com/sharing/share-offsite/?url=${url}&title=${title}&summary=${summary}`, + '_blank' + ); + }, + }, + { + name: 'Copy', + icon: , + shareFunction: () => { + window.navigator.clipboard.writeText(window.location.href); + toast({ + variant: 'success', + description: 'Successfully copied the Profile Url.', + }); + }, + }, + ]; + + return ( + + + + + + + Share Job + +
+ {shareOptions.map((option) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/components/profile/emptycontainers/ProfileEmptyContainers.tsx b/src/components/profile/emptycontainers/ProfileEmptyContainers.tsx index 51d3401c..025caf44 100644 --- a/src/components/profile/emptycontainers/ProfileEmptyContainers.tsx +++ b/src/components/profile/emptycontainers/ProfileEmptyContainers.tsx @@ -18,10 +18,16 @@ const ProfileEmptyContainers = ({ }) => { return (
- +

{title}

-

{description}

+

+ {description} +

{isOwner && (
+

+ {' '} + {!previewImg && + form.formState.errors.projectThumbnail?.message && + 'Project Thumbnail is required.'}{' '} +

Describe yourself between 50 to 255 characters. + )} /> @@ -98,7 +100,11 @@ const ReadMeForm = ({ type="submit" className="mt-0 text-white rounded-[8px]" > - {form.formState.isSubmitting ? 'Please Wait...' : 'Add About Me'} + {form.formState.isSubmitting + ? 'Please Wait...' + : aboutMe + ? 'Update About Me' + : 'Add About Me'} @@ -107,4 +113,4 @@ const ReadMeForm = ({ ); }; -export default ReadMeForm; +export default AboutMeForm; diff --git a/src/components/profile/forms/SkillsForm.tsx b/src/components/profile/forms/SkillsForm.tsx index 2fd1afa2..2e9ad5c5 100644 --- a/src/components/profile/forms/SkillsForm.tsx +++ b/src/components/profile/forms/SkillsForm.tsx @@ -8,9 +8,8 @@ import { import { addUserSkills } from '@/actions/user.profile.actions'; import { useToast } from '@/components/ui/use-toast'; import { Form } from '@/components/ui/form'; -import { SkillsCombobox } from '@/components/skills-combobox'; -import { LoadingSpinner } from '@/components/loading-spinner'; import { Button } from '@/components/ui/button'; +import { ProfileSkillsCombobox } from '../profile-skills-combobox'; export const SkillsForm = ({ handleClose, @@ -21,7 +20,6 @@ export const SkillsForm = ({ }) => { const [comboBoxSelectedValues, setComboBoxSelectedValues] = useState(skills); - const [isLoading, setIsLoading] = useState(false); const form = useForm({ resolver: zodResolver(addSkillsSchema), @@ -32,7 +30,6 @@ export const SkillsForm = ({ const { toast } = useToast(); const onSubmit = async (data: addSkillsSchemaType) => { try { - setIsLoading(true); const response = await addUserSkills(data); if (!response.status) { return toast({ @@ -53,7 +50,6 @@ export const SkillsForm = ({ variant: 'destructive', }); } finally { - setIsLoading(false); handleClose(); } }; @@ -72,16 +68,11 @@ export const SkillsForm = ({ className="flex h-full flex-col justify-between" >
- - {isLoading && ( -
- {' '} -
- )} + >
+ } +
+ + ))} + + )} + + ); +} diff --git a/src/components/profile/profileComboBox.tsx b/src/components/profile/profileComboBox.tsx new file mode 100644 index 00000000..16f0fdec --- /dev/null +++ b/src/components/profile/profileComboBox.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { LoadingSpinner } from '../loading-spinner'; + +export type TcomboBoxValue = { value: string; label: string }; + +export function ProfileComboBox({ + dropdownValues, + setComboBoxInputValue, + isLoading, + setComboBoxSelectedValues, + comboBoxSelectedValues, +}: { + comboBoxSelectedValues: string[]; + isLoading: boolean; + setComboBoxInputValue: React.Dispatch>; + dropdownValues: TcomboBoxValue[]; + setComboBoxSelectedValues: React.Dispatch>; +}) { + const [open, setOpen] = React.useState(false); + const [value, setValue] = React.useState(''); + + return ( + + + + + + + { + setComboBoxInputValue(value); + }} + placeholder="Search skillset ..." + /> + + {isLoading ? ( + + + + ) : ( + <> + {!dropdownValues.length && ( + No framework found. + )} + + + {dropdownValues.map((item) => ( + { + setValue(currentValue === value ? '' : currentValue); + setOpen(false); + setComboBoxSelectedValues((prev) => { + const foundSelectedValueIndex = prev.findIndex( + (val) => val === item.value + ); + if (foundSelectedValueIndex < 0) { + return [...prev, currentValue]; + } else return prev; + }); + }} + > + + {item.label} + + ))} + + + )} + + + + + ); +} diff --git a/src/components/skills-combobox.tsx b/src/components/skills-combobox.tsx index 53b3ed69..6977dfb8 100644 --- a/src/components/skills-combobox.tsx +++ b/src/components/skills-combobox.tsx @@ -74,7 +74,7 @@ export function SkillsCombobox({ return ( <>
- Skills + Skills Required
- {comboBoxSelectedValues.length !== 0 && ( -
- {comboBoxSelectedValues.map((item, index) => ( -
-
- {_.startCase(item.toLowerCase())} - { - - } -
+ +
+ {comboBoxSelectedValues.map((item, index) => ( +
+
+ {_.startCase(item.toLowerCase())} + { + + }
- ))} -
- )} +
+ ))} +
); } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index b6e19639..f5c44e4d 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -105,7 +105,6 @@ export const options = { email: email, password: hashedPassword, name: name, - username: 'random', // todo username }, select: { id: true, diff --git a/src/lib/authOptions.ts b/src/lib/authOptions.ts index 5ed400bb..c34b5d0a 100644 --- a/src/lib/authOptions.ts +++ b/src/lib/authOptions.ts @@ -96,7 +96,6 @@ export const authOptions = { oauthProvider: 'GOOGLE', email: email as string, name: name as string, - username: 'ishadfoi', //to do avatar, isVerified: true, emailVerified: new Date(), diff --git a/src/lib/validators/user.profile.validator.ts b/src/lib/validators/user.profile.validator.ts index 5e35bd35..a9d36a90 100644 --- a/src/lib/validators/user.profile.validator.ts +++ b/src/lib/validators/user.profile.validator.ts @@ -83,15 +83,16 @@ export const aboutMeSchema = z.object({ aboutMe: z .string() .min(50, { message: 'Description must be at least 50 characters' }) - .max(255, { message: 'Description cannot exceed 255 characters' }), + .max(255, { message: 'Description cannot exceed 255 characters' }) + .optional() + .or(z.literal('')), }); export const profileSchema = z.object({ avatar: z.string().optional(), name: z.string().min(1, 'Name is required'), - username: z.string().min(1, 'Username is required'), email: z.string().min(1, 'Email is required').email(), - contactEmail: z.string().email().optional(), + contactEmail: z.string().email().optional().or(z.literal('')), aboutMe: z .string() .min(50, { message: 'Description must be at least 50 characters' }) @@ -131,7 +132,7 @@ export const profileResumeSchema = z.object({ }); export const profileProjectSchema = z.object({ - projectThumbnail: z.string().optional(), + projectThumbnail: z.string().min(1, 'Project Thumbnail is required.'), projectName: z.string().min(1, 'Project name is required'), projectSummary: z .string() @@ -143,7 +144,8 @@ export const profileProjectSchema = z.object({ .refine((url) => url.startsWith('https://'), { message: 'URL must be a https request', }) - .optional(), + .optional() + .or(z.literal('')), projectGithub: z .string({ message: 'Github Link is required' }) .url({ message: 'Invalid URL format' }) diff --git a/src/types/user.types.ts b/src/types/user.types.ts index 54d92389..e0369982 100644 --- a/src/types/user.types.ts +++ b/src/types/user.types.ts @@ -38,7 +38,6 @@ export interface ExperienceType { export interface UserType { name: string; - username: string; id: string; email: string; skills: string[];