diff --git a/www/app/[username]/page.tsx b/www/app/[username]/page.tsx index 7997e0e7..d5b64de5 100644 --- a/www/app/[username]/page.tsx +++ b/www/app/[username]/page.tsx @@ -16,10 +16,13 @@ export const maxDuration = 60; export default async function Page({ params, + searchParams, }: { params: Promise<{ username: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const { username } = await params; + const urlSearchParams = await searchParams; if (!username) return null; @@ -40,7 +43,7 @@ export default async function Page({ }> - + diff --git a/www/app/page.tsx b/www/app/page.tsx index d8650ad9..a4024423 100644 --- a/www/app/page.tsx +++ b/www/app/page.tsx @@ -175,8 +175,8 @@ export default async function Home() {
diff --git a/www/components/ProfileSection.tsx b/www/components/ProfileSection.tsx index ab5bb892..6fbde454 100644 --- a/www/components/ProfileSection.tsx +++ b/www/components/ProfileSection.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; import { Github, Globe, Linkedin, Twitter, User, BookOpen, Instagram } from "lucide-react"; import { ProfileSkeleton } from "@/components/skeletons/profile-skeleton"; -import { addUserToNocodb, getUserProfile } from "@/lib/api"; +import { addUserToSupabase, getUserProfile } from "@/lib/api"; import ClientResumeButton from "@/components/ClientResumeButton"; import { Tooltip, @@ -38,9 +38,29 @@ const detectProvider = (url: string): string => { return 'generic'; }; -export async function ProfileSection({ username }: { username: string }) { +export async function ProfileSection({ + username, + searchParams +}: { + username: string; + searchParams?: { [key: string]: string | string[] | undefined }; +}) { const user = await getUserProfile(username); - await addUserToNocodb(user); + + // Convert search params to URLSearchParams for easier handling + const urlSearchParams = new URLSearchParams(); + if (searchParams) { + Object.entries(searchParams).forEach(([key, value]) => { + if (value && typeof value === 'string') { + urlSearchParams.set(key, value); + } + }); + } + + // Run Supabase call in background without blocking UI + addUserToSupabase(user, urlSearchParams).catch((error) => { + console.error('Background analytics call failed:', error); + }); if (!user) return ; diff --git a/www/components/github-modal/client.tsx b/www/components/github-modal/client.tsx index 1b6724bc..c2905794 100644 --- a/www/components/github-modal/client.tsx +++ b/www/components/github-modal/client.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import Image from "next/image"; -import { useRouter } from "next/navigation"; import { Github, Loader, X } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -40,15 +39,29 @@ export default function GitHubModal({ onClose }: GitHubModalProps) { const [isValidating, setIsValidating] = useState(false); const [error, setError] = useState(""); const [profile, setProfile] = useState(null); - const router = useRouter(); const [loading, setLoading] = useState(false); - const redirectToProfilePage = async () => { + const redirectToProfilePage = () => { if (!profile) return; setLoading(true); - await router.push(`/${profile?.login}?ref=modal`); + + // Get current search params and preserve them + const currentParams = new URLSearchParams(window.location.search); + currentParams.set('ref', 'modal'); + + // Use window.location for instant navigation + window.location.href = `/${profile?.login}?${currentParams.toString()}`; + }; - // no need to setLoading(false) because navigation will replace this page + const redirectToProfilePageFromCard = () => { + if (!profile) return; + + // Get current search params and preserve them + const currentParams = new URLSearchParams(window.location.search); + currentParams.set('ref', 'modelv2'); + + // Use window.location for instant navigation + window.location.href = `/${profile?.login}?${currentParams.toString()}`; }; // Debounce the username input to prevent excessive API calls const debouncedUsername = useDebounce(username, 500); @@ -152,7 +165,8 @@ export default function GitHubModal({ onClose }: GitHubModalProps) { @{username}

{bio}

- + { + const urlLower = url.toLowerCase(); + if (urlLower.includes('medium.com')) return 'medium'; + if (urlLower.includes('instagram.com')) return 'instagram'; + if (urlLower.includes('huggingface.co')) return 'huggingface'; + return 'generic'; +}; + /** * Fetch resource with Next.js caching */ @@ -228,29 +237,108 @@ export const getLinkedInProfileData = async ( }; /** - * API to add user to Nocodb table for analytics + * API to add user to Supabase via edge function for analytics */ -export const addUserToNocodb = async (user: Profile | null) => { +export const addUserToSupabase = async (user: Profile | null, searchParams?: URLSearchParams) => { if (!user) return; - const url = `https://app.nocodb.com/api/v2/tables/${process.env.NOCODB_TABLE_ID}/records`; - const headers = { - accept: "application/json", - "xc-token": process.env.NOCODB_API_KEY || "", - "Content-Type": "application/json", - }; + + const supabaseUrl = process.env.SUPABASE_URL; + const supabaseAnonKey = process.env.SUPABASE_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + console.error("Supabase configuration missing"); + return; + } - const data = { + const url = `${supabaseUrl}/functions/v1/devb-io`; + + // Map user data to match Supabase function whitelist + const mappedData: Record = { name: user.username, - socials: user.social_accounts, + "full name": user.name, + "devb profile": `https://devb.io/${user.username}`, + github: `https://github.com/${user.username}`, + }; + + // Add query parameters if available + if (searchParams) { + // UTM parameters + const utmSource = searchParams.get('utm_source'); + const utmMedium = searchParams.get('utm_medium'); + const utmCampaign = searchParams.get('utm_campaign'); + const utmTerm = searchParams.get('utm_term'); + const utmContent = searchParams.get('utm_content'); + + // Referral parameter + const ref = searchParams.get('ref'); + + // Add to mapped data if they exist + if (utmSource) mappedData['utm_source'] = utmSource; + if (utmMedium) mappedData['utm_medium'] = utmMedium; + if (utmCampaign) mappedData['utm_campaign'] = utmCampaign; + if (utmTerm) mappedData['utm_term'] = utmTerm; + if (utmContent) mappedData['utm_content'] = utmContent; + if (ref) mappedData['ref'] = ref; + } + + // Counter for generic URLs + let genericCounter = 1; + + // Add social accounts based on provider + user.social_accounts?.forEach((account) => { + const provider = account.provider.toLowerCase(); + + // If provider is generic, detect the actual platform + const actualProvider = provider === "generic" ? detectProvider(account.url) : provider; + + switch (actualProvider) { + case "linkedin": + mappedData["Linkedin"] = account.url; + break; + case "twitter": + mappedData["twitter"] = account.url; + break; + case "medium": + mappedData["Medium"] = account.url; + break; + case "instagram": + mappedData["instagram"] = account.url; + break; + case "huggingface": + // Could add huggingface to whitelist if needed + break; + case "generic": + // Check if it's a devb.io link + if (account.url.includes("devb.io")) { + mappedData["devb"] = account.url; + } else { + // For other generic URLs, number them + mappedData[`generic ${genericCounter}`] = account.url; + genericCounter++; + } + break; + } + }); + + const headers = { + "Content-Type": "application/json", + "Authorization": `Bearer ${supabaseAnonKey}`, }; try { - await fetch(url, { + const response = await fetch(url, { method: "POST", headers: headers, - body: JSON.stringify(data), + body: JSON.stringify(mappedData), }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log("User data sent to Supabase:", result); } catch (error) { - console.error("Error:", error); + console.error("Error sending data to Supabase:", error); } };