Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function Header() {
return pathname.startsWith(path)
}


const navItems = [
{ href: "/", label: "Home" },
{ href: "/about", label: "About" },
Expand Down Expand Up @@ -92,19 +92,17 @@ export default function Header() {
<Link
key={item.href}
href={item.href}
className={`text-sm font-medium transition-colors relative group ${
isActive(item.href)
className={`text-sm font-medium transition-colors relative group ${isActive(item.href)
? "text-primary"
: "text-foreground hover:text-primary"
}`}
}`}
>
{item.label}
<span
className={`absolute -bottom-1 left-0 h-0.5 bg-primary transition-all duration-300 ${
isActive(item.href)
className={`absolute -bottom-1 left-0 h-0.5 bg-primary transition-all duration-300 ${isActive(item.href)
? "w-full"
: "w-0 group-hover:w-full"
}`}
}`}
></span>
</Link>
))}
Expand All @@ -114,9 +112,14 @@ export default function Header() {
<div className="hidden md:flex items-center space-x-3 flex-shrink-0">
{/* <ThemeToggle /> */}
{loading ? (
<div className="text-sm text-muted-foreground">Loading...</div>
) : user ? (
<div className="flex items-center space-x-3">
{/* Skeleton for Sign In button */}
<div className="w-[70px] h-[34px] bg-muted/50 rounded-md animate-pulse" />
{/* Skeleton for Sign Up button */}
<div className="w-[75px] h-[34px] bg-muted/50 rounded-md animate-pulse" />
</div>
) : user ? (
<div className="flex items-center space-x-3" key={user.id}>
<PremiumButton user={user} />
<UserDisplay userId={user.id} showCodeuniaId={false} />
<UserIcon />
Expand All @@ -143,9 +146,8 @@ export default function Header() {
variant="ghost"
size="icon"
onClick={() => setIsMenuOpen(!isMenuOpen)}
className={`hover:scale-105 transition-all duration-200 ml-1 w-8 h-8 ${
isMenuOpen ? 'bg-muted/50' : ''
}`}
className={`hover:scale-105 transition-all duration-200 ml-1 w-8 h-8 ${isMenuOpen ? 'bg-muted/50' : ''
}`}
>
{isMenuOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
</Button>
Expand All @@ -157,11 +159,11 @@ export default function Header() {
{isMenuOpen && (
<>
{/* Backdrop overlay */}
<div
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[60] md:hidden animate-in fade-in duration-200"
onClick={() => setIsMenuOpen(false)}
/>

{/* Side drawer */}
<div className="mobile-menu-container fixed top-0 right-0 bottom-0 w-[280px] max-w-[85vw] bg-background border-l shadow-2xl z-[70] md:hidden animate-in slide-in-from-right duration-300">
<nav className="flex flex-col h-full">
Expand All @@ -186,11 +188,10 @@ export default function Header() {
<Link
key={item.href}
href={item.href}
className={`block text-sm font-medium transition-colors py-2.5 px-3 rounded-md relative ${
isActive(item.href)
className={`block text-sm font-medium transition-colors py-2.5 px-3 rounded-md relative ${isActive(item.href)
? "text-primary font-semibold bg-primary/10"
: "text-foreground hover:text-primary hover:bg-muted/50"
}`}
}`}
onClick={() => setIsMenuOpen(false)}
>
{item.label}
Expand All @@ -213,7 +214,7 @@ export default function Header() {
<Shield className="h-4 w-4" />
<span>Dashboard</span>
</Link>

{/* Logout Button */}
<button
onClick={handleLogout}
Expand Down
30 changes: 9 additions & 21 deletions lib/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isHydrated, setIsHydrated] = useState(false)
const [is_admin, setIsAdmin] = useState(false)

// Optimized profile fetching with caching
Expand Down Expand Up @@ -40,29 +39,23 @@ export function useAuth() {
}, [])

useEffect(() => {
setIsHydrated(true)
}, [])

useEffect(() => {
if (!isHydrated) return

let mounted = true

const initializeAuth = async () => {
try {
const supabase = createClient()

// Get initial session first
const { data: { session }, error: sessionError } = await supabase.auth.getSession()

if (mounted) {
if (sessionError) {
console.error('Session error:', sessionError)
setError(sessionError.message)
setIsAdmin(false)
} else {
setUser(session?.user ?? null)

// Check admin status from profiles table with caching
if (session?.user) {
const profile = await fetchUserProfile(session.user.id)
Expand All @@ -79,15 +72,15 @@ export function useAuth() {
async (event, session) => {
if (mounted) {
setUser(session?.user ?? null)

// Check admin status from profiles table with caching
if (session?.user) {
const profile = await fetchUserProfile(session.user.id)
setIsAdmin(profile?.is_admin || false)
} else {
setIsAdmin(false)
}

setLoading(false)
}
}
Expand All @@ -108,16 +101,11 @@ export function useAuth() {
return () => {
mounted = false
}
}, [isHydrated, fetchUserProfile])

// Return loading state during hydration
if (!isHydrated) {
return { user: null, loading: true, error: null, is_admin: false }
}
}, [fetchUserProfile])

return {
user,
loading,
return {
user,
loading,
error,
is_admin
}
Expand Down
75 changes: 55 additions & 20 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const nextConfig: NextConfig = {
generateBuildId: async () => {
// Use environment-specific build IDs for better cache control
const timestamp = Date.now()
const gitCommit = process.env.VERCEL_GIT_COMMIT_SHA?.substring(0, 7) ||
process.env.GITHUB_SHA?.substring(0, 7) ||
Math.random().toString(36).substring(7)
const gitCommit = process.env.VERCEL_GIT_COMMIT_SHA?.substring(0, 7) ||
process.env.GITHUB_SHA?.substring(0, 7) ||
Math.random().toString(36).substring(7)
const buildId = `${timestamp}-${gitCommit}`
console.log(`🏗️ Build ID: ${buildId}`)
return buildId
Expand All @@ -28,7 +28,7 @@ const nextConfig: NextConfig = {
crypto: false,
}
}

return config
},

Expand Down Expand Up @@ -107,15 +107,15 @@ const nextConfig: NextConfig = {
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
const buildId = process.env.BUILD_ID || Date.now().toString()

return [
// STATIC IMMUTABLE: Static assets (build files, immutable resources)
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: isDev
value: isDev
? 'no-cache, no-store, must-revalidate'
: 'public, max-age=31536000, immutable', // 1 year immutable
},
Expand All @@ -137,14 +137,14 @@ const nextConfig: NextConfig = {
},
],
},

// STATIC IMMUTABLE: Images and media files
{
source: '/(images|media|assets)/:path*.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot)',
headers: [
{
key: 'Cache-Control',
value: isDev
value: isDev
? 'no-cache, no-store, must-revalidate'
: 'public, max-age=2592000, immutable', // 30 days immutable
},
Expand All @@ -158,14 +158,41 @@ const nextConfig: NextConfig = {
},
],
},


// HOMEPAGE: No caching to ensure auth state is always fresh
{
source: '/',
headers: [
{
key: 'Cache-Control',
value: 'no-cache, no-store, must-revalidate',
},
{
key: 'CDN-Cache-Control',
value: 'no-cache',
},
{
key: 'Pragma',
value: 'no-cache',
},
{
key: 'Vary',
value: 'Cookie, Accept-Encoding',
},
{
key: 'X-Build-ID',
value: buildId,
},
],
},

// DYNAMIC CONTENT: Dynamic pages (events, hackathons, etc.)
{
source: '/(hackathons|events|leaderboard|opportunities)/:path*',
headers: [
{
key: 'Cache-Control',
value: isDev
value: isDev
? 'no-cache, no-store, must-revalidate'
: 'public, max-age=60, stale-while-revalidate=300', // 1min cache, 5min SWR
},
Expand All @@ -181,16 +208,20 @@ const nextConfig: NextConfig = {
key: 'Cache-Tag',
value: 'content',
},
{
key: 'Vary',
value: 'Cookie, Accept-Encoding',
},
],
},

// DATABASE QUERIES: API routes that query database
{
source: '/api/(hackathons|leaderboard|tests|verify-certificate)/:path*',
headers: [
{
key: 'Cache-Control',
value: isDev
value: isDev
? 'no-cache, no-store, must-revalidate'
: 'public, max-age=300, stale-while-revalidate=600', // 5min cache, 10min SWR
},
Expand All @@ -208,7 +239,7 @@ const nextConfig: NextConfig = {
},
],
},

// USER PRIVATE: Auth and user-specific routes
{
source: '/(protected|admin|profile|dashboard|auth)/:path*',
Expand All @@ -227,20 +258,20 @@ const nextConfig: NextConfig = {
},
],
},

// API STANDARD: General API routes and public pages
{
source: '/((?!_next|protected|admin|profile|dashboard|auth).*)',
headers: [
{
key: 'Cache-Control',
value: isDev
value: isDev
? 'no-cache, no-store, must-revalidate'
: 'public, max-age=120, stale-while-revalidate=300', // 2min cache, 5min SWR
: 'public, max-age=0, must-revalidate', // Always revalidate HTML
},
{
key: 'CDN-Cache-Control',
value: isProd ? 'public, max-age=120, stale-while-revalidate=300' : 'no-cache',
value: isProd ? 'public, max-age=60, stale-while-revalidate=300' : 'no-cache',
},
{
key: 'Cache-Tag',
Expand All @@ -250,6 +281,10 @@ const nextConfig: NextConfig = {
key: 'X-Build-ID',
value: buildId,
},
{
key: 'Vary',
value: 'Cookie, Accept-Encoding',
},
// Security headers
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
Expand All @@ -261,13 +296,13 @@ const nextConfig: NextConfig = {
},

reactStrictMode: false,

// Optimize for production builds
// swcMinify is now default in Next.js 15

// Enable static optimization
trailingSlash: false,

// Optimize for faster builds
onDemandEntries: {
maxInactiveAge: 25 * 1000,
Expand Down
Loading