Major addition. User register, login and update is now functional on frontend, removed UUID5 for UUID4 for users and many more changes

This commit is contained in:
Thastertyn committed 2025-03-16 22:24:54 +01:00
1 parent d87c280109
commit d0690f2f06
67 files changed
+3389 -1066

No files matched your search

@@ -1,4 +1,4 @@
import { HTMLAttributes, useState } from "react";
import { HTMLAttributes } from "react";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -15,6 +15,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/password-input";
import useAuth from "@/hooks/useAuth";
type UserAuthFormProps = HTMLAttributes<HTMLDivElement>;
@@ -34,7 +35,7 @@ const formSchema = z.object({
});
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const [isLoading, setIsLoading] = useState(false);
const { loginMutation, resetError } = useAuth();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -44,13 +45,13 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
}
});
function onSubmit(data: z.infer<typeof formSchema>) {
setIsLoading(true);
console.log(data);
setTimeout(() => {
setIsLoading(false);
}, 3000);
async function onSubmit(data: z.infer<typeof formSchema>) {
resetError();
try {
await loginMutation.mutateAsync({formData: {username: data.email, password: data.password}});
} catch {
// Error handled in useAuth hook
}
}
return (
@@ -91,8 +92,8 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
</FormItem>
)}
/>
<Button className="mt-2" disabled={isLoading}>
Login
<Button className="mt-2" disabled={loginMutation.isPending}>
{loginMutation.isPending ? "Logging in..." : "Login"}
</Button>
<div className="relative my-2">
@@ -16,6 +16,8 @@ import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/password-input";
import { registerUser } from "@/lib/api";
import { ApiError } from "@/errors/api-error";
import useAuth from "@/hooks/useAuth";
import { PhoneInput } from "@/components/ui/phone-number-input";
type SignUpFormProps = HTMLAttributes<HTMLDivElement>;
@@ -64,6 +66,7 @@ const formSchema = z
});
export function SignUpForm({ className, ...props }: SignUpFormProps) {
const { signUpMutation } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
@@ -85,12 +88,7 @@ export function SignUpForm({ className, ...props }: SignUpFormProps) {
setSuccessMessage(null);
try {
await registerUser({
username: data.username,
email: data.email,
password: data.password,
phone_number: data.phone_number
});
signUpMutation.mutate(data);
setSuccessMessage("Account created successfully!");
} catch (error: unknown) {
setError(error instanceof ApiError ? error.response : "Something went wrong.");
@@ -137,7 +135,7 @@ export function SignUpForm({ className, ...props }: SignUpFormProps) {
<FormItem className="space-y-1">
<FormLabel>Phone Number</FormLabel>
<FormControl>
<Input placeholder="+1234567890" {...field} />
<PhoneInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
-4
View File
@@ -1,4 +1,3 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -33,9 +32,6 @@ export default function Dashboard() {
<Main>
<div className="mb-2 flex items-center justify-between space-y-2">
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
<div className="flex items-center space-x-2">
<Button>Download</Button>
</div>
</div>
<Tabs
orientation="vertical"
-2
View File
@@ -18,8 +18,6 @@ export default function MainPage() {
<Pricing />
<Faq />
<CallToAction />
{/* [Optional] Consider inserting an image or illustration here to reinforce the platform’s purpose. */}
</div>
</>
);
-3
View File
@@ -1,3 +0,0 @@
export default function Nba() {
return <h1>NBA Testing</h1>;
}
@@ -27,7 +27,7 @@ const appText = new Map<string, string>([
["notConnected", "Not Connected"]
]);
export default function Apps() {
export default function Products() {
const [sort, setSort] = useState("ascending");
const [appType, setAppType] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
@@ -1,86 +1,63 @@
import { z } from "zod";
import { format } from "date-fns";
import { useForm } from "react-hook-form";
import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { zodResolver } from "@hookform/resolvers/zod";
import { cn } from "@/lib/utils";
import { toast } from "@/hooks/useToast";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@/components/ui/command";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import
{
Form,
FormControl, FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@/components/ui/popover";
import useAuth from "@/hooks/useAuth";
const languages = [
{ label: "English", value: "en" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Spanish", value: "es" },
{ label: "Portuguese", value: "pt" },
{ label: "Russian", value: "ru" },
{ label: "Japanese", value: "ja" },
{ label: "Korean", value: "ko" },
{ label: "Chinese", value: "zh" }
] as const;
// const languages = [
// { label: "English", value: "en" },
// { label: "French", value: "fr" },
// { label: "German", value: "de" },
// { label: "Spanish", value: "es" },
// { label: "Portuguese", value: "pt" },
// { label: "Russian", value: "ru" },
// { label: "Japanese", value: "ja" },
// { label: "Korean", value: "ko" },
// { label: "Chinese", value: "zh" }
// ] as const;
const accountFormSchema = z.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters."
})
.max(30, {
message: "Name must not be longer than 30 characters."
}),
dob: z.date({
required_error: "A date of birth is required."
}),
language: z.string({
required_error: "Please select a language."
})
username: z.string().min(3).max(30),
email: z.string().email(),
phone_number: z.string(),
// password: z.string().min(8).max(128),
first_name: z.string().optional(),
last_name: z.string().optional(),
// dob: z.date({
// required_error: "A date of birth is required."
// }),
// language: z.string({
// required_error: "Please select a language."
// })
});
type AccountFormValues = z.infer<typeof accountFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AccountFormValues> = {
name: ""
};
export function AccountForm() {
const { updateAccountMutation, user } = useAuth();
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
defaultValues
resolver: zodResolver(accountFormSchema)
});
function onSubmit(data: AccountFormValues) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
updateAccountMutation.mutate({
email: data.email,
// password: data.password,
password: "null",
phone_number: data.phone_number,
username: data.username,
first_name: data.first_name,
last_name: data.last_name
});
}
@@ -89,126 +66,58 @@ export function AccountForm() {
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
<Input placeholder="Your username" defaultValue={user?.username} {...field} />
</FormControl>
<FormDescription>
This is the name that will be displayed on your profile and in
emails.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dob"
name="email"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn(
"w-[240px] pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}>
{field.value ? (
format(field.value, "MMM d, yyyy")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date: Date) =>
date > new Date() || date < new Date("1900-01-01")
}
/>
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Your email" defaultValue={user?.email} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
name="phone_number"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Language</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[200px] justify-between",
!field.value && "text-muted-foreground"
)}>
{field.value
? languages.find(
(language) => language.value === field.value
)?.label
: "Select language"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search language..." />
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
<CommandList>
{languages.map((language) => (
<CommandItem
value={language.label}
key={language.value}
onSelect={() => {
form.setValue("language", language.value);
}}>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
language.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
{language.label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the language that will be used in the dashboard.
</FormDescription>
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormControl>
<Input placeholder="Your phone number" defaultValue={user?.phone_number} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update account</Button>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="New password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Account</Button>
</form>
</Form>
);
}
}
+8 -8
View File
@@ -1,9 +1,9 @@
import { Outlet } from "@tanstack/react-router";
import {
IconBrowserCheck,
IconLock,
IconNotification,
IconPalette,
IconTool,
IconUser
} from "@tabler/icons-react";
import { Separator } from "@/components/ui/separator";
@@ -53,26 +53,26 @@ const sidebarNavItems = [
{
title: "Profile",
icon: <IconUser size={18} />,
href: "/settings"
href: "/dashboard/settings"
},
{
title: "Account",
icon: <IconTool size={18} />,
href: "/settings/account"
title: "Security",
icon: <IconLock size={18} />,
href: "/dashboard/settings/security"
},
{
title: "Appearance",
icon: <IconPalette size={18} />,
href: "/settings/appearance"
href: "/dashboard/settings/appearance"
},
{
title: "Notifications",
icon: <IconNotification size={18} />,
href: "/settings/notifications"
href: "/dashboard/settings/notifications"
},
{
title: "Display",
icon: <IconBrowserCheck size={18} />,
href: "/settings/display"
href: "/dashboard/settings/display"
}
];
@@ -1,8 +1,7 @@
import { z } from "zod";
import { useFieldArray, useForm } from "react-hook-form";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { toast } from "@/hooks/useToast";
import { Button } from "@/components/ui/button";
import {
@@ -15,63 +14,71 @@ import {
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { PhoneInput } from "@/components/ui/phone-number-input";
import useAuth from "@/hooks/useAuth";
const profileFormSchema = z.object({
username: z
.string()
.min(2, {
message: "Username must be at least 2 characters."
.string({
required_error: "Please enter your desired username"
})
.max(30, {
message: "Username must not be longer than 30 characters."
.min(3, {
message: "Username must be at least 3 characters."
})
.max(64, {
message: "Username must not be longer than 64 characters."
}),
email: z
.string({
required_error: "Please select an email to display."
.string()
.email({
message: "Please enter your email address."
})
.email(),
bio: z.string().max(160).min(4),
urls: z
.array(
z.object({
value: z.string().url({ message: "Please enter a valid URL." })
})
)
phone_number: z
.string({
required_error: "Please input a valid phone number."
})
.min(2, {
message: "Phone number must be at least 2 characters."
})
.max(16, {
message: "Phone number must not be longer than 16 characters."
}),
first_name: z
.string()
.max(64, {
message: "First name must not be longer than 64 characters."
})
.optional(),
last_name: z
.string()
.max(64, {
message: "Last name must not be longer than 64 characters."
})
.optional()
});
type ProfileFormValues = z.infer<typeof profileFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<ProfileFormValues> = {
bio: "I own a computer.",
urls: [
{ value: "https://shadcn.com" },
{ value: "http://twitter.com/shadcn" }
]
};
export default function ProfileForm() {
const { user, updateAccountMutation } = useAuth();
const defaultValues: ProfileFormValues = {
username: user?.username ?? "",
email: user?.email ?? "",
phone_number: user?.phone_number ?? "",
first_name: user?.first_name ?? "",
last_name: user?.last_name ?? ""
};
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues,
mode: "onChange"
});
const { fields, append } = useFieldArray({
name: "urls",
control: form.control
});
function onSubmit(data: ProfileFormValues) {
updateAccountMutation.mutate(data);
toast({
title: "You submitted the following values:",
description: (
@@ -92,11 +99,11 @@ export default function ProfileForm() {
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
<Input placeholder="Username" {...field} />
</FormControl>
<FormDescription>
This is your public display name. It can be your real name or a
pseudonym. You can only change this once every 30 days.
pseudonym.
</FormDescription>
<FormMessage />
</FormItem>
@@ -108,18 +115,9 @@ export default function ProfileForm() {
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a verified email to display" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="m@example.com">m@example.com</SelectItem>
<SelectItem value="m@google.com">m@google.com</SelectItem>
<SelectItem value="m@support.com">m@support.com</SelectItem>
</SelectContent>
</Select>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormDescription>
You can manage verified email addresses in your{" "}
<Link to="/">email settings</Link>.
@@ -130,56 +128,44 @@ export default function ProfileForm() {
/>
<FormField
control={form.control}
name="bio"
name="first_name"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormLabel>First Name</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a little bit about yourself"
className="resize-none"
{...field}
/>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="last_name"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone_number"
render={({ field }) => (
<FormItem>
<FormLabel>Phone number</FormLabel>
<FormControl>
<PhoneInput {...field} />
</FormControl>
<FormDescription>
You can <span>@mention</span> other users and organizations to
link to them.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div>
{fields.map((field, index) => (
<FormField
control={form.control}
key={field.id}
name={`urls.${index}.value`}
render={({ field }) => (
<FormItem>
<FormLabel className={cn(index !== 0 && "sr-only")}>
URLs
</FormLabel>
<FormDescription className={cn(index !== 0 && "sr-only")}>
Add links to your website, blog, or social media profiles.
</FormDescription>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<Button
type="button"
variant="outline"
size="sm"
className="mt-2"
onClick={() => append({ value: "" })}>
Add URL
</Button>
</div>
<Button type="submit">Update profile</Button>
</form>
</Form>
Whitespace-only changes.
Whitespace-only changes.