diff --git a/.env.local.example b/.env.local.example index 3765cf0..06ff0e5 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,2 +1,4 @@ NEXT_PUBLIC_SUPABASE_URL=your-project-url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \ No newline at end of file +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +NEXT_PUBLIC_AUTH_GOOGLE_ID=your-google-id +NEXT_PUBLIC_AUTH_GOOGLE_SECRET=your-google-secret \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 575d9d4..361a165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,19 @@ "dependencies": { "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@supabase/ssr": "^0.4.0", - "@supabase/supabase-js": "^2.45.1", + "@supabase/ssr": "^0.4.1", + "@supabase/supabase-js": "^2.45.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "embla-carousel-react": "^8.2.0", "lucide-react": "^0.428.0", "next": "14.2.5", "next-themes": "^0.3.0", "react": "^18", + "react-countup": "^6.5.3", "react-dom": "^18", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7" @@ -820,6 +824,29 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", + "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "dependencies": { + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -850,6 +877,28 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", + "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -1069,9 +1118,9 @@ } }, "node_modules/@supabase/ssr": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.4.0.tgz", - "integrity": "sha512-6WS3NUvHDhCPAFN2kJ79AQDO8+M9fJ7y2fYpxgZqIuJEpnnGsHDNnB5Xnv8CiaJIuRU+0pKboy62RVZBMfZ0Lg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.4.1.tgz", + "integrity": "sha512-000i7y4ITXjXU0T1JytZYU33VbUNklX9YN47hCweaLKsTBAEigJJJCeq3G+/IiwEggBt58Vu0KQ3UGXON7OmDQ==", "dependencies": { "cookie": "^0.6.0" }, @@ -1083,24 +1132,24 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.6.0.tgz", - "integrity": "sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.0.tgz", + "integrity": "sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==", "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "node_modules/@supabase/supabase-js": { - "version": "2.45.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.1.tgz", - "integrity": "sha512-/PVe3lXmalazD8BGMIoI7+ttvT1mLXy13lNcoAPtjP1TDDY83g8csZbVR6l+0/RZtvJxl3LGXfTJT4bjWgC5Nw==", + "version": "2.45.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.2.tgz", + "integrity": "sha512-kJKY3ISFusVKQWCP8Kqo20Ebxy2WLp6Ry/Suco0aQsPXH7bvn7clswsdhcfcH/5Tr0MYz/jcCjF0n/27SetiCw==", "dependencies": { "@supabase/auth-js": "2.64.4", "@supabase/functions-js": "2.4.1", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.15.8", "@supabase/realtime-js": "2.10.2", - "@supabase/storage-js": "2.6.0" + "@supabase/storage-js": "2.7.0" } }, "node_modules/@swc/counter": { @@ -1830,6 +1879,11 @@ "node": ">= 0.6" } }, + "node_modules/countup.js": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.0.tgz", + "integrity": "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2050,6 +2104,31 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/embla-carousel": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.2.0.tgz", + "integrity": "sha512-rf2GIX8rab9E6ZZN0Uhz05746qu2KrDje9IfFyHzjwxLwhvGjUt6y9+uaY1Sf+B0OPSa3sgas7BE2hWZCtopTA==" + }, + "node_modules/embla-carousel-react": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.2.0.tgz", + "integrity": "sha512-dWqbmaEBQjeAcy/EKrcAX37beVr0ubXuHPuLZkx27z58V1FIvRbbMb4/c3cLZx0PAv/ofngX2QFrwUB+62SPnw==", + "dependencies": { + "embla-carousel": "8.2.0", + "embla-carousel-reactive-utils": "8.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.2.0.tgz", + "integrity": "sha512-ZdaPNgMydkPBiDRUv+wRIz3hpZJ3LKrTyz+XWi286qlwPyZFJDjbzPBiXnC3czF9N/nsabSc7LTRvGauUzwKEg==", + "peerDependencies": { + "embla-carousel": "8.2.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -4470,6 +4549,17 @@ "node": ">=0.10.0" } }, + "node_modules/react-countup": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz", + "integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==", + "dependencies": { + "countup.js": "^2.8.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/package.json b/package.json index 16cb4e5..0b37c81 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@supabase/ssr": "^0.4.0", - "@supabase/supabase-js": "^2.45.1", + "@supabase/ssr": "^0.4.1", + "@supabase/supabase-js": "^2.45.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "embla-carousel-react": "^8.2.0", diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts new file mode 100644 index 0000000..451854e --- /dev/null +++ b/src/app/auth/callback/route.ts @@ -0,0 +1,24 @@ +import { createSupabaseClient } from "@/lib/supabase/serverComponentClient"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + + const code = searchParams.get("code"); + + // if "next" is in param, use it in the redirect URL + const next = searchParams.get("next") ?? "/"; + + if (code) { + const supabase = createSupabaseClient(); + + const { error } = await supabase.auth.exchangeCodeForSession(code); + + if (!error) { + return NextResponse.redirect(`${origin}${next}`); + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/error`); +} diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx new file mode 100644 index 0000000..7552b02 --- /dev/null +++ b/src/app/auth/error/page.tsx @@ -0,0 +1,3 @@ +export default function AuthError() { + return
Authentication Error
; +} diff --git a/src/app/login/page.tsx b/src/app/auth/page.tsx similarity index 73% rename from src/app/login/page.tsx rename to src/app/auth/page.tsx index 597d993..f5f5e10 100644 --- a/src/app/login/page.tsx +++ b/src/app/auth/page.tsx @@ -2,6 +2,9 @@ import Image from "next/image"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { LoginButton } from "@/components/auth/loginButton"; +import { LogoutButton } from "@/components/auth/logoutButton"; + export default function Login() { return (

Continue With

- - + +
By signing up, you agree to the Terms of Service and acknowledge you’ve read our Privacy Policy. diff --git a/src/app/page.tsx b/src/app/page.tsx index e664b98..0be7e81 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,7 +19,7 @@ export default function Home() {

Together, we turn ideas into impact.

@@ -79,10 +79,40 @@ export default function Home() {

The deals attracting the most interest right now

- - - - + + + +
+ ); +} diff --git a/src/components/auth/logoutButton.tsx b/src/components/auth/logoutButton.tsx new file mode 100644 index 0000000..d4cd348 --- /dev/null +++ b/src/components/auth/logoutButton.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { createSupabaseClient } from "@/lib/supabase/clientComponentClient"; +import { useRouter } from "next/navigation"; + +export function LogoutButton() { + const supabase = createSupabaseClient(); + const router = useRouter(); + + const handleLogout = async () => { + await supabase.auth.signOut(); + router.push("/"); + }; + + return ; +} diff --git a/src/components/navigationBar/Unsigned.tsx b/src/components/navigationBar/Unsigned.tsx index a641179..2bc21c2 100644 --- a/src/components/navigationBar/Unsigned.tsx +++ b/src/components/navigationBar/Unsigned.tsx @@ -31,31 +31,27 @@ const landings = [ route: "/crm-landing", }, ]; -const ListItem = React.forwardRef< - React.ElementRef<"a">, - React.ComponentPropsWithoutRef<"a"> ->(({ className, title, children, ...props }, ref) => { - return ( -
  • - - -
    {title}
    -
    -

    - {children} -

    -
    -
    -
  • - ); -}); +const ListItem = React.forwardRef, React.ComponentPropsWithoutRef<"a">>( + ({ className, title, children, ...props }, ref) => { + return ( +
  • + + +
    {title}
    +
    +

    {children}

    +
    +
    +
  • + ); + } +); ListItem.displayName = "ListItem"; export function UnsignedNav() { @@ -90,8 +86,7 @@ export function UnsignedNav() { + aria-label="Brand"> logo B2DVentures @@ -103,17 +98,11 @@ export function UnsignedNav() { - - Businesses - + Businesses
      {businessComponents.map((component) => ( - + {component.description} ))} @@ -122,17 +111,11 @@ export function UnsignedNav() { - - Projects - + Projects
        {projectComponents.map((component) => ( - + {component.description} ))} @@ -141,17 +124,11 @@ export function UnsignedNav() { - - Blogs - + Blogs
          {blogComponents.map((component) => ( - + {component.description} ))} @@ -160,10 +137,7 @@ export function UnsignedNav() { - + Docs @@ -177,7 +151,7 @@ export function UnsignedNav() {
          - + diff --git a/src/lib/supabase/clientComponentClient.ts b/src/lib/supabase/clientComponentClient.ts new file mode 100644 index 0000000..9a6ebd4 --- /dev/null +++ b/src/lib/supabase/clientComponentClient.ts @@ -0,0 +1,5 @@ +import { createBrowserClient } from "@supabase/ssr"; + +export function createSupabaseClient() { + return createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!); +} diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts new file mode 100644 index 0000000..996f463 --- /dev/null +++ b/src/lib/supabase/middleware.ts @@ -0,0 +1,57 @@ +import { createServerClient } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)); + supabaseResponse = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options)); + }, + }, + } + ); + + // IMPORTANT: Avoid writing any logic between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user && !request.nextUrl.pathname.startsWith("/auth") && !request.nextUrl.pathname.startsWith("/auth")) { + // no user, potentially respond by redirecting the user to the login page + const url = request.nextUrl.clone(); + url.pathname = "/auth"; + return NextResponse.redirect(url); + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're + // creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse; +} diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts deleted file mode 100644 index df4e8c5..0000000 --- a/src/lib/supabase/server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createServerClient } from "@supabase/ssr"; -import { cookies } from "next/headers"; - -export const createClient = () => { - const cookieStore = cookies(); - - return createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { - cookies: { - getAll() { - return cookieStore.getAll(); - }, - setAll(cookiesToSet) { - try { - cookiesToSet.forEach(({ name, value, options }) => { - cookieStore.set(name, value, options); - }); - } catch (error) { - // The `set` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } - }, - }, - }); -}; diff --git a/src/lib/supabase/serverComponentClient.ts b/src/lib/supabase/serverComponentClient.ts new file mode 100644 index 0000000..7f224fa --- /dev/null +++ b/src/lib/supabase/serverComponentClient.ts @@ -0,0 +1,19 @@ +import { createServerClient, type CookieOptions } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export function createSupabaseClient() { + const cookieStore = cookies(); + + return createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)); + } catch {} + }, + }, + }); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..9226aa2 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,19 @@ +import { type NextRequest } from "next/server"; +import { updateSession } from "@/lib/supabase/middleware"; + +export async function middleware(request: NextRequest) { + return await updateSession(request); +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + "/((?!_next/static|_next/image|$|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], +};