Skip to content

Commit

Permalink
updates account settings
Browse files Browse the repository at this point in the history
  • Loading branch information
christianhelp committed Jan 4, 2025
1 parent a3f1832 commit 2c31028
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 148 deletions.
15 changes: 4 additions & 11 deletions apps/web/src/actions/user-profile-mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import { eq } from "db/drizzle";
import { del, put } from "@vercel/blob";
import { decodeBase64AsFile } from "@/lib/utils/shared/files";
import { revalidatePath } from "next/cache";
import { getUser, getUserByTag } from "db/functions";
import { RegistrationSettingsFormValidator } from "@/validators/shared/RegistrationSettingsForm";
import { UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE } from "@/lib/constants";
import c from "config";
import { DatabaseError } from "db/types";
import { registrationSettingsFormValidator, modifyAccountSettingsSchema } from "@/validators/settings";

export const modifyRegistrationData = authenticatedAction
.schema(RegistrationSettingsFormValidator)
.schema(registrationSettingsFormValidator)
.action(
async ({
parsedInput: {
Expand Down Expand Up @@ -152,23 +151,17 @@ export const modifyProfileData = authenticatedAction
},
);

// 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(),
}),
modifyAccountSettingsSchema,
)
.action(
async ({
parsedInput: {
firstName,
lastName,
hackerTag,
hasSearchableProfile,
isSearchable: hasSearchableProfile,
},
ctx: { userId },
}) => {
Expand Down
271 changes: 144 additions & 127 deletions apps/web/src/components/settings/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,36 @@ import { Input } from "@/components/shadcn/ui/input";
import { Button } from "@/components/shadcn/ui/button";
import { Label } from "@/components/shadcn/ui/label";
import { toast } from "sonner";
import { useState } from "react";
import { useEffect, 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";
import { modifyAccountSettingsSchema } from "@/validators/settings";
import z from "zod"
import { useForm} from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../shadcn/ui/form";
import { User } from "@clerk/nextjs/server";

interface UserProps {
firstName: string;
lastName: string;
email: string;
hackerTag: string;
isSearchable: boolean;
}
type UserProps = z.infer<typeof modifyAccountSettingsSchema>;

export default function AccountSettings({ user }: { user: UserProps }) {
const [newFirstName, setNewFirstName] = useState(user.firstName);
const [newLastName, setNewLastName] = useState(user.lastName);
const [newHackerTag, setNewHackerTag] = useState(user.hackerTag);
const [newIsProfileSearchable, setNewIsProfileSearchable] = useState(
user.isSearchable,
);
const [hackerTagTakenAlert, setHackerTagTakenAlert] = useState(false);
console.log(user);
const form = useForm<UserProps>({
resolver:zodResolver(modifyAccountSettingsSchema),
defaultValues:user
});

const { execute: runModifyAccountSettings, status: loadingState } =
useAction(modifyAccountSettings, {
Expand All @@ -36,9 +42,18 @@ export default function AccountSettings({ user }: { user: UserProps }) {
if (!data?.success) {
if (data?.message == "hackertag_not_unique") {
toast.error("Hackertag already exists");
setHackerTagTakenAlert(true);
form.setError("hackerTag",{
message:"Hackertag already exists"
});
}
} else toast.success("Account updated successfully!");
} else {
toast.success("Account updated successfully!",{
duration:1500
});
form.reset({
...form.getValues()
})
};
},
onError: () => {
toast.dismiss();
Expand All @@ -48,122 +63,124 @@ export default function AccountSettings({ user }: { user: UserProps }) {
},
});

function handleSubmit(data:UserProps){
toast.dismiss();
if (!form.formState.isDirty){
toast.error("Please change something before updating");
return;
}
runModifyAccountSettings(data);
}


return (
<main>
<div className="rounded-lg border-2 border-muted px-5 py-10">
<h2 className="pb-5 text-3xl font-semibold">
Personal Information
</h2>
<div className="grid max-w-[500px] grid-cols-2 gap-x-2 gap-y-2">
<div>
<Label htmlFor="firstname">First Name</Label>
<Input
className="mt-2"
name="firstname"
value={newFirstName}
onChange={(e) => setNewFirstName(e.target.value)}
/>
{!newFirstName ? (
<div className={"mt-1 text-sm text-red-500"}>
This field can't be empty!
</div>
) : null}
</div>
<div>
<Label htmlFor={"lastname"}>Last Name</Label>
<Input
className="mt-2"
name="lastname"
value={newLastName}
onChange={(e) => setNewLastName(e.target.value)}
/>
{!newLastName ? (
<div className={"mt-1 text-sm text-red-500"}>
This field can't be empty!
</div>
) : null}
</div>
</div>
<h2 className="pb-5 pt-7 text-3xl font-semibold">
Public Information
</h2>
<div className="grid max-w-[500px] grid-cols-1 gap-x-2 gap-y-2">
<div>
<Label htmlFor="hackertag">HackerTag</Label>
<div className="mt-2 flex">
<div className="flex h-10 w-10 items-center justify-center rounded-l bg-accent text-lg font-light text-primary">
@
</div>
<Input
className="rounded-l-none"
placeholder={`${c.hackathonName.toLowerCase()}`}
value={newHackerTag}
onChange={(e) => {
setNewHackerTag(e.target.value);
setHackerTagTakenAlert(false);
}}
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<div className="rounded-lg border-2 border-muted px-5 py-10">
<h2 className="pb-5 text-3xl font-semibold">
Personal Information
</h2>
<div className="grid max-w-[500px] grid-cols-2 gap-x-2 gap-y-2">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input
placeholder="shadcn"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input
placeholder="shadcn"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{hackerTagTakenAlert ? (
<div className={"text-sm text-red-500"}>
HackerTag is already taken!
</div>
) : (
""
)}
{!newHackerTag ? (
<div className={"mt-1 text-sm text-red-500"}>
This field can't be empty!
</div>
) : null}
</div>
<div
className={
"flex max-w-[600px] flex-row items-start space-x-3 space-y-0 rounded-md border p-4"
}
>
<Checkbox
checked={newIsProfileSearchable}
onCheckedChange={() =>
setNewIsProfileSearchable(
!newIsProfileSearchable,
)
<h2 className="pb-5 pt-7 text-3xl font-semibold">
Public Information
</h2>
<div className="grid max-w-[500px] grid-cols-1 gap-x-2 gap-y-2">
<FormField
control={form.control}
name="hackerTag"
render={({ field }) => (
<FormItem>
<FormLabel>Hacker Tag</FormLabel>
<FormControl>
<div className="mt-2 flex">
<div className="flex h-10 w-10 items-center justify-center rounded-l bg-accent text-lg font-light text-primary">
@
</div>
<Input
placeholder="shadcn"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isSearchable"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Make my profile searchable by other hackers
</FormLabel>
</div>
</FormItem>
)}
/>
</div>
<Button
className="mt-5"
type="submit"
disabled={
loadingState === "executing"
}
/>
<Label htmlFor="profileIsSearchable">
Make my profile searchable by other Hackers
</Label>
>
{loadingState === "executing" ? (
<>
<Loader2
className={"mr-2 h-4 w-4 animate-spin"}
/>
<div>Updating</div>
</>
) : (
"Update"
)}
</Button>
</div>
</div>
<Button
className="mt-5"
onClick={() => {
if (isProfane(newHackerTag)) {
toast.dismiss();
toast.error("Profanity is not allowed");
return;
}
toast.loading("Updating settings...");
runModifyAccountSettings({
firstName: newFirstName,
lastName: newLastName,
//email: newEmail,
hackerTag: newHackerTag,
hasSearchableProfile: newIsProfileSearchable,
});
}}
disabled={loadingState === "executing"}
>
{loadingState === "executing" ? (
<>
<Loader2 className={"mr-2 h-4 w-4 animate-spin"} />
<div>Updating</div>
</>
) : (
"Update"
)}
</Button>
</div>
</form>
</Form>
</main>
);
}
Loading

0 comments on commit 2c31028

Please sign in to comment.