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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# dependencies
/node_modules
/node_modules/
**/node_modules/
/.pnp
.pnp.*
.yarn/*
Expand Down
6 changes: 5 additions & 1 deletion app/api/certificates/bulk-generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ export async function POST(request: NextRequest) {
await new Promise(resolve => setTimeout(resolve, 200));

} catch (error) {
console.error(`Error generating certificate for ${participant.name}:`, error);
console.error('Error generating certificate for participant:', {
participantId: participant.id,
participantName: participant.name,
error: error instanceof Error ? error.message : 'Unknown error'
});
errors.push({
participantId: participant.id,
name: participant.name,
Expand Down
7 changes: 6 additions & 1 deletion app/api/certificates/bulk-send-email/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ export async function POST(request: NextRequest) {
await new Promise(resolve => setTimeout(resolve, 1000));

} catch (error) {
console.error(`Error sending email for ${cert.name}:`, error);
console.error('Error sending email for certificate:', {
certId: cert.certId,
certName: cert.name,
email: cert.email,
error: error instanceof Error ? error.message : 'Unknown error'
});
errors.push({
certId: cert.certId,
name: cert.name,
Expand Down
5 changes: 4 additions & 1 deletion app/api/certificates/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ export async function POST(request: NextRequest) {
template_url: templateUrl,
placeholders
});
console.log(`βœ… Activity logged: certificate_earned for user ${user.id} with cert ${certId}`);
console.log('βœ… Activity logged: certificate_earned', {
userId: user.id,
certId: certId
});
} catch (activityError) {
console.error('❌ Failed to log certificate earned activity:', activityError);
// Don't fail the certificate generation if activity logging fails
Expand Down
19 changes: 18 additions & 1 deletion app/api/certificates/upload-qr/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,24 @@ export async function POST(request: NextRequest) {
);
}

// Convert data URL to blob
// Validate and convert data URL to blob (SSRF protection)
if (!qrCodeDataUrl.startsWith('data:image/')) {
return NextResponse.json(
{ error: 'Invalid QR code data URL format' },
{ status: 400 }
);
}

// Additional validation for data URL format
const dataUrlPattern = /^data:image\/(png|jpeg|jpg|gif|webp);base64,/i;
if (!dataUrlPattern.test(qrCodeDataUrl)) {
return NextResponse.json(
{ error: 'Invalid image format in data URL' },
{ status: 400 }
);
}

// Convert data URL to blob (safe - no external requests)
const qrCodeBlob = await fetch(qrCodeDataUrl).then(r => r.blob());
const qrFileName = `qr-codes/${certId}.png`;

Expand Down
32 changes: 21 additions & 11 deletions app/api/email-preview/internship/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ export const runtime = 'nodejs';

// NextResponse imported but not used in GET handler - keeping for potential future use

// HTML escaping function to prevent XSS
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

// Mock data for email preview
const mockData = {
applicantName: 'John Doe',
Expand Down Expand Up @@ -59,23 +69,23 @@ const getApplicantConfirmationTemplate = (data: typeof mockData) => {
<div class="content">
<div class="success-badge">βœ… Successfully Applied</div>

<p>Hi ${applicantName},</p>
<p>Hi ${escapeHtml(applicantName)},</p>

<p>Great news! Your application for <strong>${internshipTitle}</strong> has been successfully submitted and confirmed.</p>
<p>Great news! Your application for <strong>${escapeHtml(internshipTitle)}</strong> has been successfully submitted and confirmed.</p>

<div class="details-card">
<h3 style="margin-top: 0; color: #1f2937;">Application Details</h3>
<div class="detail-row">
<span class="detail-label">Internship:</span>
<span class="detail-value">${internshipTitle}</span>
<span class="detail-value">${escapeHtml(internshipTitle)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Domain:</span>
<span class="detail-value">${domain}</span>
<span class="detail-value">${escapeHtml(domain)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Level:</span>
<span class="detail-value">${level}</span>
<span class="detail-value">${escapeHtml(level)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Duration:</span>
Expand Down Expand Up @@ -185,15 +195,15 @@ const getAdminNotificationTemplate = (data: typeof mockData) => {
<h3 style="margin-top: 0; color: #1f2937;">Application Details</h3>
<div class="detail-row">
<span class="detail-label">Internship:</span>
<span class="detail-value">${internshipTitle}</span>
<span class="detail-value">${escapeHtml(internshipTitle)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Domain:</span>
<span class="detail-value">${domain}</span>
<span class="detail-value">${escapeHtml(domain)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Level:</span>
<span class="detail-value">${level}</span>
<span class="detail-value">${escapeHtml(level)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Duration:</span>
Expand Down Expand Up @@ -371,15 +381,15 @@ const getStatusUpdateTemplate = (data: {
<h3 style="margin-top: 0; color: #1f2937;">Application Details</h3>
<div class="detail-row">
<span class="detail-label">Internship:</span>
<span class="detail-value">${internshipTitle}</span>
<span class="detail-value">${escapeHtml(internshipTitle)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Domain:</span>
<span class="detail-value">${domain}</span>
<span class="detail-value">${escapeHtml(domain)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Level:</span>
<span class="detail-value">${level}</span>
<span class="detail-value">${escapeHtml(level)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Duration:</span>
Expand Down
30 changes: 30 additions & 0 deletions app/api/reserved-usernames/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';

export async function GET() {
try {
const supabase = await createClient();

const { data, error } = await supabase
.from('reserved_usernames')
.select('*')
.eq('is_active', true)
.order('username');

if (error) {
console.error('Error fetching reserved usernames:', error);
return NextResponse.json(
{ error: 'Failed to fetch reserved usernames' },
{ status: 500 }
);
}

return NextResponse.json(data || []);
} catch (error) {
console.error('Error in reserved usernames API:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
17 changes: 17 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<head>
{/* Critical CSS for above-the-fold content */}
<style dangerouslySetInnerHTML={{
__html: `
/* Critical CSS for initial render */
body { margin: 0; font-family: var(--font-geist-sans), system-ui, sans-serif; }
.hero-section { min-height: 100vh; display: flex; align-items: center; }
.loading-spinner { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.fade-in { animation: fadeIn 0.5s ease-in; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
`
}} />

