diff --git a/app/[locale]/Providers.tsx b/app/[locale]/Providers.tsx index f2b5e72..5ef2b98 100644 --- a/app/[locale]/Providers.tsx +++ b/app/[locale]/Providers.tsx @@ -14,7 +14,11 @@ interface Props { function Providers({ locale, messages, children }: Props) { return ( - + {children} diff --git a/app/[locale]/[id]/page.tsx b/app/[locale]/[id]/page.tsx index 68da69e..34f5c43 100644 --- a/app/[locale]/[id]/page.tsx +++ b/app/[locale]/[id]/page.tsx @@ -1,9 +1,10 @@ +import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import HighlineImage from "@/components/HighlineImage"; import RegistryEntry from "@/components/RegistryEntry"; import Tabs from "@/components/tabs/Tabs"; -import supabase from "@/utils/supabase"; +import { useSupabaseServer } from "@/utils/supabase/server"; import GoBack from "./_components/GoBack"; @@ -14,6 +15,9 @@ export default async function Highline({ }: { params: { id: string }; }) { + const cookieStore = cookies(); + const supabase = useSupabaseServer(cookieStore); + const { data: highline } = await supabase .from("highline") .select() diff --git a/app/[locale]/_components/UsernameDialog.tsx b/app/[locale]/_components/UsernameDialog.tsx index cf6476f..ec0867a 100644 --- a/app/[locale]/_components/UsernameDialog.tsx +++ b/app/[locale]/_components/UsernameDialog.tsx @@ -1,10 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { - createClientComponentClient, - User, -} from "@supabase/auth-helpers-nextjs"; +import type { User } from "@supabase/supabase-js"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -28,8 +25,8 @@ import { FormMessage, } from "@/components/ui/Form"; import { Input } from "@/components/ui/Input"; -import { Database } from "@/utils/database.types"; -import { useRouter } from "next/navigation"; +import { useRouter } from "@/navigation"; +import useSupabaseBrowser from "@/utils/supabase/client"; const formSchema = z.object({ username: z @@ -44,7 +41,7 @@ const formSchema = z.object({ type FormSchema = z.infer; export default function UsernameDialog() { - const supabase = createClientComponentClient(); + const supabase = useSupabaseBrowser(); const t = useTranslations("usernameDialog"); const router = useRouter(); diff --git a/app/[locale]/auth/callback/route.ts b/app/[locale]/auth/callback/route.ts index 4695cf4..16fd5b2 100644 --- a/app/[locale]/auth/callback/route.ts +++ b/app/[locale]/auth/callback/route.ts @@ -1,11 +1,10 @@ // Refer to the following documentation for more context // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange -import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import { Database } from "@/utils/database.types"; +import { useSupabaseServer } from "@/utils/supabase/server"; export const dynamic = "force-dynamic"; @@ -14,14 +13,15 @@ export async function GET(request: Request) { // by the Auth Helpers package. It exchanges an auth code for the user's session. const requestUrl = new URL(request.url); const code = requestUrl.searchParams.get("code"); + const redirectTo = requestUrl.searchParams.get("redirect_to"); if (code) { const cookieStore = cookies(); - const supabase = createRouteHandlerClient({ - cookies: () => cookieStore, - }); + // eslint-disable-next-line react-hooks/rules-of-hooks + const supabase = useSupabaseServer(cookieStore); + await supabase.auth.exchangeCodeForSession(code); } - return NextResponse.redirect(requestUrl.origin); + return NextResponse.redirect(redirectTo || requestUrl.origin); } diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 960226f..a105a48 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -42,7 +42,7 @@ export default function RootLayout({ className={`min-h-screen bg-gray-50 dark:bg-gray-900 md:px-0 ${inter.variable} font-sans`} > -
+
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 63a099d..0c0bbb1 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,9 +1,7 @@ -import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; import { cookies } from "next/headers"; -import { unstable_setRequestLocale } from "next-intl/server"; import Highline from "@/components/Highline"; -import { Database } from "@/utils/database.types"; +import { useSupabaseServer } from "@/utils/supabase/server"; import Search from "./_components/search"; @@ -17,11 +15,11 @@ export default async function Home({ searchParams?: { [key: string]: string | string[] | undefined }; }) { const cookieStore = cookies(); - const supabase = createServerComponentClient({ - cookies: () => cookieStore, - }); + const supabase = useSupabaseServer(cookieStore); - const { q: searchValue } = searchParams as { [key: string]: string }; + const { q: searchValue } = searchParams as { + [key: string]: string; + }; const highlines = searchValue ? ( await supabase diff --git a/app/[locale]/profile/[username]/_components/LastWalks.tsx b/app/[locale]/profile/[username]/_components/LastWalks.tsx index bd9637a..3d4abd7 100644 --- a/app/[locale]/profile/[username]/_components/LastWalks.tsx +++ b/app/[locale]/profile/[username]/_components/LastWalks.tsx @@ -1,5 +1,4 @@ import { ChevronRightIcon } from "@radix-ui/react-icons"; -import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; import { cookies } from "next/headers"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -10,8 +9,9 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/Popover"; -import { Database } from "@/utils/database.types"; import { transformSecondsToTimeString } from "@/utils/helperFunctions"; +import type { Database } from "@/utils/supabase/database.types"; +import { useSupabaseServer } from "@/utils/supabase/server"; import FormattedDate from "./FormattedDate"; @@ -23,9 +23,7 @@ interface Props { export default async function LastWalks({ username }: Props) { const cookieStore = cookies(); - const supabase = createServerComponentClient({ - cookies: () => cookieStore, - }); + const supabase = useSupabaseServer(cookieStore); const { data: entries } = await supabase .from("entry") @@ -57,7 +55,10 @@ function LastWalksContent({ entries }: ContentProps) {
    {entries.length > 0 ? ( entries.map((entry) => ( -
  • +
  • {entry.highline?.name} diff --git a/app/[locale]/profile/[username]/_components/UserHeader.tsx b/app/[locale]/profile/[username]/_components/UserHeader.tsx index 63c5f5b..9c04773 100644 --- a/app/[locale]/profile/[username]/_components/UserHeader.tsx +++ b/app/[locale]/profile/[username]/_components/UserHeader.tsx @@ -1,14 +1,17 @@ +import { User } from "@supabase/supabase-js"; import Image from "next/image"; import { useTranslations } from "next-intl"; -import { Database } from "@/utils/database.types"; +import UpdateProfile from "@/components/layout/navbar/UpdateProfile"; +import { Database } from "@/utils/supabase/database.types"; interface Props { + user: User | null; profile: Database["public"]["Tables"]["profiles"]["Row"] | null; username: string; } -function UserHeader({ profile, username }: Props) { +function UserHeader({ user, profile, username }: Props) { const t = useTranslations("profile.header"); if (!profile) { @@ -31,8 +34,8 @@ function UserHeader({ profile, username }: Props) { } return ( -
    -
    +
    +
    -
    -

    @{username}

    +
    +

    @{username}

    + {user && user.id === profile.id ? ( + + ) : null}
    diff --git a/app/[locale]/profile/[username]/page.tsx b/app/[locale]/profile/[username]/page.tsx index fabf363..58f2e7a 100644 --- a/app/[locale]/profile/[username]/page.tsx +++ b/app/[locale]/profile/[username]/page.tsx @@ -1,9 +1,9 @@ -import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; -import { Metadata } from "next"; +import type { Metadata, ResolvingMetadata } from "next"; import { cookies } from "next/headers"; +import { getTranslations } from "next-intl/server"; import { Suspense } from "react"; -import { Database } from "@/utils/database.types"; +import { useSupabaseServer } from "@/utils/supabase/server"; import GoBack from "./_components/GoBack"; import LastWalks, { LastWalksSkeleton } from "./_components/LastWalks"; @@ -13,20 +13,29 @@ import UserNotFound from "./_components/UserNotFound"; export const dynamic = "force-dynamic"; -export const metadata: Metadata = { - title: "Profile", - description: "User profile", +type Props = { + params: { locale: string; username: string }; + searchParams: { [key: string]: string | string[] | undefined }; }; -export default async function Profile({ - params: { username }, -}: { - params: { username: string }; -}) { +export async function generateMetadata( + { params, searchParams }: Props, + parent: ResolvingMetadata +): Promise { + const t = await getTranslations("profileMetadata"); + return { + title: t("title", { username: `@${params.username}` }), + description: t("description"), + }; +} + +export default async function Profile({ params: { username } }: Props) { const cookieStore = cookies(); - const supabase = createServerComponentClient({ - cookies: () => cookieStore, - }); + const supabase = useSupabaseServer(cookieStore); + + const { + data: { user }, + } = await supabase.auth.getUser(); const { data: profile } = await supabase .from("profiles") @@ -52,9 +61,9 @@ export default async function Profile({ } return ( -
    +
    - + ; const CreateHighline = () => { + const supabase = useSupabaseBrowser(); + const t = useTranslations("home.newHighline"); const [dialogOpen, setDialogOpen] = useState(false); const [newHighlineUUID, setNewHighlineUUID] = useState(null); diff --git a/components/Highline.tsx b/components/Highline.tsx index 39b5ebf..cc92d84 100644 --- a/components/Highline.tsx +++ b/components/Highline.tsx @@ -2,7 +2,7 @@ import { useTranslations } from "next-intl"; import { ArrowIcon } from "@/assets"; import { Link } from "@/navigation"; -import type { Tables } from "@/utils/supabase"; +import type { Tables } from "@/utils/supabase/database.types"; import HighlineImage from "./HighlineImage"; diff --git a/components/RegistryEntry.tsx b/components/RegistryEntry.tsx index f5539e1..c19b687 100644 --- a/components/RegistryEntry.tsx +++ b/components/RegistryEntry.tsx @@ -9,7 +9,7 @@ import { z } from "zod"; import { PlusSvg } from "@/assets"; import { transformTimeStringToSeconds } from "@/utils/helperFunctions"; -import supabase from "@/utils/supabase"; +import useSupabaseBrowser from "@/utils/supabase/client"; import { SuccessAnimation } from "./animations/SuccessAnimation"; import Button from "./ui/Button"; @@ -73,8 +73,9 @@ interface Props { } const CreateHighline = ({ highlineId, highlineDistance }: Props) => { - const [dialogOpen, setDialogOpen] = useState(false); + const supabase = useSupabaseBrowser(); + const [dialogOpen, setDialogOpen] = useState(false); const t = useTranslations("highline.registry"); const queryClient = useQueryClient(); diff --git a/components/layout/navbar/ProfileMenu.tsx b/components/layout/navbar/ProfileMenu.tsx index 513ec21..756e2a8 100644 --- a/components/layout/navbar/ProfileMenu.tsx +++ b/components/layout/navbar/ProfileMenu.tsx @@ -1,8 +1,9 @@ -import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; -import { cookies } from "next/headers"; +"use client"; + +import { User } from "@supabase/supabase-js"; import { useTranslations } from "next-intl"; -import { UserCircleIcon } from "@/assets"; +import { LogOutIcon, UserCircleIcon } from "@/assets"; import { DropdownMenu, DropdownMenuContent, @@ -10,21 +11,20 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/DropdownMenu"; -import { Link } from "@/navigation"; - -import SignOut from "./SignOut"; +import { Link, useRouter } from "@/navigation"; +import useSupabaseBrowser from "@/utils/supabase/client"; -export const dynamic = "force-dynamic"; +export default async function ProfileMenu({ user }: { user: User }) { + const supabase = useSupabaseBrowser(); + const t = useTranslations("profileMenu"); + const router = useRouter(); -export default async function ProfileMenu() { - const cookieStore = cookies(); - const supabase = createServerComponentClient({ - cookies: () => cookieStore, - }); + const username: string | null = user.user_metadata["username"]; - const { - data: { user }, - } = await supabase.auth.getUser(); + const signOut = async () => { + await supabase.auth.signOut(); + router.refresh(); + }; return ( @@ -39,24 +39,19 @@ export default async function ProfileMenu() { sideOffset={8} className="max-w-[12rem] overflow-hidden" > - {/* TODO: User can't see correct ProfileSection after set up username since this component would not know */} - {user?.user_metadata["username"] ? ( - + {username ? ( + + + {t("myProfile")} + + ) : null} - + + {t("signOut")} + + ); } - -function ProfileSection({ username }: { username: string }) { - const t = useTranslations("profileMenu"); - return ( - - {t("myProfile")} - - ); -} diff --git a/components/layout/navbar/SignOut.tsx b/components/layout/navbar/SignOut.tsx deleted file mode 100644 index 71716fa..0000000 --- a/components/layout/navbar/SignOut.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; - -import { LogOutIcon } from "@/assets"; -import { DropdownMenuItem } from "@/components/ui/DropdownMenu"; -import { useRouter } from "@/navigation"; -import supabase from "@/utils/supabase"; - -function SignOut() { - const t = useTranslations("profileMenu"); - const router = useRouter(); - - const signOut = async () => { - await supabase.auth.signOut(); - router.refresh(); - }; - - return ( - - {t("signOut")} - - - ); -} - -export default SignOut; diff --git a/components/layout/navbar/SignUp.tsx b/components/layout/navbar/SignUp.tsx index 1ef2388..b32c6f8 100644 --- a/components/layout/navbar/SignUp.tsx +++ b/components/layout/navbar/SignUp.tsx @@ -25,7 +25,7 @@ import { FormMessage, } from "@/components/ui/Form"; import { Input } from "@/components/ui/Input"; -import supabase from "@/utils/supabase"; +import useSupabaseBrowser from "@/utils/supabase/client"; const formSchema = z.object({ email: z.string().email(), @@ -34,6 +34,7 @@ const formSchema = z.object({ type FormSchema = z.infer; function SignUp() { + const supabase = useSupabaseBrowser(); const t = useTranslations(); const [step, setStep] = useState<"initial" | "email" | "inbox">("initial"); const form = useForm({ @@ -47,7 +48,7 @@ function SignUp() { await supabase.auth.signInWithOAuth({ provider: "google", options: { - redirectTo: `${window.location.origin}/auth/callback`, + redirectTo: `${location.origin}/auth/callback?redirect_to=${location.href}`, }, }); } @@ -57,7 +58,7 @@ function SignUp() { email: data.email, options: { shouldCreateUser: true, - emailRedirectTo: `${location.origin}/auth/callback`, + emailRedirectTo: `${location.origin}/auth/callback?redirect_to=${location.href}`, }, }); diff --git a/components/layout/navbar/UpdateProfile.tsx b/components/layout/navbar/UpdateProfile.tsx new file mode 100644 index 0000000..f9610d5 --- /dev/null +++ b/components/layout/navbar/UpdateProfile.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import type { User } from "@supabase/supabase-js"; +import { useMutation } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import Button from "@/components/ui/Button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/Dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/Form"; +import { Input } from "@/components/ui/Input"; +import { TextArea } from "@/components/ui/TextArea"; +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(), +}); + +type FormSchema = z.infer; + +export default function UpdateProfile({ + user, + profile, +}: { + user: User | null; + profile: Database["public"]["Tables"]["profiles"]["Row"]; +}) { + const [open, setOpen] = useState(false); + + const t = useTranslations("updateProfile"); + const router = useRouter(); + const supabase = useSupabaseBrowser(); + const profileForm = useForm({ + mode: "onTouched", + resolver: zodResolver(formSchema), + defaultValues: { + name: profile.name || "", + description: profile.description || "", + }, + }); + + async function updateProfile(formData: FormSchema) { + const { error } = await supabase + .from("profiles") + .update({ + description: formData.description, + name: formData.name, + }) + .eq("id", profile.id); + + await supabase.auth.updateUser({ + data: { + displayName: formData.name, + }, + }); + if (error) throw error; + } + + const profileMutation = useMutation(updateProfile, { + onSuccess: () => { + router.refresh(); + setOpen(false); + }, + onError: (e) => { + profileForm.setError("root", { + message: "Error on upadting the profile, try again later!", + }); + }, + }); + + const onSubmit = (data: FormSchema) => { + profileMutation.mutate(data); + }; + + const onError = (e: unknown) => { + console.log("Invalid form"); + }; + + return ( + { + if (o) { + profileForm.reset(); + } + setOpen(o); + }} + > + +