diff --git a/.env.example b/.env.example index 4e08b10..73c2cfd 100644 --- a/.env.example +++ b/.env.example @@ -5,9 +5,16 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/sos-pet" NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="nextauth-secret" -# Next Auth Providers +# Next Auth Google Sign-In GOOGLE_CLIENT_ID="google-client-id" GOOGLE_CLIENT_SECRET="google-client-secret" +# Next Auth Email magic link Sign-In +# https://resend.com/changelog/smtp-service +EMAIL_HOST="smtp.resend.com" +EMAIL_PORT="465" +EMAIL_USER="resend" +EMAIL_PASSWORD="resend-api-key" +EMAIL_FROM="noreply@sos-pet.org" # Google Maps NEXT_PUBLIC_GOOGLE_MAPS_API_KEY="google-maps-api-key" diff --git a/package.json b/package.json index 3e07f45..5987d5c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "next": "^14.2.1", "next-auth": "^4.24.6", "next-themes": "^0.3.0", + "nodemailer": "^6.9.13", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.51.4", @@ -55,6 +56,7 @@ "devDependencies": { "@types/eslint": "^8.56.2", "@types/node": "^20.11.20", + "@types/nodemailer": "^6.4.15", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^7.1.1", diff --git a/src/app/signin/_components/EmailProviderForm.tsx b/src/app/signin/_components/EmailProviderForm.tsx new file mode 100644 index 0000000..83c3720 --- /dev/null +++ b/src/app/signin/_components/EmailProviderForm.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; +import { signIn } from "next-auth/react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Button } from "~/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "~/components/ui/form"; +import { Input } from "~/components/ui/input"; + +const formSchema = z.object({ + email: z.string().email("Por favor, insira um e-mail válido"), +}); + +export function EmailProviderForm() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { email: "" }, + }); + const [isLoading, setIsLoading] = useState(false); + + const onSubmit = async ({ email }: z.infer) => { + setIsLoading(true); + await signIn("email", { email }); + setIsLoading(false); + }; + + return ( +
+ +
+ + ou + +
+
+ ( + + + + + + + )} + /> + + + + + ); +} diff --git a/src/app/signin/_components/SignInProviderButton.tsx b/src/app/signin/_components/SignInProviderButton.tsx new file mode 100644 index 0000000..60670c8 --- /dev/null +++ b/src/app/signin/_components/SignInProviderButton.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { type ClientSafeProvider, signIn } from "next-auth/react"; +import Image from "next/image"; + +type SignInProviderButtonProps = { + provider: ClientSafeProvider; + callbackUrl: string; +}; + +export function SignInProviderButton({ + provider, + callbackUrl, +}: SignInProviderButtonProps) { + return ( + + ); +} diff --git a/src/app/signin/_components/SigninProviderButton.tsx b/src/app/signin/_components/SigninProviderButton.tsx deleted file mode 100644 index cea7aa3..0000000 --- a/src/app/signin/_components/SigninProviderButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { signIn } from "next-auth/react"; -import Image from "next/image"; - -type SigninProviderButtonProps = { - provider: { id: string; name: string }; - callbackUrl: string; -}; - -export function SigninProviderButton({ - provider, - callbackUrl, -}: SigninProviderButtonProps) { - return ( - - ); -} diff --git a/src/app/signin/layout.tsx b/src/app/signin/layout.tsx new file mode 100644 index 0000000..9f71fc4 --- /dev/null +++ b/src/app/signin/layout.tsx @@ -0,0 +1,9 @@ +import { type PropsWithChildren } from "react"; + +export default function SignInVerifyLayout({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index ded9df1..bdd3e45 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -1,47 +1,77 @@ -import { getProviders } from "next-auth/react"; +import { + type ClientSafeProvider, + type LiteralUnion, + getProviders, +} from "next-auth/react"; import { redirect } from "next/navigation"; import { getServerAuthSession } from "~/server/auth"; -import { SigninProviderButton } from "./_components/SigninProviderButton"; +import { SignInProviderButton } from "./_components/SignInProviderButton"; import Image from "next/image"; import { Suspense } from "react"; import { Loader2 } from "lucide-react"; +import { EmailProviderForm } from "./_components/EmailProviderForm"; +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { FiAlertTriangle } from "react-icons/fi"; -type SigninPageProps = { +type SignInPageProps = { searchParams: Record; }; -export default async function SigninPage({ searchParams }: SigninPageProps) { - const session = await getServerAuthSession(); +const splitProviders = ( + providers: Awaited>, +): [ClientSafeProvider | null, ClientSafeProvider[]] | [null, null] => { + if (!providers) return [null, null]; + const email = providers.email; + const other = Object.values(providers).filter( + (provider) => provider.id !== "email", + ); + return [email, other]; +}; + +export default async function SignInPage({ searchParams }: SignInPageProps) { + const [session, providers] = await Promise.all([ + getServerAuthSession(), + getProviders(), + ]); if (session) { redirect(searchParams.callbackUrl ?? "/"); } - const providers = [{ id: "google", name: "Google" }]; + const [emailProvider, otherProviders] = splitProviders(providers); + const hasError = !!searchParams.error; return ( -
- }> - Logo -

- Em decorrência das enchentes que afetaram o estado do Rio Grande do - Sul, estamos gerenciando as necessidades de abrigos de animais vítimas - do desastre. -

-

- Antes de cadastrar um abrigo você precisa fazer login{" "} - com uma das opções abaixo: -

-
- {Object.values(providers).map((provider) => ( - - ))} -
-
-
+ }> + Logo +

+ Em decorrência das enchentes que afetaram o estado do Rio Grande do Sul, + estamos gerenciando as necessidades de abrigos de animais vítimas do + desastre. +

+

+ Antes de cadastrar um abrigo você precisa fazer login com + uma das opções abaixo: +

+ {hasError && ( + + + Erro ao tentar realizar o login. + + Por favor, tente novamente com outra opção disponível. + + + )} +
+ {otherProviders?.map((provider) => ( + + ))} + {!!emailProvider && } +
+
); } diff --git a/src/app/signin/verify/page.tsx b/src/app/signin/verify/page.tsx new file mode 100644 index 0000000..ccf79ce --- /dev/null +++ b/src/app/signin/verify/page.tsx @@ -0,0 +1,19 @@ +import { FaRegCheckCircle } from "react-icons/fa"; + +export default function SignInVerifyPage() { + return ( + <> +
+ +

+ E-mail enviado com sucesso +

+

+ Enviamos um e-mail com um link para você verificar sua conta. Por + favor, verifique sua caixa de entrada para continuar com o + processo de login. +

+
+ + ); +} diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index e270b43..432dba3 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -2,32 +2,41 @@ import Image from "next/image"; import { Nav } from "./nav"; import { Sidebar } from "./sidebar"; import Link from "next/link"; +import { Button } from "../ui/button"; export function Header() { + const renderMainSection = () => ( +
+
+ + Logo + +
+
    +
  • + +
  • +
  • + +
  • +
+
+ ); + return (
-
+
-
-
- - Logo - -
-
    -
  • - Home -
  • -
  • - Sobre -
  • -
-
+ {renderMainSection()}
diff --git a/src/components/header/nav/index.tsx b/src/components/header/nav/index.tsx index 6ba732c..aaa56f6 100644 --- a/src/components/header/nav/index.tsx +++ b/src/components/header/nav/index.tsx @@ -14,39 +14,36 @@ import { import Link from "next/link"; import { User } from "../user"; -import { CiCircleChevDown } from "react-icons/ci"; - export function Nav() { const { data: session } = useSession(); - const router = useRouter(); return (