Skip to content

Commit

Permalink
User now can add set his birthday
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosbodoke committed Apr 18, 2024
1 parent 4473a7c commit 3860e3e
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 40 deletions.
4 changes: 2 additions & 2 deletions app/[locale]/profile/[username]/_components/UserHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ function UserHeader({ profile, username }: Props) {
<p className="text-muted-foreground">@{username}</p>
<ul className="space-y-2">
{profile.birthday ? (
<li className="flex items-center gap-2 text-muted-foreground">
<li className="flex items-center gap-1 text-muted-foreground">
<CalendarFoldIcon className="h-4 w-4 text-muted-foreground" />
<span>age:</span> {calculateAge(profile.birthday)}
<span>{t("age")}:</span> {calculateAge(profile.birthday)}
</li>
) : null}
{/* <li className="flex gap-2 text-muted-foreground">
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/profile/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default async function Profile({

return (
<div className="mx-2 max-w-screen-md space-y-4 pt-0 md:mx-auto md:space-y-6 md:pt-8">
<div className="flex flex-row items-end justify-between">
<div className="flex justify-end">
{profile && profile.id === user?.id ? (
<UpdateProfile profile={profile} />
) : null}
Expand Down
133 changes: 98 additions & 35 deletions components/layout/navbar/UpdateProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { format } from "date-fns";
import { enUS, ptBR } from "date-fns/locale";
import { CalendarIcon } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button, ButtonLoading } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Drawer,
DrawerClose,
Expand All @@ -22,16 +27,28 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/Input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { TextArea } from "@/components/ui/TextArea";
import { cn } from "@/lib/utils";
import { useRouter } from "@/navigation";
import useSupabaseBrowser from "@/utils/supabase/client";
import { Database } from "@/utils/supabase/database.types";

const formSchema = z.object({
name: z.string().min(3, "Deve conter ao menos 3 caracteres"),
description: z.string().optional(),
birthday: z
.date({
required_error: "A date of birth is required.",
})
.nullable(),
});

type FormSchema = z.infer<typeof formSchema>;
Expand All @@ -41,42 +58,46 @@ export default function UpdateProfile({
}: {
profile: Database["public"]["Tables"]["profiles"]["Row"];
}) {
const dateLocale = useLocale() === "pt" ? ptBR : enUS;
const t = useTranslations("updateProfile");
const [open, setOpen] = useState(false);
const router = useRouter();
const supabase = useSupabaseBrowser();
const profileForm = useForm<FormSchema>({
const form = useForm<FormSchema>({
mode: "onTouched",
resolver: zodResolver(formSchema),
defaultValues: {
name: profile.name || "",
description: profile.description || "",
birthday: profile.birthday ? new Date(profile.birthday) : null,
},
});

async function updateProfile(formData: FormSchema) {
const { error } = await supabase
.from("profiles")
.update({
description: formData.description,
name: formData.name,
})
.eq("id", profile.id);
const profileMutation = useMutation<void, Error, FormSchema>({
mutationFn: async (formData) => {
const { error } = await supabase
.from("profiles")
.update({
description: formData.description,
name: formData.name,
birthday: formData.birthday?.toDateString(),
})
.eq("id", profile.id);

await supabase.auth.updateUser({
data: {
displayName: formData.name,
},
});
if (error) throw error;
}
if (error) throw new Error(error.message);

const profileMutation = useMutation({
mutationFn: updateProfile,
onSuccess(data, variables, context) {
await supabase.auth.updateUser({
data: {
displayName: formData.name,
},
});
},
onSuccess() {
setOpen(false);
router.refresh();
},
onError: (e) => {
profileForm.setError("root", {
onError: () => {
form.setError("root", {
message: "Error on upadting the profile, try again later!",
});
},
Expand All @@ -91,22 +112,22 @@ export default function UpdateProfile({
};

return (
<Drawer>
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button variant="outline">{t("trigger")}</Button>
</DrawerTrigger>
<DrawerContent>
<div className="scrollbar mx-auto flex w-full max-w-md flex-col overflow-auto rounded-t-[10px] p-4">
<DrawerHeader>
<div className="scrollbar mx-auto flex w-full max-w-md flex-col space-y-4 overflow-auto rounded-t-[10px] p-4">
<DrawerHeader className="p-0">
<DrawerTitle>{t("title")}</DrawerTitle>
</DrawerHeader>
<Form {...profileForm}>
<Form {...form}>
<form
onSubmit={profileForm.handleSubmit(onSubmit, onError)}
onSubmit={form.handleSubmit(onSubmit, onError)}
className="space-y-6"
>
<FormField
control={profileForm.control}
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
Expand All @@ -121,7 +142,7 @@ export default function UpdateProfile({
)}
/>
<FormField
control={profileForm.control}
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
Expand All @@ -137,16 +158,58 @@ export default function UpdateProfile({
</FormItem>
)}
/>
<FormField
control={form.control}
name="birthday"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>{t("fields.birthday.label")}</FormLabel>
<Popover modal={true}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn(
"font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? (
format(field.value, "PPP", {
locale: dateLocale,
})
) : (
<span>{t("fields.birthday.placeholder")}</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="single"
captionLayout="dropdown-buttons"
locale={dateLocale}
selected={field.value || undefined}
fromYear={1900}
toYear={new Date().getFullYear()}
onSelect={field.onChange}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<DrawerFooter className="p-0">
{profileMutation.isPending ? (
<ButtonLoading />
) : (
<>
<Button type="submit">{t("fields.submit")}</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</>
<Button type="submit">{t("fields.submit")}</Button>
)}
</DrawerFooter>
</form>
Expand Down
130 changes: 130 additions & 0 deletions components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"use client";

import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { DayPicker } from "react-day-picker";

import { buttonVariants } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps & { onChange?: React.ChangeEventHandler<HTMLSelectElement> }) {
const handleCalendarChange = (
_value: string | number,
_e: React.ChangeEventHandler<HTMLSelectElement>
) => {
const _event = {
target: {
value: String(_value),
},
} as React.ChangeEvent<HTMLSelectElement>;
_e(_event);
};

return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption_start: "is-start",
caption_between: "is-between",
caption_end: "is-end",
caption: "flex justify-center pt-1 relative items-center gap-1",
caption_label:
"flex h-7 text-sm font-medium justify-center items-center grow [.is-multiple_&]:flex",
caption_dropdowns: "flex justify-center gap-1 grow dropdowns pl-8 pr-9",
multiple_months: "is-multiple",
vhidden:
"hidden [.is-between_&]:flex [.is-end_&]:flex [.is-start.is-end_&]:hidden",
nav: "flex items-center [&:has([name='previous-month'])]:order-first [&:has([name='next-month'])]:order-last gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 text-muted-foreground"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
Dropdown: ({ ...props }) => (
<Select
{...props}
onValueChange={(value) => {
if (props.onChange) {
handleCalendarChange(value, props.onChange);
}
}}
value={props.value as string}
>
<SelectTrigger
className={cn(
buttonVariants({ variant: "ghost" }),
"h-7 w-fit py-2 pl-2 pr-1 font-medium [.is-between_&]:hidden [.is-end_&]:hidden [.is-start.is-end_&]:flex"
)}
>
<SelectValue placeholder={props?.caption}>
{props?.caption}
</SelectValue>
</SelectTrigger>
<SelectContent className="scrolling-auto max-h-[var(--radix-popper-available-height);] min-w-[var(--radix-popper-anchor-width)] overflow-y-auto">
{props.children &&
React.Children.map(props.children, (child) => (
<SelectItem
value={(child as React.ReactElement<any>)?.props?.value}
className="min-w-[var(--radix-popper-anchor-width)]"
>
{(child as React.ReactElement<any>)?.props?.children}
</SelectItem>
))}
</SelectContent>
</Select>
),
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";

export { Calendar };
7 changes: 6 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
},
"profile": {
"header": {
"notVerified": "This user is not verified"
"notVerified": "This user is not verified",
"age": "Age"
},
"notFound": {
"title": "User don't exists",
Expand Down Expand Up @@ -228,6 +229,10 @@
"label": "Bio",
"placeholder": "Tell us a bit about yourself"
},
"birthday": {
"label": "Birthday",
"placeholder": "Select a date"
},
"submit": "Save"
}
},
Expand Down
Loading

0 comments on commit 3860e3e

Please sign in to comment.