{/* Preload critical resources */}
<link rel="preload" href="/fonts/geist-sans.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
<link rel="preload" href="/fonts/geist-mono.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />

{/* Structured Data */}
<script
type="application/ld+json"
Expand Down
61 changes: 44 additions & 17 deletions components/home/HeroSection2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,34 @@ const Particles = dynamic(() => import("@/components/ui/particles").then(mod =>

export function HeroSection2() {
const { quality } = useLocalPerformanceMonitor()
const [isVisible, setIsVisible] = useState(false)

// Intersection observer for lazy loading 3D content
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)

const element = document.getElementById('hero-globe')
if (element) {
observer.observe(element)
}

return () => observer.disconnect()
}, [])

// Memoized globe configuration with performance-based adjustments
const globeConfig = useMemo(() => {
const baseConfig = {
pointSize: 4,
pointSize: 3, // Reduced from 4
globeColor: "#062056",
showAtmosphere: true,
showAtmosphere: false, // Disabled by default for better performance
atmosphereColor: "#FFFFFF",
atmosphereAltitude: 0.1,
emissive: "#062056",
Expand All @@ -91,31 +112,31 @@ export function HeroSection2() {
pointLight: "#ffffff",
arcTime: 1000,
arcLength: 0.9,
rings: 1,
maxRings: 3,
rings: 0, // Reduced from 1
maxRings: 2, // Reduced from 3
initialPosition: { lat: 22.3193, lng: 114.1694 },
autoRotate: true,
autoRotateSpeed: 0.5,
autoRotateSpeed: 0.3, // Reduced from 0.5
}

// Adjust quality based on performance
switch (quality) {
case 'low':
return {
...baseConfig,
pointSize: 2,
pointSize: 1,
showAtmosphere: false,
rings: 0,
maxRings: 1,
autoRotateSpeed: 0.2,
maxRings: 0,
autoRotateSpeed: 0.1,
}
case 'medium':
return {
...baseConfig,
pointSize: 3,
rings: 1,
maxRings: 2,
autoRotateSpeed: 0.3,
pointSize: 2,
rings: 0,
maxRings: 1,
autoRotateSpeed: 0.2,
}
default:
return baseConfig
Expand Down Expand Up @@ -634,17 +655,23 @@ export function HeroSection2() {
</div>


<div className="relative animate-fade-in w-full lg:order-2" style={{ animationDelay: '0.6s' }}>
<div id="hero-globe" className="relative animate-fade-in w-full lg:order-2" style={{ animationDelay: '0.6s' }}>
<div className="h-[300px] sm:h-[400px] md:h-[500px] lg:h-[600px] w-full relative">
<div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-purple-500/20 rounded-2xl sm:rounded-3xl blur-2xl sm:blur-3xl opacity-30 animate-pulse" />
<div className="relative z-10 h-full">
<Suspense fallback={
{isVisible ? (
<Suspense fallback={
<div className="h-full w-full flex items-center justify-center">
<div className="animate-pulse text-primary">Loading...</div>
</div>
}>
<World globeConfig={globeConfig} data={sampleArcs} />
</Suspense>
) : (
<div className="h-full w-full flex items-center justify-center">
<div className="animate-pulse text-primary">Loading...</div>
</div>
}>
<World globeConfig={globeConfig} data={sampleArcs} />
</Suspense>
)}
</div>
</div>

Expand Down
Loading
Loading