diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..25458c98 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI/CD Pipeline + +on: + pull_request: + branches: + - main + +jobs: + lint-and-build: + name: Lint and Build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js (Use Required Version) + uses: actions/setup-node@v4 + with: + node-version: 20.18.0 # Set to the required version + + - name: Install dependencies + run: npm install + + - name: Set up environment variables + run: | + echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env + echo "NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}" >> .env + echo "NEXT_PUBLIC_APP_URL=${{ secrets.NEXT_PUBLIC_APP_URL }}" >> .env + echo "RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}" >> .env + echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env + echo "DATABASE_URL_UNPOOLED=${{ secrets.DATABASE_URL_UNPOOLED }}" >> .env + echo "PGHOST=${{ secrets.PGHOST }}" >> .env + echo "PGHOST_UNPOOLED=${{ secrets.PGHOST_UNPOOLED }}" >> .env + echo "PGUSER=${{ secrets.PGUSER }}" >> .env + echo "PGDATABASE=${{ secrets.PGDATABASE }}" >> .env + echo "PGPASSWORD=${{ secrets.PGPASSWORD }}" >> .env + echo "POSTGRES_URL=${{ secrets.POSTGRES_URL }}" >> .env + echo "POSTGRES_URL_NON_POOLING=${{ secrets.POSTGRES_URL_NON_POOLING }}" >> .env + echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env + echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env + echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env + echo "POSTGRES_DATABASE=${{ secrets.POSTGRES_DATABASE }}" >> .env + echo "POSTGRES_URL_NO_SSL=${{ secrets.POSTGRES_URL_NO_SSL }}" >> .env + echo "POSTGRES_PRISMA_URL=${{ secrets.POSTGRES_PRISMA_URL }}" >> .env + echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env + echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env + echo "GITHUB_ID=${{ secrets.GH_ID }}" >> .env + echo "GITHUB_SECRET=${{ secrets.GH_SECRET }}" >> .env + + - name: Debug File Listing + run: | + echo "Checking for matching files..." + find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" -o -name "*.css" -o -name "*.scss" \) \ + ! -path "./node_modules/*" ! -path "./dist/*" ! -path "./.git/*" ! -path "./coverage/*" \ + || echo "No matching files found." + + - name: Run Biome Formatter & Linter + run: | + FILES=$(find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" -o -name "*.css" -o -name "*.scss" \) \ + ! -path "./node_modules/*" ! -path "./dist/*" ! -path "./.git/*" ! -path "./coverage/*") + + if [ -n "$FILES" ]; then + echo "Running Biome on found files..." + echo "$FILES" | xargs npx biome format --write + echo "$FILES" | xargs npx biome lint --write + else + echo "No files found to format or lint. Skipping." + fi + + - name: Run Build + run: npm run build diff --git a/app/admin/page.tsx b/app/admin/page.tsx index c903e012..f2fb41de 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,33 +1,34 @@ -import { authOptions } from "@/lib/auth.config" -import { getServerSession } from "next-auth/next" -import { redirect } from "next/navigation" +import { authOptions } from "@/lib/auth.config"; +import { getServerSession } from "next-auth/next"; +import { redirect } from "next/navigation"; export default async function AdminDashboard() { - const session = await getServerSession(authOptions) + const session = await getServerSession(authOptions); - if (!session || session.user.role !== "ADMIN") { - redirect("/unauthorized") - } + if (!session || session.user.role !== "ADMIN") { + redirect("/unauthorized"); + } - return ( -
-
-
-
-
-
-

Welcome to the Admin Dashboard!

-
-
-
-

You are signed in as an admin: {session.user.email}

-

Your role is: {session.user.role}

