diff --git a/.gitignore b/.gitignore index 9f67afc6..adc80bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ yarn-error.log* # vscode .vscode + +#Jetbrians +.idea \ No newline at end of file diff --git a/apps/web/src/actions/user-profile-mod.ts b/apps/web/src/actions/user-profile-mod.ts index 66693bce..1afb8131 100644 --- a/apps/web/src/actions/user-profile-mod.ts +++ b/apps/web/src/actions/user-profile-mod.ts @@ -3,54 +3,198 @@ import { authenticatedAction } from "@/lib/safe-action"; import { z } from "zod"; import { db } from "db"; -import { userCommonData } from "db/schema"; +import { userCommonData, userHackerData } from "db/schema"; import { eq } from "db/drizzle"; import { put } from "@vercel/blob"; import { decodeBase64AsFile } from "@/lib/utils/shared/files"; import { returnValidationErrors } from "next-safe-action"; import { revalidatePath } from "next/cache"; -import { getUser } from "db/functions"; +import { getUser, getUserByTag } from "db/functions"; +import { RegistrationSettingsFormValidator } from "@/validators/shared/RegistrationSettingsForm"; -// TODO: Add skill updating export const modifyRegistrationData = authenticatedAction + .schema(RegistrationSettingsFormValidator) + .action( + async ({ + parsedInput: { + age, + gender, + race, + ethnicity, + isEmailable, + university, + major, + levelOfStudy, + schoolID, + hackathonsAttended, + softwareBuildingExperience, + heardAboutEvent, + shirtSize, + dietaryRestrictions, + accommodationNote, + github, + linkedin, + personalWebsite, + phoneNumber, + countryOfResidence, + }, + ctx: { userId }, + }) => { + const user = await getUser(userId); + if (!user) throw new Error("User not found"); + await Promise.all([ + db + .update(userCommonData) + .set({ + age, + gender, + race, + ethnicity, + shirtSize, + dietRestrictions: dietaryRestrictions, + accommodationNote, + phoneNumber, + countryOfResidence, + }) + .where(eq(userCommonData.clerkID, user.clerkID)), + db + .update(userHackerData) + .set({ + isEmailable, + university, + major, + levelOfStudy, + schoolID, + hackathonsAttended, + softwareExperience: softwareBuildingExperience, + heardFrom: heardAboutEvent, + GitHub: github, + LinkedIn: linkedin, + PersonalWebsite: personalWebsite, + }) + .where(eq(userHackerData.clerkID, user.clerkID)), + ]); + return { + success: true, + newAge: age, + newGender: gender, + newRace: race, + newEthnicity: ethnicity, + newWantsToReceiveMLHEmails: isEmailable, + newUniversity: university, + newMajor: major, + newLevelOfStudy: levelOfStudy, + newSchoolID: schoolID, + newHackathonsAttended: hackathonsAttended, + newSoftwareExperience: softwareBuildingExperience, + newHeardFrom: heardAboutEvent, + newShirtSize: shirtSize, + newDietaryRestrictions: dietaryRestrictions, + newAccommodationNote: accommodationNote, + newGitHub: github, + newLinkedIn: linkedin, + newPersonalWebsite: personalWebsite, + newPhoneNumber: phoneNumber, + newCountryOfResidence: countryOfResidence, + }; + }, + ); + +export const modifyResume = authenticatedAction .schema( z.object({ - bio: z.string().max(500), - skills: z.string().max(100), + resume: z.string(), }), ) - .action(async ({ parsedInput: { bio, skills }, ctx: { userId } }) => { - const user = await getUser(userId); - if (!user) - returnValidationErrors(z.null(), { _errors: ["User not found"] }); - + .action(async ({ parsedInput: { resume }, ctx: { userId } }) => { await db - .update(userCommonData) - .set({ bio }) - .where(eq(userCommonData.clerkID, user.clerkID)); - return { success: true, newbio: bio }; + .update(userHackerData) + .set({ resume }) + .where(eq(userHackerData.clerkID, userId)); + return { + success: true, + newResume: resume, + }; }); +export const modifyProfileData = authenticatedAction + .schema( + z.object({ + pronouns: z.string(), + bio: z.string(), + skills: z.string().array(), + discord: z.string(), + }), + ) + .action( + async ({ + parsedInput: { bio, discord, pronouns, skills }, + ctx: { userId }, + }) => { + const user = await getUser(userId); + if (!user) { + throw new Error("User not found"); + } + await db + .update(userCommonData) + .set({ pronouns, bio, skills, discord }) + .where(eq(userCommonData.clerkID, user.clerkID)); + return { + success: true, + newPronouns: pronouns, + newBio: bio, + newSkills: skills, + newDiscord: discord, + }; + }, + ); + +// TODO: Fix after registration enhancements to allow for failure on conflict and return appropriate error message export const modifyAccountSettings = authenticatedAction .schema( z.object({ firstName: z.string().min(1).max(50), lastName: z.string().min(1).max(50), + hackerTag: z.string().min(1).max(50), + hasSearchableProfile: z.boolean(), }), ) .action( - async ({ parsedInput: { firstName, lastName }, ctx: { userId } }) => { + async ({ + parsedInput: { + firstName, + lastName, + hackerTag, + hasSearchableProfile, + }, + ctx: { userId }, + }) => { const user = await getUser(userId); if (!user) throw new Error("User not found"); - + let oldHackerTag = user.hackerTag; // change when hackertag is not PK on profileData table + if (oldHackerTag != hackerTag) + if (await getUserByTag(hackerTag)) + //if hackertag changed + // copied from /api/registration/create + return { + success: false, + message: "hackertag_not_unique", + }; await db .update(userCommonData) - .set({ firstName, lastName }) + .set({ + firstName, + lastName, + hackerTag, + isSearchable: hasSearchableProfile, + }) .where(eq(userCommonData.clerkID, userId)); return { success: true, newFirstName: firstName, newLastName: lastName, + newHackerTag: hackerTag, + newHasSearchableProfile: hasSearchableProfile, }; }, ); @@ -60,11 +204,10 @@ export const updateProfileImage = authenticatedAction .action( async ({ parsedInput: { fileBase64, fileName }, ctx: { userId } }) => { const image = await decodeBase64AsFile(fileBase64, fileName); - const user = await getUser(userId); - if (!user) - returnValidationErrors(z.null(), { - _errors: ["User not found"], - }); + const user = await db.query.userCommonData.findFirst({ + where: eq(userCommonData.clerkID, userId), + }); + if (!user) throw new Error("User not found"); const blobUpload = await put(image.name, image, { access: "public", diff --git a/apps/web/src/app/settings/account/page.tsx b/apps/web/src/app/settings/account/page.tsx deleted file mode 100644 index fbfdd1e0..00000000 --- a/apps/web/src/app/settings/account/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import AccountSettings from "@/components/settings/AccountSettings"; -import { auth } from "@clerk/nextjs"; -import { redirect } from "next/navigation"; -import { getHacker } from "db/functions"; - -export default async function Page() { - const { userId } = auth(); - if (!userId) return redirect("/sign-in"); - - const user = await getHacker(userId, false); - if (!user) return redirect("/sign-in"); - return ; -} - -export const runtime = "edge"; diff --git a/apps/web/src/app/settings/layout.tsx b/apps/web/src/app/settings/layout.tsx index f0497ab5..560f9dd9 100644 --- a/apps/web/src/app/settings/layout.tsx +++ b/apps/web/src/app/settings/layout.tsx @@ -34,12 +34,15 @@ export default async function ({ children }: { children: ReactNode }) { -
- {/* */} - - -
-
{children}
+ +
{children}
); diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 9c4722c4..685c8251 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -1,3 +1,32 @@ -export default function Page() { - return howdy; +import AccountSettings from "@/components/settings/AccountSettings"; +import { auth } from "@clerk/nextjs"; +import { redirect } from "next/navigation"; +import ProfileSettings from "@/components/settings/ProfileSettings"; +import RegistrationSettings from "@/components/settings/RegistrationSettings"; +import { getUser } from "db/functions"; + +export default async function Page() { + const { userId } = auth(); + if (!userId) return redirect("/sign-in"); + const user = await getUser(userId); + if (!user) return redirect("/sign-in"); + + return ( +
+
+ +
+ +
+ +
+ ); +} + +function Header({ tag }: { tag: string }) { + return ( +

+ {tag} +

+ ); } diff --git a/apps/web/src/app/settings/profile/page.tsx b/apps/web/src/app/settings/profile/page.tsx deleted file mode 100644 index 06e9a82d..00000000 --- a/apps/web/src/app/settings/profile/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import ProfileSettings from "@/components/settings/ProfileSettings"; -import { auth } from "@clerk/nextjs"; -import { getHacker } from "db/functions"; - -export default async function Page() { - const { userId } = auth(); - if (!userId) throw new Error("User not found"); - - const user = await getHacker(userId, false); - if (!user) throw new Error("User not found"); - return ( - - ); -} - -export const runtime = "edge"; diff --git a/apps/web/src/app/settings/registration/page.tsx b/apps/web/src/app/settings/registration/page.tsx new file mode 100644 index 00000000..ba11a134 --- /dev/null +++ b/apps/web/src/app/settings/registration/page.tsx @@ -0,0 +1,15 @@ +import { auth } from "@clerk/nextjs"; +import { redirect } from "next/navigation"; +import RegistrationFormSettings from "@/components/settings/RegistrationForm/RegisterFormSettings"; +import { getHackerData, getUser } from "db/functions"; + +export default async function Page() { + const { userId } = auth(); + if (!userId) return redirect("/sign-in"); + const user = await getUser(userId); + if (!user) return redirect("/sign-in"); + const hackerData = await getHackerData(userId); + if (!hackerData) return redirect("/sign-in"); + + return ; +} diff --git a/apps/web/src/components/registration/RegisterForm.tsx b/apps/web/src/components/registration/RegisterForm.tsx index 85feddfe..1658a8ff 100644 --- a/apps/web/src/components/registration/RegisterForm.tsx +++ b/apps/web/src/components/registration/RegisterForm.tsx @@ -116,10 +116,6 @@ export default function RegisterForm({ defaultEmail }: RegisterFormProps) { } }, [universityValue]); - useEffect(() => { - console.log(countryValue); - }, [countryValue]); - async function onSubmit(data: z.infer) { console.log(data); setIsLoading(true); diff --git a/apps/web/src/components/settings/AccountSettings.tsx b/apps/web/src/components/settings/AccountSettings.tsx index f05151f9..9e693dad 100644 --- a/apps/web/src/components/settings/AccountSettings.tsx +++ b/apps/web/src/components/settings/AccountSettings.tsx @@ -7,32 +7,47 @@ import { toast } from "sonner"; import { useState } from "react"; import { useAction } from "next-safe-action/hooks"; import { modifyAccountSettings } from "@/actions/user-profile-mod"; +import { Checkbox } from "@/components/shadcn/ui/checkbox"; +import c from "config"; +import { Loader2 } from "lucide-react"; +import { isProfane } from "no-profanity"; interface UserProps { firstName: string; lastName: string; -} -interface AccountSettingsProps { - user: UserProps; + email: string; + hackerTag: string; + isSearchable: boolean; } -export default function AccountSettings({ user }: AccountSettingsProps) { +export default function AccountSettings({ user }: { user: UserProps }) { const [newFirstName, setNewFirstName] = useState(user.firstName); const [newLastName, setNewLastName] = useState(user.lastName); + //const [newEmail, setNewEmail] = useState(user.email); + const [newHackerTag, setNewHackerTag] = useState(user.hackerTag); + const [newIsProfileSearchable, setNewIsProfileSearchable] = useState( + user.isSearchable, + ); + const [hackerTagTakenAlert, setHackerTagTakenAlert] = useState(false); - const { execute: runModifyAccountSettings } = useAction( - modifyAccountSettings, - { - onSuccess: () => { + const { execute: runModifyAccountSettings, status: loadingState } = + useAction(modifyAccountSettings, { + onSuccess: ({ data }) => { toast.dismiss(); - toast.success("Name updated successfully!"); + if (!data?.success) { + if (data?.message == "hackertag_not_unique") { + toast.error("Hackertag already exists"); + setHackerTagTakenAlert(true); + } + } else toast.success("Account updated successfully!"); }, onError: () => { toast.dismiss(); - toast.error("An error occurred while updating your name!"); + toast.error( + "An error occurred while updating your account settings!", + ); }, - }, - ); + }); return (
@@ -40,7 +55,7 @@ export default function AccountSettings({ user }: AccountSettingsProps) {

Personal Information

-
+
setNewFirstName(e.target.value)} /> + {!newFirstName ? ( +
+ This field can't be empty! +
+ ) : null}
- + setNewLastName(e.target.value)} /> + {!newLastName ? ( +
+ This field can't be empty! +
+ ) : null}
-
+

+ Public Information +

+
+
+ +
+
+ @ +
+ { + setNewHackerTag(e.target.value); + setHackerTagTakenAlert(false); + }} + /> +
+ {hackerTagTakenAlert ? ( +
+ HackerTag is already taken! +
+ ) : ( + "" + )} + {!newHackerTag ? ( +
+ This field can't be empty! +
+ ) : null} +
+
- Update - + + setNewIsProfileSearchable( + !newIsProfileSearchable, + ) + } + /> + +
+
); diff --git a/apps/web/src/components/settings/ProfileSettings.tsx b/apps/web/src/components/settings/ProfileSettings.tsx index 792a6a3b..8f455aaa 100644 --- a/apps/web/src/components/settings/ProfileSettings.tsx +++ b/apps/web/src/components/settings/ProfileSettings.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/shadcn/ui/button"; import { Label } from "@/components/shadcn/ui/label"; import { Textarea } from "@/components/shadcn/ui/textarea"; import { - modifyRegistrationData, + modifyProfileData, updateProfileImage, } from "@/actions/user-profile-mod"; import { useUser } from "@clerk/nextjs"; @@ -13,41 +13,72 @@ import { useAction } from "next-safe-action/hooks"; import { toast } from "sonner"; import { useState } from "react"; import { encodeFileAsBase64 } from "@/lib/utils/shared/files"; +import { Tag, TagInput } from "@/components/shadcn/ui/tag/tag-input"; +import { Loader2 } from "lucide-react"; +import { Avatar, AvatarImage } from "@/components/shadcn/ui/avatar"; -interface ProfileSettingsProps { +interface ProfileData { + pronouns: string; bio: string; - university: string; + skills: string[]; + discord: string | null; + profilePhoto: string; } -export default function ProfileSettings({ - bio, - university, -}: ProfileSettingsProps) { - const [newBio, setNewBio] = useState(bio); - const [newUniversity, setNewUniversity] = useState(university); + +interface ProfileSettingsProps { + profile: ProfileData; +} + +export default function ProfileSettings({ profile }: ProfileSettingsProps) { + const [newPronouns, setNewPronouns] = useState(profile.pronouns); + const [newBio, setNewBio] = useState(profile.bio); const [newProfileImage, setNewProfileImage] = useState(null); + let curSkills: Tag[] = []; + // for (let i = 0; i < profile.skills.length; i++) { + // let t: Tag = { + // id: profile.skills[i], + // text: profile.skills[i], + // }; + // curSkills.push(t); + // } + profile.skills.map((skill) => { + curSkills.push({ + id: skill, + text: skill, + }); + }); + const [newSkills, setNewSkills] = useState(curSkills); + const [newDiscord, setNewDiscord] = useState(profile.discord || ""); + + const [isProfilePictureLoading, setIsProfilePictureLoading] = + useState(false); + const [isProfileSettingsLoading, setIsProfileSettingsLoading] = + useState(false); + const { user } = useUser(); - const { execute: runModifyRegistrationData } = useAction( - modifyRegistrationData, - { - onSuccess: () => { - toast.dismiss(); - toast.success("Profile updated successfully!"); - }, - onError: () => { - toast.dismiss(); - toast.error("An error occurred while updating your profile!"); - }, + const { execute: runModifyProfileData } = useAction(modifyProfileData, { + onSuccess: () => { + setIsProfileSettingsLoading(false); + toast.dismiss(); + toast.success("Profile updated successfully!"); }, - ); + onError: () => { + setIsProfileSettingsLoading(false); + toast.dismiss(); + toast.error("An error occurred while updating your profile!"); + }, + }); const { execute: runUpdateProfileImage } = useAction(updateProfileImage, { onSuccess: async () => { + setIsProfilePictureLoading(false); toast.dismiss(); await user?.setProfileImage({ file: newProfileImage }); toast.success("Profile Photo updated successfully!"); }, onError: (err) => { + setIsProfilePictureLoading(false); toast.dismiss(); toast.error("An error occurred while updating your profile photo!"); console.error(err); @@ -65,7 +96,12 @@ export default function ProfileSettings({

Profile Photo

- + + +

Profile Data

-
- {/*
- - setNewUniversity(e.target.value)} - /> -
*/} +
+ + setNewPronouns(e.target.value)} + /> +
+
- +