This commit is contained in:
Gabriel Brown 2025-03-20 09:56:35 -05:00
parent d1e9c7e6bb
commit a346612d48
8 changed files with 639 additions and 569 deletions

View File

@ -8,6 +8,18 @@ import { withSentryConfig } from '@sentry/nextjs';
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
// You can put your base config options here // You can put your base config options here
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gibbyb.com',
},
{
protocol: 'https',
hostname: '*.gbrown.org',
},
],
},
}; };
// Sentry configuration // Sentry configuration

View File

@ -25,15 +25,15 @@
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@sentry/nextjs": "^9", "@sentry/nextjs": "^9.6.1",
"@supabase/ssr": "^0.6.1", "@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.1", "@supabase/supabase-js": "^2.49.1",
"@t3-oss/env-nextjs": "^0.10.1", "@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"geist": "^1.3.0", "geist": "^1.3.1",
"lucide-react": "^0.483.0", "lucide-react": "^0.483.0",
"next": "^15.0.1", "next": "^15.2.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -45,22 +45,22 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@types/eslint": "^8.56.10", "@types/eslint": "^8.56.12",
"@types/node": "^20.14.10", "@types/node": "^20.17.24",
"@types/react": "^18.3.3", "@types/react": "^18.3.19",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.1.0", "@typescript-eslint/parser": "^8.27.0",
"eslint": "^8.57.0", "eslint": "^8.57.1",
"eslint-config-next": "^15.0.1", "eslint-config-next": "^15.2.3",
"postcss": "^8.4.39", "postcss": "^8.5.3",
"prettier": "^3.3.2", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.17",
"typescript": "^5.5.3" "typescript": "^5.8.2"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.38.1" "initVersion": "7.38.1"
}, },
"packageManager": "pnpm@10.5.2" "packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af"
} }

