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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion www/app/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

<Suspense fallback={<ProfileSkeleton />}>
<ProfileSection username={username} />
<ProfileSection username={username} searchParams={urlSearchParams} />
</Suspense>
</div>

Expand Down
4 changes: 2 additions & 2 deletions www/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ export default async function Home() {
<div className="container mx-auto px-4">
<div className="max-w-3xl mx-auto">
<AnimatedStats
value={2050}
subtitle="Profiles Generated in 2 Months from Around the Globe"
value={6010}
subtitle="Profiles Generated in 8 Months from Around the Globe"
/>
</div>
</div>
Expand Down
26 changes: 23 additions & 3 deletions www/components/ProfileSection.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The value && check filters out empty strings (e.g., from ?ref=), which might be unintentional as empty strings can be valid parameter values. The typeof value === 'string' check already handles null and undefined. Consider removing value && to allow tracking of parameters with empty values.

Suggested change
if (value && typeof value === 'string') {
if (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 <ProfileSkeleton />;

Expand Down
26 changes: 20 additions & 6 deletions www/components/github-modal/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -40,15 +39,29 @@ export default function GitHubModal({ onClose }: GitHubModalProps) {
const [isValidating, setIsValidating] = useState(false);
const [error, setError] = useState("");
const [profile, setProfile] = useState<GitHubProfile | null>(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);
Comment on lines 46 to 67

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's code duplication between redirectToProfilePage and redirectToProfilePageFromCard. You can extract the common navigation logic into a helper function to improve maintainability and reduce redundancy.

  const navigateToProfile = (ref: 'modal' | 'modelv2') => {
    if (!profile) return;

    // Get current search params and preserve them
    const currentParams = new URLSearchParams(window.location.search);
    currentParams.set('ref', ref);

    // Use window.location for instant navigation
    window.location.href = `/${profile.login}?${currentParams.toString()}`;
  };

  const redirectToProfilePage = () => {
    setLoading(true);
    navigateToProfile('modal');
  };

  const redirectToProfilePageFromCard = () => {
    navigateToProfile('modelv2');
  };

Expand Down Expand Up @@ -152,7 +165,8 @@ export default function GitHubModal({ onClose }: GitHubModalProps) {
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-50 rounded-lg p-4 flex items-center gap-4"
onClick={redirectToProfilePageFromCard}
className="bg-gray-50 rounded-lg p-4 flex items-center gap-4 cursor-pointer hover:bg-gray-100 transition-colors"
>
<Image
src={profile.avatar_url}
Expand Down
2 changes: 1 addition & 1 deletion www/components/profile-card/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function ProfileCardClient({
<p className="text-gray-600 text-sm mb-2 truncate">@{username}</p>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{bio}</p>

<a href={`/${username}`} target="_blank" rel="noopener noreferrer">
<a href={`/${username}?utm_source=devb_io&utm_medium=contributor&utm_campaign=devb_io`} target="_blank" rel="noopener noreferrer">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
Expand Down
4 changes: 2 additions & 2 deletions www/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ NEXT_PUBLIC_X_API_KEY=
NEXT_PUBLIC_CLARITY_ID=
NEXT_PUBLIC_GA_MEASUREMENT_ID=

NOCODB_TABLE_ID=
NOCODB_API_KEY=
SUPABASE_URL=
SUPABASE_KEY=
114 changes: 101 additions & 13 deletions www/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { parseStringPromise } from "xml2js";
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.NEXT_PUBLIC_X_API_KEY;

// Utility function to detect provider from URL
const detectProvider = (url: string): string => {
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';
};
Comment on lines +12 to +19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This detectProvider function is duplicated in www/components/ProfileSection.tsx. To avoid code duplication, it should be centralized in a shared location and imported where needed.

While you're at it, you could improve this function by adding detection for devb.io URLs. This would allow you to simplify the switch statement in addUserToSupabase by creating a separate case "devb": instead of nesting the logic inside the generic case.

Example update for detectProvider:

if (urlLower.includes('devb.io')) return 'devb';


/**
* Fetch resource with Next.js caching
*/
Expand Down Expand Up @@ -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<string, string> = {
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;
Comment on lines +265 to +281

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for handling UTM parameters and ref is repetitive. You can use a loop to make it more concise and easier to maintain if more parameters are added in the future.

Suggested change
// 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;
const paramsToTrack = [
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'ref',
];
paramsToTrack.forEach((param) => {
const value = searchParams.get(param);
if (value) {
mappedData[param] = value;
}
});

}

// 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);
}
};
Loading