-
-
-
-
-
-
- ) + return ( +
+
+
+
+
+
+

+ Welcome to the Admin Dashboard! +

+
+
+
+

You are signed in as an admin: {session.user.email}

+

Your role is: {session.user.role}

+
+
+
+
+
+
+ ); } - diff --git a/app/auth/forgot-password/page.tsx b/app/auth/forgot-password/page.tsx index cf6649e3..c9b0c923 100644 --- a/app/auth/forgot-password/page.tsx +++ b/app/auth/forgot-password/page.tsx @@ -1,89 +1,109 @@ -"use client" +"use client"; -import { useState } from "react" -import { motion } from "framer-motion" -import Image from "next/image" -import { useRouter } from "next/navigation" +import { useState } from "react"; +import { motion } from "framer-motion"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; export default function ForgotPassword() { - const [email, setEmail] = useState("") - const [error, setError] = useState("") - const [success, setSuccess] = useState("") - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() + const [email, setEmail] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError("") - setSuccess("") - setIsLoading(true) + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setSuccess(""); + setIsLoading(true); - try { - const response = await fetch("/api/auth/forgot-password", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - }) + try { + const response = await fetch("/api/auth/forgot-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); - if (response.ok) { - setSuccess("Password reset link sent to your email.") - } else { - const data = await response.json() - setError(data.message || "Failed to send reset link.") - } - } catch (error) { - setError(`An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`) - } finally { - setIsLoading(false) - } - } + if (response.ok) { + setSuccess("Password reset link sent to your email."); + } else { + const data = await response.json(); + setError(data.message || "Failed to send reset link."); + } + } catch (error) { + setError( + `An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`, + ); + } finally { + setIsLoading(false); + } + }; - return ( -
-
-
- Boundless -

Forgot Password

-

Enter your email to receive a password reset link.

-
+ return ( +
+
+
+ Boundless +

+ Forgot Password +

+

+ Enter your email to receive a password reset link. +

+
-
-
- - setEmail(e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#194247] focus:border-[#194247]" - /> -
+ +
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#194247] focus:border-[#194247]" + /> +
- {error &&

{error}

} - {success &&

{success}

} + {error &&

{error}

} + {success && ( +

{success}

+ )} - - {isLoading ? "Sending..." : "Send Reset Link"} - -
+ + {isLoading ? "Sending..." : "Send Reset Link"} + + -
- -
-
-
- ) +
+ +
+
+
+ ); } diff --git a/app/auth/reset-password/page.tsx b/app/auth/reset-password/page.tsx index fddc08a5..a252dd3b 100644 --- a/app/auth/reset-password/page.tsx +++ b/app/auth/reset-password/page.tsx @@ -1,111 +1,135 @@ -"use client" +"use client"; -import { useEffect, useState } from "react" -import { useRouter, useSearchParams } from "next/navigation" -import { motion } from "framer-motion" +import { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { motion } from "framer-motion"; export default function ResetPassword() { - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [confirmPassword, setConfirmPassword] = useState("") - const [error, setError] = useState("") - const [success, setSuccess] = useState("") - const [isLoading, setIsLoading] = useState(false) + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isLoading, setIsLoading] = useState(false); - const router = useRouter() - const searchParams = useSearchParams() - const otp = searchParams.get("token") + const router = useRouter(); + const searchParams = useSearchParams(); + const otp = searchParams.get("token"); - useEffect(() => { - if (!otp) { - router.push("/auth/forgot-password") - } - }, [otp, router]) + useEffect(() => { + if (!otp) { + router.push("/auth/forgot-password"); + } + }, [otp, router]); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError("") - setSuccess("") - setIsLoading(true) + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setSuccess(""); + setIsLoading(true); - if (password !== confirmPassword) { - setError("Passwords do not match") - setIsLoading(false) - return - } + if (password !== confirmPassword) { + setError("Passwords do not match"); + setIsLoading(false); + return; + } - try { - const response = await fetch("/api/auth/reset-password", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password, otp }), - }) + try { + const response = await fetch("/api/auth/reset-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, otp }), + }); - if (response.ok) { - setSuccess("Password reset successfully! Redirecting...") - setTimeout(() => router.push("/auth/signin"), 3000) - } else { - const data = await response.json() - setError(data.message || "Failed to reset password") - } - } catch (error) { - setError(`An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`) - } finally { - setIsLoading(false) - } - } + if (response.ok) { + setSuccess("Password reset successfully! Redirecting..."); + setTimeout(() => router.push("/auth/signin"), 3000); + } else { + const data = await response.json(); + setError(data.message || "Failed to reset password"); + } + } catch (error) { + setError( + `An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`, + ); + } finally { + setIsLoading(false); + } + }; - return ( -
-
-

Reset Password

-
-
- - setEmail(e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#194247] focus:border-[#194247]" - /> -
+ return ( +
+
+

+ Reset Password +

+ +
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#194247] focus:border-[#194247]" + /> +
-
- - setPassword(e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#194247] focus:border-[#194247]" - /> -
+
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#194247] focus:border-[#194247]" + /> +
-
- - setConfirmPassword(e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#194247] focus:border-[#194247]" - /> -
+
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#194247] focus:border-[#194247]" + /> +
- {error &&

{error}

} - {success &&

{success}

} + {error &&

{error}

} + {success && ( +

{success}

+ )} - - {isLoading ? "Resetting..." : "Reset Password"} - - -
-
- ) + + {isLoading ? "Resetting..." : "Reset Password"} + + +
+
+ ); } diff --git a/app/auth/verify-otp/page.tsx b/app/auth/verify-otp/page.tsx index 2d46a6ed..16bd97d1 100644 --- a/app/auth/verify-otp/page.tsx +++ b/app/auth/verify-otp/page.tsx @@ -1,177 +1,194 @@ -"use client" +"use client"; -import { useState, type FormEvent, useEffect } from "react" -import { useRouter, useSearchParams } from "next/navigation" -import Image from "next/image" -import { motion } from "framer-motion" +import { useState, type FormEvent, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import Image from "next/image"; +import { motion } from "framer-motion"; export default function VerifyOTP() { - const [otp, setOtp] = useState(["", "", "", "", "", ""]) - const [error, setError] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [resendTimer, setResendTimer] = useState(0) - const router = useRouter() - const searchParams = useSearchParams() - const email = searchParams.get("email") - - // Protect the route - useEffect(() => { - if (!email) { - router.push("/auth/signin") - } - }, [email, router]) - - // Handle resend timer - useEffect(() => { - if (resendTimer > 0) { - const timer = setTimeout(() => setResendTimer((prev) => prev - 1), 1000) - return () => clearTimeout(timer) - } - }, [resendTimer]) - - const handleChange = (index: number, value: string) => { - if (value.length > 1) return - - const newOTP = [...otp] - newOTP[index] = value - setOtp(newOTP) - - if (value && index < 5) { - const nextInput = document.getElementById(`otp-${index + 1}`) - nextInput?.focus() - } - } - - const handleKeyDown = (index: number, e: React.KeyboardEvent) => { - if (e.key === "Backspace" && !otp[index] && index > 0) { - const prevInput = document.getElementById(`otp-${index - 1}`) - prevInput?.focus() - } - } - - const handleResendOTP = async () => { - if (resendTimer > 0 || !email) return - - setError("") - setIsLoading(true) - - try { - const response = await fetch("/api/auth/resend-otp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - }) - - if (response.ok) { - setResendTimer(30) // Start 30 second countdown - } else { - const data = await response.json() - setError(data.message || "Failed to resend OTP.") - } - } catch (error) { - setError(`An error occurred while resending OTP: ${error instanceof Error ? error.message : String(error)}`) - } finally { - setIsLoading(false) - } - } - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault() - if (!email) return - - setError("") - setIsLoading(true) - const otpString = otp.join("") - - try { - const response = await fetch("/api/auth/verify-otp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, otp: otpString }), - }) - - if (response.ok) { - // const data = await response.json() - router.push("/projects/12") - } else { - const data = await response.json() - setError(data.message || "Invalid OTP. Please try again.") - } - } catch (error) { - setError(`An error occurred during OTP verification: ${error instanceof Error ? error.message : String(error)}`) - } finally { - setIsLoading(false) - } - } - - if (!email) { - return null - } - - return ( -
-
-
- Boundless -

Verify Your Email

-

We sent a code to {email}

-

The OTP will expire in 10 minutes

-
- -
-
- {otp.map((digit, index) => ( - handleChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - className="w-12 h-12 text-center text-xl font-semibold border-2 border-[#194247] rounded-lg focus:outline-none focus:ring-2 focus:ring-[#194247] focus:border-transparent" - whileFocus={{ scale: 1.05 }} - required - disabled={isLoading} - /> - ))} -
- - {error && ( - - {error} - - )} - -
- - {isLoading ? "Verifying..." : "Verify Email"} - -
- -
- -
-
-
-
- ) + const [otp, setOtp] = useState(["", "", "", "", "", ""]); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [resendTimer, setResendTimer] = useState(0); + const router = useRouter(); + const searchParams = useSearchParams(); + const email = searchParams.get("email"); + + // Protect the route + useEffect(() => { + if (!email) { + router.push("/auth/signin"); + } + }, [email, router]); + + // Handle resend timer + useEffect(() => { + if (resendTimer > 0) { + const timer = setTimeout(() => setResendTimer((prev) => prev - 1), 1000); + return () => clearTimeout(timer); + } + }, [resendTimer]); + + const handleChange = (index: number, value: string) => { + if (value.length > 1) return; + + const newOTP = [...otp]; + newOTP[index] = value; + setOtp(newOTP); + + if (value && index < 5) { + const nextInput = document.getElementById(`otp-${index + 1}`); + nextInput?.focus(); + } + }; + + const handleKeyDown = (index: number, e: React.KeyboardEvent) => { + if (e.key === "Backspace" && !otp[index] && index > 0) { + const prevInput = document.getElementById(`otp-${index - 1}`); + prevInput?.focus(); + } + }; + + const handleResendOTP = async () => { + if (resendTimer > 0 || !email) return; + + setError(""); + setIsLoading(true); + + try { + const response = await fetch("/api/auth/resend-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + if (response.ok) { + setResendTimer(30); // Start 30 second countdown + } else { + const data = await response.json(); + setError(data.message || "Failed to resend OTP."); + } + } catch (error) { + setError( + `An error occurred while resending OTP: ${error instanceof Error ? error.message : String(error)}`, + ); + } finally { + setIsLoading(false); + } + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!email) return; + + setError(""); + setIsLoading(true); + const otpString = otp.join(""); + + try { + const response = await fetch("/api/auth/verify-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, otp: otpString }), + }); + + if (response.ok) { + // const data = await response.json() + router.push("/projects/12"); + } else { + const data = await response.json(); + setError(data.message || "Invalid OTP. Please try again."); + } + } catch (error) { + setError( + `An error occurred during OTP verification: ${error instanceof Error ? error.message : String(error)}`, + ); + } finally { + setIsLoading(false); + } + }; + + if (!email) { + return null; + } + + return ( +
+
+
+ Boundless +

+ Verify Your Email +

+

+ We sent a code to {email} +

+

+ The OTP will expire in 10 minutes +

+
+ +
+
+ {otp.map((digit, index) => ( + handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + className="w-12 h-12 text-center text-xl font-semibold border-2 border-[#194247] rounded-lg focus:outline-none focus:ring-2 focus:ring-[#194247] focus:border-transparent" + whileFocus={{ scale: 1.05 }} + required + disabled={isLoading} + /> + ))} +
+ + {error && ( + + {error} + + )} + +
+ + {isLoading ? "Verifying..." : "Verify Email"} + +
+ +
+ +
+
+
+
+ ); } - diff --git a/biome.json b/biome.json index 7d775796..85eda777 100644 --- a/biome.json +++ b/biome.json @@ -8,6 +8,9 @@ "linter": { "enabled": true, "rules": { + "correctness": { + "useExhaustiveDependencies": "off" + }, "recommended": true } } diff --git a/components/auth-tabs.tsx b/components/auth-tabs.tsx index 9e7a57d3..33b40287 100644 --- a/components/auth-tabs.tsx +++ b/components/auth-tabs.tsx @@ -1,132 +1,139 @@ -"use client" - -import { useState } from "react" -import { motion, AnimatePresence, Variants } from "framer-motion" -import Image from "next/image" -import SignInButtons from "@/components/signin-buttons" -import SignInForm from "@/components/signin-form" -import RegistrationForm from "./registeration-form" -import Link from "next/link" -import { BuiltInProviderType } from "next-auth/providers/index" -import { ClientSafeProvider, LiteralUnion } from "next-auth/react" +"use client"; +import { useState } from "react"; +import { motion, AnimatePresence, type Variants } from "framer-motion"; +import Image from "next/image"; +import SignInButtons from "@/components/signin-buttons"; +import SignInForm from "@/components/signin-form"; +import RegistrationForm from "./registeration-form"; +import Link from "next/link"; +import type { BuiltInProviderType } from "next-auth/providers/index"; +import type { ClientSafeProvider, LiteralUnion } from "next-auth/react"; const tabVariants: Variants = { - active: { - backgroundColor: "#194247", - color: "white", - transition: { duration: 0.2 } - }, - inactive: { - backgroundColor: "#dffce8", - color: "#194247", - transition: { duration: 0.2 } - }, -} + active: { + backgroundColor: "#194247", + color: "white", + transition: { duration: 0.2 }, + }, + inactive: { + backgroundColor: "#dffce8", + color: "#194247", + transition: { duration: 0.2 }, + }, +}; const contentVariants: Variants = { - enter: { - x: -20, - opacity: 0, - }, - center: { - x: 0, - opacity: 1, - transition: { - duration: 0.3, - ease: "easeOut" - } - }, - exit: { - x: 20, - opacity: 0, - transition: { - duration: 0.3, - ease: "easeIn" - } - } -} + enter: { + x: -20, + opacity: 0, + }, + center: { + x: 0, + opacity: 1, + transition: { + duration: 0.3, + ease: "easeOut", + }, + }, + exit: { + x: 20, + opacity: 0, + transition: { + duration: 0.3, + ease: "easeIn", + }, + }, +}; interface AuthTabsProps { - providers: Record, ClientSafeProvider> | null - + providers: Record< + LiteralUnion, + ClientSafeProvider + > | null; } export default function AuthTabs({ providers }: AuthTabsProps) { - const [activeTab, setActiveTab] = useState<"signin" | "register">("signin") - - return ( -
- {/* Changed space-y-8 to space-y-6 here */} -
-
- Boundless -

- {activeTab === "signin" ? "Sign in to your account" : "Create an account"} -

-
+ const [activeTab, setActiveTab] = useState<"signin" | "register">("signin"); + + return ( +
+ {/* Changed space-y-8 to space-y-6 here */} +
+
+ Boundless +

+ {activeTab === "signin" + ? "Sign in to your account" + : "Create an account"} +

+
- {/* Added -mb-2 here to reduce space between tabs and form */} -
- setActiveTab("signin")} - className="w-full rounded-l-md border border-[#194247] px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#194247] focus:z-10" - > - Sign In - - setActiveTab("register")} - className="w-full rounded-r-md border border-[#194247] px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#194247] focus:z-10" - > - Register - -
+ {/* Added -mb-2 here to reduce space between tabs and form */} +
+ setActiveTab("signin")} + className="w-full rounded-l-md border border-[#194247] px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#194247] focus:z-10" + > + Sign In + + setActiveTab("register")} + className="w-full rounded-r-md border border-[#194247] px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#194247] focus:z-10" + > + Register + +
- {/* Added mt-4 here to control space after tabs */} - - - - {activeTab === "signin" ? : } - - - + {/* Added mt-4 here to control space after tabs */} + + + + {activeTab === "signin" ? : } + + + -
-
-
-
-
-
- Or continue with -
-
- {providers && } -
-Forgot password? -
-
-
-
- ) -} \ No newline at end of file +
+
+
+
+
+
+ + Or continue with + +
+
+ {providers && } +
+ + Forgot password? + +
+
+
+
+ ); +} diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx index 60e6c96f..c9abc38e 100644 --- a/components/ui/breadcrumb.tsx +++ b/components/ui/breadcrumb.tsx @@ -1,115 +1,116 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode - } ->(({ ...props }, ref) =>