952
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,9 @@ const HomePage = async () => {
const { data: { session } } = await supabase.auth.getSession(); const { data: { session } } = await supabase.auth.getSession();
if (!session) { if (!session) {
return ( return (
<div className='flex flex-col items-center justify-center md:min-h-[70vh]'> <div className='flex flex-col items-center
justify-center md:min-h-[70vh]'
>
<LoginForm /> <LoginForm />
</div> </div>
); );

View File

@ -13,12 +13,14 @@ import {
import { createClient } from '@/utils/supabase/client'; import { createClient } from '@/utils/supabase/client';
import type { Session } from '@supabase/supabase-js'; import type { Session } from '@supabase/supabase-js';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { redirect } from 'next/navigation'; import type { User } from '@/lib/types';
const AvatarDropdown = () => { const AvatarDropdown = () => {
const supabase = createClient(); const supabase = createClient();
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [pfp, setPfp] = useState<string>('/images/default_user_pfp.png');
useEffect(() => { useEffect(() => {
// Function to fetch the session // Function to fetch the session
@ -28,6 +30,24 @@ const AvatarDropdown = () => {
data: { session }, data: { session },
} = await supabase.auth.getSession(); } = await supabase.auth.getSession();
setSession(session); setSession(session);
if (session?.user?.id) {
const { data: userData, error } = await supabase
.from('profiles')
.select('*')
.eq('id', session?.user.id)
.single();
if (error) {
console.error('Error fetching user data:', error);
return;
}
if (userData) {
const user = userData as User;
console.log(user);
setUser(user);
}
}
} catch (error) { } catch (error) {
console.error('Error fetching session:', error); console.error('Error fetching session:', error);
} finally { } finally {
@ -53,10 +73,36 @@ const AvatarDropdown = () => {
}; };
}, [supabase]); }, [supabase]);
useEffect(() => {
if (user?.avatar_url) {
console.log('Avatar Url:', user.avatar_url);
if (user.avatar_url.startsWith('http')) {
setPfp(user.avatar_url);
return;
}
// Get public URL - this is synchronous
const { data } = supabase
.storage
.from('avatars')
.getPublicUrl(user.avatar_url);
console.log('Avatar URL:', data.publicUrl);
setPfp(data.publicUrl);
}
}, [user, supabase]);
const getInitials = (fullName: string | undefined): string => {
if (!fullName || fullName.trim() === '' || fullName === 'undefined') return 'NA';
const nameParts = fullName.trim().split(' ');
const firstInitial = nameParts[0]?.charAt(0).toUpperCase() ?? 'N';
if (nameParts.length === 1) return 'NA';
const lastIntitial = nameParts[nameParts.length -1]?.charAt(0).toUpperCase() ?? 'A';
return firstInitial + lastIntitial;
};
// Handle sign out // Handle sign out
const handleSignOut = async () => { const handleSignOut = async () => {
await supabase.auth.signOut(); await supabase.auth.signOut();
redirect('/');
}; };
// Show nothing while loading // Show nothing while loading
@ -69,18 +115,6 @@ const AvatarDropdown = () => {
return <div />; return <div />;
} }
// Get user details
const pfp =
(session.user?.user_metadata?.avatar_url as string) ??
(session.user?.user_metadata?.picture as string) ??
'/images/default_user_pfp.png';
const name: string =
(session.user?.user_metadata?.full_name as string) ??
(session.user?.user_metadata?.name as string) ??
(session.user?.email as string) ??
('Profile' as string);
// If no session, return empty div // If no session, return empty div
if (!session) return <div />; if (!session) return <div />;
return ( return (
@ -89,7 +123,7 @@ const AvatarDropdown = () => {
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Image <Image
src={pfp} src={pfp}
alt='User profile' alt={getInitials(user?.full_name) ?? 'NA'}
width={35} width={35}
height={35} height={35}
className='rounded-full border-2 border-white m-auto mr-1 md:mr-2 className='rounded-full border-2 border-white m-auto mr-1 md:mr-2
@ -97,7 +131,7 @@ const AvatarDropdown = () => {
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel>{name}</DropdownMenuLabel> <DropdownMenuLabel>{user?.full_name}</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Button onClick={handleSignOut} className='w-full text-left'> <Button onClick={handleSignOut} className='w-full text-left'>

View File

@ -1,9 +1,7 @@
'use client'; 'use client';
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { Session } from '@supabase/supabase-js';
import { createClient } from '@/utils/supabase/client';
interface TVModeContextProps { interface TVModeContextProps {
tvMode: boolean; tvMode: boolean;
@ -34,62 +32,31 @@ export const useTVMode = () => {
return context; return context;
}; };
export const TVToggle = () => { type TVToggleProps = {
width?: number;
height?: number;
};
export const TVToggle = ({
width = 25,
height = 25,
}: TVToggleProps) => {
const { tvMode, toggleTVMode } = useTVMode(); const { tvMode, toggleTVMode } = useTVMode();
const supabase = createClient();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Function to fetch the session
async function fetchSession() {
try {
const {
data: { session },
} = await supabase.auth.getSession();
setSession(session);
} catch (error) {
console.error('Error fetching session:', error);
} finally {
setLoading(false);
}
}
// Call the function
fetchSession().catch((error) => {
console.error('Error fetching session:', error);
});
// Set up auth state change listener
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
// Clean up the subscription when component unmounts
return () => {
subscription.unsubscribe();
};
}, [supabase]);
if (loading || !session) return <div />;
return ( return (
<button onClick={toggleTVMode} className='mr-4 mt-1'> <button onClick={toggleTVMode} className='mr-4 mt-1'>
{tvMode ? ( {tvMode ? (
<Image <Image
src='/images/exit_fullscreen.svg' src='/images/exit_fullscreen.svg'
alt='Exit TV Mode' alt='Exit TV Mode'
width={25} width={width}
height={25} height={height}
/> />
) : ( ) : (
<Image <Image
src='/images/fullscreen.svg' src='/images/fullscreen.svg'
alt='Enter TV Mode' alt='Enter TV Mode'
width={25} width={width}
height={25} height={height}
/> />
)} )}
</button> </button>

View File

@ -1,20 +1,69 @@
'use client'; 'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { TVToggle, useTVMode } from '@/components/context/TVMode'; import { TVToggle, useTVMode } from '@/components/context/TVMode';
import { ThemeToggle } from '@/components/context/Theme'; import { ThemeToggle } from '@/components/context/Theme';
import AvatarDropdown from '@/components/auth/AvatarDropdown'; import AvatarDropdown from '@/components/auth/AvatarDropdown';
import { createClient } from '@/utils/supabase/client';
import type { Session } from '@supabase/supabase-js';
const Header = () => { const Header = () => {
const { tvMode } = useTVMode(); const { tvMode } = useTVMode();
const supabase = createClient();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Function to fetch the session
async function fetchSession() {
try {
const {
data: { session },
} = await supabase.auth.getSession();
setSession(session);
} catch (error) {
console.error('Error fetching session:', error);
} finally {
setLoading(false);
}
}
// Call the function
fetchSession().catch((error) => {
console.error('Error fetching session:', error);
});
// Set up auth state change listener
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
// Clean up the subscription when component unmounts
return () => {
subscription.unsubscribe();
};
}, [supabase]);
if (tvMode) { if (tvMode) {
return ( return (
<div className='w-full flex flex-row items-end justify-end'> <div className='w-full flex flex-row items-end justify-end'>
<div className='flex flex-row my-auto items-center pt-2 pr-0 sm:pr-8 sm:pt-4'> <div className='flex flex-row my-auto items-center
<AvatarDropdown /> justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
<div className='mb-0.5 ml-2'> >
<TVToggle />
</div>
<ThemeToggle /> <ThemeToggle />
{session && !loading && (
<div className='flex flex-row my-auto items-center
justify-center'
>
<div className='mb-0.5 ml-4'>
<TVToggle width={22} height={22} />
</div>
<AvatarDropdown />
</div>
)}
</div> </div>
</div> </div>
); );
@ -22,15 +71,20 @@ const Header = () => {
return ( return (
<header className='w-full min-h-[10vh]'> <header className='w-full min-h-[10vh]'>
<div className='w-full flex flex-row items-end justify-end'> <div className='w-full flex flex-row items-end justify-end'>
<div <div className='flex flex-row my-auto items-center
className='flex flex-row my-auto items-center justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
pt-2 pr-0 sm:pt-4 sm:pr-8'
> >
<AvatarDropdown />
<div className='mb-0.5 ml-2'>
<TVToggle />
</div>
<ThemeToggle /> <ThemeToggle />
{session && !loading && (
<div className='flex flex-row my-auto items-center
justify-center'
>
<div className='mb-0.5 ml-4'>
<TVToggle width={22} height={22} />
</div>
<AvatarDropdown />
</div>
)}
</div> </div>
</div> </div>
<div <div

View File

@ -4,6 +4,7 @@ export type User = {
email: string; email: string;
avatar_url?: string; avatar_url?: string;
provider: string; provider: string;
updated_at?: Date;
}; };
export type Status = { export type Status = {