Skip to content

Commit 55ce52f

Browse files
committed
feat(profile): migrate user data handling from Nocodb to Supabase
1 parent 49de183 commit 55ce52f

4 files changed

Lines changed: 127 additions & 19 deletions

File tree

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/components/ProfileSection.tsx

Lines changed: 20 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,26 @@ 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+
await addUserToSupabase(user, urlSearchParams);
4461

4562
if (!user) return <ProfileSkeleton />;
4663

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)