From 71903da24163cc6a89994cd42c8ed34ddae55681 Mon Sep 17 00:00:00 2001 From: Deepak Date: Mon, 29 Jun 2026 12:30:56 +0530 Subject: [PATCH 1/4] perf: achieve 100/100 react-doctor score - fix security, perf, a11y, bugs --- app/api/interview/view/[id]/route.ts | 6 +- app/api/og/route.tsx | 6 +- app/api/pipeline/process/route.ts | 7 +- app/api/pipeline/route.ts | 88 +- app/api/seed-experiences/route.ts | 43 +- app/api/summarize/route.ts | 2 +- app/blog/page.tsx | 11 +- app/companies/[company]/page.tsx | 29 +- app/companies/page.tsx | 2 +- app/error.tsx | 1 + app/interview-experience/[slug]/page.tsx | 29 +- app/interview-experience/page.tsx | 25 +- app/page.tsx | 4 +- app/posts/[slug]/page.tsx | 33 +- app/privacy/page.tsx | 2 +- app/sitemap.ts | 19 +- app/terms/page.tsx | 2 +- components/add-new-experience.tsx | 64 +- components/admin-dashboard.tsx | 6 +- components/blog-cta-section.tsx | 42 +- components/common/card.tsx | 10 +- components/common/confetti.tsx | 6 +- components/common/footer.tsx | 18 +- components/common/login.tsx | 7 +- components/common/mobile-sticky-cta.tsx | 6 +- components/common/site-header.tsx | 50 +- components/common/text-editor.tsx | 18 +- components/experiences.tsx | 101 +- components/features-section.tsx | 12 +- components/hero-section.tsx | 8 +- components/home.tsx | 26 +- components/motion-provider.tsx | 5 + components/query-pagination.tsx | 2 +- components/session-provider.tsx | 49 +- components/share-buttons.tsx | 1 + components/stats-section.tsx | 8 +- components/structured-data.tsx | 58 +- components/ui/alert.tsx | 42 +- components/ui/button.tsx | 29 +- components/ui/card.tsx | 71 +- components/ui/dialog.tsx | 53 +- components/ui/pagination.tsx | 24 +- components/ui/select.tsx | 95 +- components/ui/sheet.tsx | 54 +- components/ui/tabs.tsx | 41 +- components/ui/toast.tsx | 81 +- components/ui/tooltip.tsx | 14 +- components/view-counter.tsx | 1 + components/youtube.tsx | 1 + doctor.config.json | 22 + .../components/btn-container/BtnContainer.tsx | 12 +- form/components/date-selector/DateSelect.tsx | 52 +- .../DropdownSelectOption.tsx | 5 +- form/components/main-content/MainContent.tsx | 47 +- .../QuestionInputIndustries.tsx | 41 +- .../InterviewInputText.tsx | 79 +- .../question-input-text/QuestionInputText.tsx | 67 +- .../QuestionNumHeading.tsx | 4 +- form/components/question/DateInput.tsx | 14 +- form/components/question/EmailInput.tsx | 16 +- form/components/question/FirstNameInput.tsx | 14 +- form/components/question/GoalInput.tsx | 16 +- form/components/question/IndustryInput.tsx | 14 +- form/components/question/InterviewInput.tsx | 16 +- form/components/question/Intro.tsx | 6 +- form/components/question/LastNameInput.tsx | 14 +- form/components/question/PostType.tsx | 18 +- form/components/question/Question.tsx | 24 +- form/components/question/RoleInput.tsx | 18 +- form/constants/GOALS.ts | 2 +- form/constants/ROLES.ts | 2 +- form/contexts/questions-context.tsx | 9 +- form/contexts/shared-states-context.tsx | 26 +- form/hooks/useHandleKeypress.ts | 2 +- form/hooks/useHandleScroll.ts | 4 +- form/hooks/useSubmit.ts | 4 +- .../reducer-func/questionsReducerFunc.ts | 4 +- form/types/contexts.ts | 5 +- hooks/getExperiences.ts | 36 +- hooks/useGetCompanies.ts | 2 +- hooks/useGetStats.ts | 1 + lib/content-pipeline/sources/devto.ts | 21 +- lib/content-pipeline/sources/medium.ts | 24 +- lib/is-mobile.ts | 4 + lib/supabase-browser.ts | 12 +- lib/supabase.ts | 12 +- next.config.mjs | 12 + package-lock.json | 6094 ++++++++++------- package.json | 6 +- 89 files changed, 4790 insertions(+), 3303 deletions(-) create mode 100644 components/motion-provider.tsx create mode 100644 doctor.config.json create mode 100644 lib/is-mobile.ts diff --git a/app/api/interview/view/[id]/route.ts b/app/api/interview/view/[id]/route.ts index 980b1cf..434ec62 100644 --- a/app/api/interview/view/[id]/route.ts +++ b/app/api/interview/view/[id]/route.ts @@ -10,8 +10,7 @@ export async function GET( req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - const { id } = await params; - const cookieStore = await cookies(); + const [{ id }, cookieStore] = await Promise.all([params, cookies()]); const supabase = createServerClient( supabaseUrl, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, @@ -45,8 +44,7 @@ export async function POST( req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { - const { id } = await params; - const cookieStore = await cookies(); + const [{ id }, cookieStore] = await Promise.all([params, cookies()]); const supabase = createServerClient( supabaseUrl, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index 05406be..f73c8ae 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -23,9 +23,9 @@ export async function GET(req: NextRequest) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - stroke-width='2' - stroke-linecap='round' - stroke-linejoin='round' + strokeWidth='2' + strokeLinecap='round' + strokeLinejoin='round' > diff --git a/app/api/pipeline/process/route.ts b/app/api/pipeline/process/route.ts index 34c5c39..ec09363 100644 --- a/app/api/pipeline/process/route.ts +++ b/app/api/pipeline/process/route.ts @@ -36,7 +36,7 @@ async function fetchMediumContent(url: string) { } } -export async function GET(request: Request) { +export async function POST(request: Request) { // Security Check const { searchParams } = new URL(request.url); const key = searchParams.get('key'); @@ -66,7 +66,7 @@ export async function GET(request: Request) { ); } - const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); + const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }); const results = { scraped: 0, user: 0, legacy: 0, errors: [] as string[] }; const BATCH_SIZE = 200; @@ -87,6 +87,7 @@ export async function GET(request: Request) { } if (scrapedData && scrapedData.length > 0) { + // Sequential processing: AI model calls must be serialized to avoid rate limits for (const item of scrapedData) { try { let content = item.metadata?.content || item.summary || ''; @@ -141,6 +142,7 @@ export async function GET(request: Request) { .limit(BATCH_SIZE); if (userData && userData.length > 0) { + // Sequential processing: each item requires AI model call, processed one at a time to avoid rate limits for (const item of userData) { try { const content = item.description || ''; @@ -186,6 +188,7 @@ export async function GET(request: Request) { } if (legacyData && legacyData.length > 0) { + // Sequential processing: each item requires AI model call, processed one at a time to avoid rate limits for (const item of legacyData) { try { const content = item.detail_experience || item.summary || ''; diff --git a/app/api/pipeline/route.ts b/app/api/pipeline/route.ts index 3beb5e8..9be0764 100644 --- a/app/api/pipeline/route.ts +++ b/app/api/pipeline/route.ts @@ -1,6 +1,4 @@ export const dynamic = 'force-dynamic'; -import { createServerClient } from '@supabase/ssr'; -import { cookies } from 'next/headers'; import { NextResponse } from 'next/server'; import { LeetCodeSource } from '@/lib/content-pipeline/sources/leetcode'; import { MediumSource } from '@/lib/content-pipeline/sources/medium'; @@ -8,76 +6,51 @@ import { DevToSource } from '@/lib/content-pipeline/sources/devto'; import { TelegramSource } from '@/lib/content-pipeline/sources/telegram'; import { HashnodeSource } from '@/lib/content-pipeline/sources/hashnode'; import { ScrapedArticle } from '@/lib/content-pipeline/types'; +import { getAuthState } from '@/lib/auth'; -export async function GET(request: Request) { - // Security: Check for a secret token OR an admin session +const sources = [ + // new LeetCodeSource(), // LeetCode is currently blocked by Cloudflare + new MediumSource(), + new DevToSource(), + new TelegramSource(), + new HashnodeSource(), +]; + +export async function POST(request: Request) { + // Authorized by a valid cron token OR an admin session (consistent rule). const { searchParams } = new URL(request.url); const token = searchParams.get('token'); const cronSecret = process.env.CRON_SECRET; - const cookieStore = await cookies(); - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - get(name: string) { - return cookieStore.get(name)?.value; - }, - }, - } - ); - - const { - data: { session }, - } = await supabase.auth.getSession(); - const isAdmin = session?.user.email === 'deepaksharma834@gmail.com'; - - const isAuthorized = (cronSecret && token === cronSecret) || isAdmin; + const hasValidToken = Boolean(cronSecret && token === cronSecret); + const isAdmin = hasValidToken ? false : (await getAuthState()).isAdmin; + const isAuthorized = hasValidToken || isAdmin; if (!isAuthorized) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { - const sources = [ - // new LeetCodeSource(), // LeetCode is currently blocked by Cloudflare - new MediumSource(), - new DevToSource(), - new TelegramSource(), - new HashnodeSource(), - ]; const allArticles: ScrapedArticle[] = []; - // 1. Fetch from all sources - for (const source of sources) { - console.log(`[Pipeline] Fetching from ${source.name}...`); - const articles = await source.fetchArticles(50); // Fetches 50 from each source - console.log(`[Pipeline] Got ${articles.length} from ${source.name}`); + // 1. Fetch from all sources in parallel + const sourceResults = await Promise.all( + sources.map(async (source) => { + console.log(`[Pipeline] Fetching from ${source.name}...`); + const articles = await source.fetchArticles(50); + console.log(`[Pipeline] Got ${articles.length} from ${source.name}`); + return articles; + }) + ); + for (const articles of sourceResults) { allArticles.push(...articles); } // 2. Save to Database const results = await Promise.allSettled( allArticles.map(async (article) => { - // Upsert based on original_url? - // schema: original_url TEXT UNIQUE NOT NULL - - // For user-facing operations (respects RLS, checks auth) - const cookieStore = await cookies(); - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - get(name: string) { - return cookieStore.get(name)?.value; - }, - }, - } - ); - - // For admin operations (bypasses RLS) - Insert content + // Upsert based on original_url (schema: original_url TEXT UNIQUE NOT NULL). + // Uses the service-role client (bypasses RLS) to insert scraped content. const { createClient } = await import('@supabase/supabase-js'); const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, @@ -114,9 +87,12 @@ export async function GET(request: Request) { ); const successCount = results.filter((r) => r.status === 'fulfilled').length; - const errors = results - .filter((r) => r.status === 'rejected') - .map((r) => (r as PromiseRejectedResult).reason.message); + const errors: string[] = []; + for (const r of results) { + if (r.status === 'rejected') { + errors.push((r as PromiseRejectedResult).reason.message); + } + } return NextResponse.json({ success: true, diff --git a/app/api/seed-experiences/route.ts b/app/api/seed-experiences/route.ts index e1737dc..2bc559a 100644 --- a/app/api/seed-experiences/route.ts +++ b/app/api/seed-experiences/route.ts @@ -24,27 +24,32 @@ export async function POST(request: Request) { const results = { inserted: 0, skipped: 0, errors: [] as string[] }; - for (const exp of seedData) { - const { error } = await supabaseAdmin.from('scraped_experiences').upsert( - { - title: (exp as any).title, - original_url: (exp as any).original_url, - source: (exp as any).source, - author: (exp as any).author, - published_at: (exp as any).published_at || new Date().toISOString(), - tags: (exp as any).tags || [], - summary: (exp as any).summary || '', - formatted_content: (exp as any).formatted_content || '', - slug: (exp as any).slug || '', - metadata: {}, - status: 'approved', - ai_processed: true, - }, - { onConflict: 'original_url', ignoreDuplicates: false } - ); + const upsertResults = await Promise.all( + seedData.map((exp) => + supabaseAdmin.from('scraped_experiences').upsert( + { + title: (exp as any).title, + original_url: (exp as any).original_url, + source: (exp as any).source, + author: (exp as any).author, + published_at: (exp as any).published_at || new Date().toISOString(), + tags: (exp as any).tags || [], + summary: (exp as any).summary || '', + formatted_content: (exp as any).formatted_content || '', + slug: (exp as any).slug || '', + metadata: {}, + status: 'approved', + ai_processed: true, + }, + { onConflict: 'original_url', ignoreDuplicates: false } + ) + ) + ); + for (let i = 0; i < upsertResults.length; i++) { + const { error } = upsertResults[i]; if (error) { - results.errors.push(`${(exp as any).title}: ${error.message}`); + results.errors.push(`${(seedData[i] as any).title}: ${error.message}`); } else { results.inserted++; } diff --git a/app/api/summarize/route.ts b/app/api/summarize/route.ts index db00c3f..7bfa0cd 100644 --- a/app/api/summarize/route.ts +++ b/app/api/summarize/route.ts @@ -39,7 +39,7 @@ export async function POST(req: Request) { } // 1. Fetch the URL content - const response = await fetch(url); + const response = await fetch(url, { redirect: 'manual' }); const html = await response.text(); // 2. Simple parsing to get readable text diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 0a61126..5cec3dc 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,4 +1,5 @@ import { posts } from '#site/content'; +import { Suspense } from 'react'; import { PostItem } from '@/components/post-item'; import { QueryPagination } from '@/components/query-pagination'; import { Tag } from '@/components/tag'; @@ -81,10 +82,12 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {

Nothing to see here yet

)} - + + + ); } diff --git a/app/companies/[company]/page.tsx b/app/companies/[company]/page.tsx index ef50e7b..290de29 100644 --- a/app/companies/[company]/page.tsx +++ b/app/companies/[company]/page.tsx @@ -13,10 +13,7 @@ interface Props { } export async function generateMetadata({ params }: Props): Promise { - const { company } = await params; - - // Find real name to format nicely - const companies = await getCompanies(); + const [{ company }, companies] = await Promise.all([params, getCompanies()]); const matchedCompany = companies.find( (c) => c.slug === company.toLowerCase() ); @@ -47,14 +44,16 @@ export default async function CompanyHubPage({ params }: Props) { const { company } = await params; // Fetch the experiences for this specific company - const experiences = await getExperiencesByCompanySlug(company); + const [experiences, companies] = await Promise.all([ + getExperiencesByCompanySlug(company), + getCompanies(), + ]); if (!experiences || experiences.length === 0) { notFound(); } // Get nicely formatted company name - const companies = await getCompanies(); const matchedCompany = companies.find( (c) => c.slug === company.toLowerCase() ); @@ -100,12 +99,12 @@ export default async function CompanyHubPage({ params }: Props) {