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} */
const config = {
// You can put your base config options here
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gibbyb.com',
},
{
protocol: 'https',
hostname: '*.gbrown.org',
},
],
},
};
// Sentry configuration

View File

@ -25,15 +25,15 @@
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@sentry/nextjs": "^9",
"@sentry/nextjs": "^9.6.1",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.1",
"@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.3.0",
"geist": "^1.3.1",
"lucide-react": "^0.483.0",
"next": "^15.0.1",
"next": "^15.2.3",
"next-themes": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -45,22 +45,22 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.1",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.5.3"
"@types/eslint": "^8.56.12",
"@types/node": "^20.17.24",
"@types/react": "^18.3.19",
"@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"eslint": "^8.57.1",
"eslint-config-next": "^15.2.3",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2"
},
"ct3aMetadata": {
"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();
if (!session) {
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 />
</div>
);

View File

@ -13,12 +13,14 @@ import {
import { createClient } from '@/utils/supabase/client';
import type { Session } from '@supabase/supabase-js';
import { Button } from '@/components/ui/button';
import { redirect } from 'next/navigation';
import type { User } from '@/lib/types';
const AvatarDropdown = () => {
const supabase = createClient();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [pfp, setPfp] = useState<string>('/images/default_user_pfp.png');
useEffect(() => {
// Function to fetch the session
@ -28,6 +30,24 @@ const AvatarDropdown = () => {
data: { session },
} = await supabase.auth.getSession();
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) {
console.error('Error fetching session:', error);
} finally {
@ -53,10 +73,36 @@ const AvatarDropdown = () => {
};
}, [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
const handleSignOut = async () => {
await supabase.auth.signOut();
redirect('/');
};
// Show nothing while loading
@ -69,18 +115,6 @@ const AvatarDropdown = () => {
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 (!session) return <div />;
return (
@ -89,7 +123,7 @@ const AvatarDropdown = () => {
<DropdownMenuTrigger>
<Image
src={pfp}
alt='User profile'
alt={getInitials(user?.full_name) ?? 'NA'}
width={35}
height={35}
className='rounded-full border-2 border-white m-auto mr-1 md:mr-2
@ -97,7 +131,7 @@ const AvatarDropdown = () => {
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>{name}</DropdownMenuLabel>
<DropdownMenuLabel>{user?.full_name}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button onClick={handleSignOut} className='w-full text-left'>

View File

@ -1,9 +1,7 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import React, { createContext, useContext, useState } from 'react';
import Image from 'next/image';
import type { ReactNode } from 'react';
import type { Session } from '@supabase/supabase-js';
import { createClient } from '@/utils/supabase/client';
interface TVModeContextProps {
tvMode: boolean;
@ -34,62 +32,31 @@ export const useTVMode = () => {
return context;
};
export const TVToggle = () => {
type TVToggleProps = {
width?: number;
height?: number;
};
export const TVToggle = ({
width = 25,
height = 25,
}: TVToggleProps) => {
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 (
<button onClick={toggleTVMode} className='mr-4 mt-1'>
{tvMode ? (
<Image
src='/images/exit_fullscreen.svg'
alt='Exit TV Mode'
width={25}
height={25}
width={width}
height={height}
/>
) : (
<Image
src='/images/fullscreen.svg'
alt='Enter TV Mode'
width={25}
height={25}
width={width}
height={height}
/>
)}
</button>

View File

@ -1,20 +1,69 @@
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { TVToggle, useTVMode } from '@/components/context/TVMode';
import { ThemeToggle } from '@/components/context/Theme';
import AvatarDropdown from '@/components/auth/AvatarDropdown';
import { createClient } from '@/utils/supabase/client';
import type { Session } from '@supabase/supabase-js';
const Header = () => {
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) {
return (
<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'>
<AvatarDropdown />
<div className='mb-0.5 ml-2'>
<TVToggle />
</div>
<div className='flex flex-row my-auto items-center
justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
>
<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>
);
@ -22,15 +71,20 @@ const Header = () => {
return (
<header className='w-full min-h-[10vh]'>
<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:pt-4 sm:pr-8'
<div className='flex flex-row my-auto items-center
justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
>
<AvatarDropdown />
<div className='mb-0.5 ml-2'>
<TVToggle />
</div>
<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

View File

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