Skip to content

Commit 8e7b870

Browse files
authored
Merge pull request #112 from sunithvs/home-updates
Analytics Integration & Modal Optimization
2 parents f5e900f + e110184 commit 8e7b870

File tree

7 files changed

+153
-28
lines changed

7 files changed

+153
-28
lines changed

www/app/[username]/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ export const maxDuration = 60;
1616

1717
export default async function Page({
1818
params,
19+
searchParams,
1920
}: {
2021
params: Promise<{ username: string }>;
22+
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
2123
}) {
2224
const { username } = await params;
25+
const urlSearchParams = await searchParams;
2326

2427
if (!username) return null;
2528

@@ -40,7 +43,7 @@ export default async function Page({
4043
</Link>
4144

4245
<Suspense fallback={<ProfileSkeleton />}>
43-
<ProfileSection username={username} />
46+
<ProfileSection username={username} searchParams={urlSearchParams} />
4447
</Suspense>
4548
</div>
4649

www/app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ export default async function Home() {
175175
<div className="container mx-auto px-4">
176176
<div className="max-w-3xl mx-auto">
177177
<AnimatedStats
178-
value={2050}
179-
subtitle="Profiles Generated in 2 Months from Around the Globe"
178+
value={6010}
179+
subtitle="Profiles Generated in 8 Months from Around the Globe"
180180
/>
181181
</div>
182182
</div>

www/components/ProfileSection.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Image from "next/image";
22
import { Github, Globe, Linkedin, Twitter, User, BookOpen, Instagram } from "lucide-react";
33
import { ProfileSkeleton } from "@/components/skeletons/profile-skeleton";
4-
import { addUserToNocodb, getUserProfile } from "@/lib/api";
4+
import { addUserToSupabase, getUserProfile } from "@/lib/api";
55
import ClientResumeButton from "@/components/ClientResumeButton";
66
import {
77
Tooltip,
@@ -38,9 +38,29 @@ const detectProvider = (url: string): string => {
3838
return 'generic';
3939
};
4040

41-
export async function ProfileSection({ username }: { username: string }) {
41+
export async function ProfileSection({
42+
username,
43+
searchParams
44+
}: {
45+
username: string;
46+
searchParams?: { [key: string]: string | string[] | undefined };
47+
}) {
4248
const user = await getUserProfile(username);
43-
await addUserToNocodb(user);
49+
50+
// Convert search params to URLSearchParams for easier handling
51+
const urlSearchParams = new URLSearchParams();
52+
if (searchParams) {
53+
Object.entries(searchParams).forEach(([key, value]) => {
54+
if (value && typeof value === 'string') {
55+
urlSearchParams.set(key, value);
56+
}
57+
});
58+
}
59+
60+
// Run Supabase call in background without blocking UI
61+
addUserToSupabase(user, urlSearchParams).catch((error) => {
62+
console.error('Background analytics call failed:', error);
63+
});
4464

4565
if (!user) return <ProfileSkeleton />;
4666

www/components/github-modal/client.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useCallback, useEffect, useState } from "react";
44
import { AnimatePresence, motion } from "framer-motion";
55
import Image from "next/image";
6-
import { useRouter } from "next/navigation";
76
import { Github, Loader, X } from "lucide-react";
87
import { cn } from "@/lib/utils";
98

@@ -40,15 +39,29 @@ export default function GitHubModal({ onClose }: GitHubModalProps) {
4039
const [isValidating, setIsValidating] = useState(false);
4140
const [error, setError] = useState("");
4241
const [profile, setProfile] = useState<GitHubProfile | null>(null);
43-
const router = useRouter();
4442
const [loading, setLoading] = useState(false);
4543

46-
const redirectToProfilePage = async () => {
44+
const redirectToProfilePage = () => {
4745
if (!profile) return;
4846
setLoading(true);
49-
await router.push(`/${profile?.login}?ref=modal`);
47+
48+
// Get current search params and preserve them
49+
const currentParams = new URLSearchParams(window.location.search);
50+
currentParams.set('ref', 'modal');
51+
52+
// Use window.location for instant navigation
53+
window.location.href = `/${profile?.login}?${currentParams.toString()}`;
54+
};
5055

51-
// no need to setLoading(false) because navigation will replace this page
56+
const redirectToProfilePageFromCard = () => {
57+
if (!profile) return;
58+
59+
// Get current search params and preserve them
60+
const currentParams = new URLSearchParams(window.location.search);
61+
currentParams.set('ref', 'modelv2');
62+
63+
// Use window.location for instant navigation
64+
window.location.href = `/${profile?.login}?${currentParams.toString()}`;
5265
};
5366
// Debounce the username input to prevent excessive API calls
5467
const debouncedUsername = useDebounce(username, 500);
@@ -152,7 +165,8 @@ export default function GitHubModal({ onClose }: GitHubModalProps) {
152165
<motion.div
153166
initial={{ opacity: 0, y: 10 }}
154167
animate={{ opacity: 1, y: 0 }}
155-
className="bg-gray-50 rounded-lg p-4 flex items-center gap-4"
168+
onClick={redirectToProfilePageFromCard}
169+
className="bg-gray-50 rounded-lg p-4 flex items-center gap-4 cursor-pointer hover:bg-gray-100 transition-colors"
156170
>
157171
<Image
158172
src={profile.avatar_url}

www/components/profile-card/client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default function ProfileCardClient({
4141
<p className="text-gray-600 text-sm mb-2 truncate">@{username}</p>
4242
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{bio}</p>
4343

44-
<a href={`/${username}`} target="_blank" rel="noopener noreferrer">
44+
<a href={`/${username}?utm_source=devb_io&utm_medium=contributor&utm_campaign=devb_io`} target="_blank" rel="noopener noreferrer">
4545
<motion.button
4646
whileHover={{ scale: 1.05 }}
4747
whileTap={{ scale: 0.95 }}

www/example.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ NEXT_PUBLIC_X_API_KEY=
44
NEXT_PUBLIC_CLARITY_ID=
55
NEXT_PUBLIC_GA_MEASUREMENT_ID=
66

7-
NOCODB_TABLE_ID=
8-
NOCODB_API_KEY=
7+
SUPABASE_URL=
8+
SUPABASE_KEY=

www/lib/api.ts

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ import { parseStringPromise } from "xml2js";
99
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
1010
const API_KEY = process.env.NEXT_PUBLIC_X_API_KEY;
1111

12+
// Utility function to detect provider from URL
13+
const detectProvider = (url: string): string => {
14+
const urlLower = url.toLowerCase();
15+
if (urlLower.includes('medium.com')) return 'medium';
16+
if (urlLower.includes('instagram.com')) return 'instagram';
17+
if (urlLower.includes('huggingface.co')) return 'huggingface';
18+
return 'generic';
19+
};
20+
1221
/**
1322
* Fetch resource with Next.js caching
1423
*/
@@ -228,29 +237,108 @@ export const getLinkedInProfileData = async (
228237
};
229238

230239
/**
231-
* API to add user to Nocodb table for analytics
240+
* API to add user to Supabase via edge function for analytics
232241
*/
233-
export const addUserToNocodb = async (user: Profile | null) => {
242+
export const addUserToSupabase = async (user: Profile | null, searchParams?: URLSearchParams) => {
234243
if (!user) return;
235-
const url = `https://app.nocodb.com/api/v2/tables/${process.env.NOCODB_TABLE_ID}/records`;
236-
const headers = {
237-
accept: "application/json",
238-
"xc-token": process.env.NOCODB_API_KEY || "",
239-
"Content-Type": "application/json",
240-
};
244+
245+
const supabaseUrl = process.env.SUPABASE_URL;
246+
const supabaseAnonKey = process.env.SUPABASE_KEY;
247+
248+
if (!supabaseUrl || !supabaseAnonKey) {
249+
console.error("Supabase configuration missing");
250+
return;
251+
}
241252

242-
const data = {
253+
const url = `${supabaseUrl}/functions/v1/devb-io`;
254+
255+
// Map user data to match Supabase function whitelist
256+
const mappedData: Record<string, string> = {
243257
name: user.username,
244-
socials: user.social_accounts,
258+
"full name": user.name,
259+
"devb profile": `https://devb.io/${user.username}`,
260+
github: `https://github.com/${user.username}`,
261+
};
262+
263+
// Add query parameters if available
264+
if (searchParams) {
265+
// UTM parameters
266+
const utmSource = searchParams.get('utm_source');
267+
const utmMedium = searchParams.get('utm_medium');
268+
const utmCampaign = searchParams.get('utm_campaign');
269+
const utmTerm = searchParams.get('utm_term');
270+
const utmContent = searchParams.get('utm_content');
271+
272+
// Referral parameter
273+
const ref = searchParams.get('ref');
274+
275+
// Add to mapped data if they exist
276+
if (utmSource) mappedData['utm_source'] = utmSource;
277+
if (utmMedium) mappedData['utm_medium'] = utmMedium;
278+
if (utmCampaign) mappedData['utm_campaign'] = utmCampaign;
279+
if (utmTerm) mappedData['utm_term'] = utmTerm;
280+
if (utmContent) mappedData['utm_content'] = utmContent;
281+
if (ref) mappedData['ref'] = ref;
282+
}
283+
284+
// Counter for generic URLs
285+
let genericCounter = 1;
286+
287+
// Add social accounts based on provider
288+
user.social_accounts?.forEach((account) => {
289+
const provider = account.provider.toLowerCase();
290+
291+
// If provider is generic, detect the actual platform
292+
const actualProvider = provider === "generic" ? detectProvider(account.url) : provider;
293+
294+
switch (actualProvider) {
295+
case "linkedin":
296+
mappedData["Linkedin"] = account.url;
297+
break;
298+
case "twitter":
299+
mappedData["twitter"] = account.url;
300+
break;
301+
case "medium":
302+
mappedData["Medium"] = account.url;
303+
break;
304+
case "instagram":
305+
mappedData["instagram"] = account.url;
306+
break;
307+
case "huggingface":
308+
// Could add huggingface to whitelist if needed
309+
break;
310+
case "generic":
311+
// Check if it's a devb.io link
312+
if (account.url.includes("devb.io")) {
313+
mappedData["devb"] = account.url;
314+
} else {
315+
// For other generic URLs, number them
316+
mappedData[`generic ${genericCounter}`] = account.url;
317+
genericCounter++;
318+
}
319+
break;
320+
}
321+
});
322+
323+
const headers = {
324+
"Content-Type": "application/json",
325+
"Authorization": `Bearer ${supabaseAnonKey}`,
245326
};
246327

247328
try {
248-
await fetch(url, {
329+
const response = await fetch(url, {
249330
method: "POST",
250331
headers: headers,
251-
body: JSON.stringify(data),
332+
body: JSON.stringify(mappedData),
252333
});
334+
335+
if (!response.ok) {
336+
throw new Error(`HTTP error! status: ${response.status}`);
337+
}
338+
339+
const result = await response.json();
340+
console.log("User data sent to Supabase:", result);
253341
} catch (error) {
254-
console.error("Error:", error);
342+
console.error("Error sending data to Supabase:", error);
255343
}
256344
};

0 commit comments

Comments
 (0)