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
6 changes: 3 additions & 3 deletions app/api/activities/live/route.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { decodeActivityCursor, encodeActivityCursor, listActivities } from '../../../../lib/activity-store.js';
import { createFallbackActivities } from '../../../../lib/platform-activity.js';
import { createFallbackActivities, normalizePlatformActivity } from '../../../../lib/platform-activity.js';

export async function GET(request) {
const { searchParams } = new URL(request.url);
Expand All @@ -17,9 +17,9 @@ export async function GET(request) {

try {
const result = await listActivities({ limit, cursor, after });
const activities = !cursor && !after && result.activities.length === 0
const activities = (!cursor && !after && result.activities.length === 0
? createFallbackActivities().slice(0, limit)
: result.activities;
: result.activities).map(normalizePlatformActivity);
return NextResponse.json({
...result,
activities,
Expand Down
3 changes: 2 additions & 1 deletion app/api/activities/platform/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { saveActivities } from '../../../../lib/activity-store.js';
import { createPlatformActivity } from '../../../../lib/platform-activity.js';

const LOGIN_PATTERN = /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i;
const ACTIVITY_TYPES = new Set(['generated_card', 'generated_readme']);

export async function POST(request) {
try {
const body = await request.json();
if (body.type !== 'generated_card' || !LOGIN_PATTERN.test(body.targetLogin || '')) {
if (!ACTIVITY_TYPES.has(body.type) || !LOGIN_PATTERN.test(body.targetLogin || '')) {
return NextResponse.json({ error: 'Invalid platform activity' }, { status: 400 });
}

Expand Down
18 changes: 10 additions & 8 deletions app/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,25 +320,25 @@ export default function Home() {
setFiltered(rankedResults);
}, [developers]);

const recordCardActivity = useCallback((targetLogin) => {
const recordPlatformActivity = useCallback((type, targetLogin) => {
fetch('/api/activities/platform', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'generated_card', targetLogin }),
body: JSON.stringify({ type, targetLogin }),
}).catch(() => {});
}, []);

const handleGenerateCard = useCallback((developer) => {
const rankedDeveloper = developers.find(item => item.login === developer.login) || developer;
recordCardActivity(rankedDeveloper.login);
recordPlatformActivity('generated_card', rankedDeveloper.login);
setSelectedDev(rankedDeveloper);
setCardContext('generate');
setCardRequest(request => request + 1);
setSidebarOpen(false);
if (rankedDeveloper.lat != null && rankedDeveloper.lng != null) {
setFlyTarget({ lat: rankedDeveloper.lat, lng: rankedDeveloper.lng });
}
}, [developers, recordCardActivity]);
}, [developers, recordPlatformActivity]);

const handleOpenCardFeature = useCallback(() => {
const developer = resolveIdentityCardDeveloper(selectedDev, user, developers);
Expand All @@ -350,20 +350,21 @@ export default function Home() {
requestAnimationFrame(() => document.querySelector('#search-bar input')?.focus());
}, [developers, handleGenerateCard, selectedDev, user]);

const handleOpenReadmeFeature = useCallback((requestedDeveloper) => {
const handleOpenReadmeFeature = useCallback((requestedDeveloper, { recordActivity = true } = {}) => {
const developer = requestedDeveloper || selectedDev || resolveIdentityCardDeveloper(null, user, developers);
if (!developer) {
setTourStep('search');
requestAnimationFrame(() => document.querySelector('#search-bar input')?.focus());
return;
}
track('profile_readme_opened', { login: developer.login, source: 'home' });
if (recordActivity) recordPlatformActivity('generated_readme', developer.login);
setCardRequest(0);
setCardContext(null);
setSelectedDev(developer);
setSidebarOpen(false);
setReadmeRequest(request => request + 1);
}, [developers, selectedDev, user]);
}, [developers, recordPlatformActivity, selectedDev, user]);

// Resume a "Generate README" request started from the home menu before sign-in.
useEffect(() => {
Expand All @@ -373,7 +374,7 @@ export default function Home() {
if (!pendingLogin) return;
try { localStorage.removeItem(PENDING_HOME_README_KEY); } catch { /* best-effort cleanup */ }
const developer = developers.find(candidate => candidate.login.toLowerCase() === pendingLogin.toLowerCase());
if (developer) handleOpenReadmeFeature(developer);
if (developer) handleOpenReadmeFeature(developer, { recordActivity: false });
}, [user, developers, handleOpenReadmeFeature]);

const handleOpenCompareFeature = useCallback(() => {
Expand Down Expand Up @@ -617,7 +618,8 @@ export default function Home() {
key={`${selectedDev.login}-${cardRequest}`}
dev={selectedDev}
onClose={handleCloseDetail}
onCardGenerated={recordCardActivity}
onCardGenerated={targetLogin => recordPlatformActivity('generated_card', targetLogin)}
onReadmeGenerated={targetLogin => recordPlatformActivity('generated_readme', targetLogin)}
claimedLogins={claimedLogins}
user={user}
onClaim={handleClaim}
Expand Down
8 changes: 2 additions & 6 deletions app/share/[login]/page.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Link from 'next/link';
import { headers } from 'next/headers';
import { getSiteUrl, SOCIAL_PREVIEW_VERSION } from '../../../lib/site.js';

export const revalidate = 86400;

export async function generateMetadata({ params }) {
const { login } = await params;
const siteUrl = getSiteUrl();
Expand Down Expand Up @@ -34,11 +35,6 @@ export async function generateMetadata({ params }) {

export default async function DeveloperSharePage({ params }) {
const { login } = await params;
const requestHeaders = await headers();
console.info('social-preview-page', {
login,
userAgent: requestHeaders.get('user-agent') || 'unknown',
});
const encodedLogin = encodeURIComponent(login);
const siteUrl = getSiteUrl();
const pageUrl = `${siteUrl}/share/${encodedLogin}`;
Expand Down
44 changes: 26 additions & 18 deletions components/DetailPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { track } from '../lib/analytics.js';
import * as d3 from 'd3';
import { formatNum, formatRelativeTime, formatUsd, isStaleData } from '../lib/format.js';
import { DIMENSIONS, SCORE_METHODOLOGY } from '../lib/scoring.js';
import { IDENTITY_CARD_VERSION, SOCIAL_PREVIEW_VERSION } from '../lib/site.js';
import { getSiteUrl, IDENTITY_CARD_VERSION, SOCIAL_PREVIEW_VERSION } from '../lib/site.js';
import { classifyAgent } from '../lib/agent-class.js';
import { AI_TOOLS } from '../lib/ai-profile.js';
import { publicApiUrl } from '../lib/public-api.js';
import { resolveReadmeAccess } from '../lib/profile-readme.js';
import SpecialTags from './SpecialTags.jsx';
import ReadmeGeneratorModal from './ReadmeGeneratorModal.jsx';

export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogins, user, onClaim, readmeRequest = 0, openCardOnMount = false, claimSuccess = false }) {
export default function DetailPanel({ dev, onClose, onCardGenerated, onReadmeGenerated, claimedLogins, user, onClaim, readmeRequest = 0, openCardOnMount = false, claimSuccess = false }) {
const [fullData, setFullData] = useState(null);
const [showCard, setShowCard] = useState(false);
const [showReadmeGenerator, setShowReadmeGenerator] = useState(false);
Expand Down Expand Up @@ -108,6 +108,7 @@ export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogi
const handleReadmeAccess = () => {
setShowReadmeGenerator(true);
track('profile_readme_opened', { login: dev.login, access: readmeAccess });
onReadmeGenerated?.(dev.login);
};

// Radar chart
Expand Down Expand Up @@ -727,25 +728,26 @@ function renderLanguages(container, languages) {
function CardModal({ dev, claimSuccess, onClose }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [linkedinCopied, setLinkedinCopied] = useState(false);
const [captionCopiedFor, setCaptionCopiedFor] = useState('');
const { login } = dev;
const name = dev.name || login;
const cardUrl = `/api/card?login=${encodeURIComponent(login)}&v=${IDENTITY_CARD_VERSION}`;
const sharePath = `/share/${encodeURIComponent(login)}?v=${SOCIAL_PREVIEW_VERSION}`;
const shareUrl = typeof window !== 'undefined' ? `${window.location.origin}${sharePath}` : sharePath;
const shareUrl = `${getSiteUrl()}${sharePath}`;

const rankText = dev.globalRank ? `Global #${dev.globalRank} of ${dev.globalTotal}` : 'Ranked on DevGlobe';
const agent = classifyAgent(dev);
const shareHashtags = ['buildinpublic', 'DevGlobe', 'OpenSource', 'DeveloperCommunity', 'GitHub'];
const shareHashtags = ['DevGlobe', 'OpenSource', 'DeveloperDiscovery'];
const hashtagText = shareHashtags.map(hashtag => `#${hashtag}`).join(' ');
const shareText = `I mapped my open-source identity on DevGlobe: ${rankText}. Generate yours and see where you rank.\n\n${hashtagText}`;
const linkedinCaption = `I mapped my open-source contributions on DevGlobe and discovered my developer identity: ${agent.name}. ${rankText}. Build your card and see where your work places you in the global developer community.\n\n${hashtagText}`;
const twitterCaption = `Open-source work should make developers discoverable.\n\nMy DevGlobe identity uses contribution signals to help people and AI agents find my work.\n\n${agent.name} · ${rankText}\n\n${hashtagText}`;
const linkedinCaption = `Open-source work should be discoverable by what you build.\n\nDevGlobe mapped my public contribution signals into an open-source developer identity designed to help engineering teams, communities, and AI agents find relevant expertise.\n\n${agent.name} · ${rankText}\n\nExplore my profile and create yours.\n\n${hashtagText}`;
const facebookCaption = `Open-source work tells a richer story than a job title.\n\nDevGlobe mapped my public contributions into a developer identity that helps teams, communities, and AI agents discover what I build and where I can contribute.\n\n${agent.name} · ${rankText}\n\nExplore my profile and create yours.\n\n${hashtagText}`;

const shareLinks = {
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`,
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(twitterCaption)}&url=${encodeURIComponent(shareUrl)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`,
reddit: `https://reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(`My DevGlobe Developer Card - ${name} ${hashtagText}`)}`,
reddit: `https://reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(`${name}'s open-source developer identity on DevGlobe - ${agent.name}, ${rankText}`)}`,
};

const handleDownload = async () => {
Expand Down Expand Up @@ -775,12 +777,15 @@ function CardModal({ dev, claimSuccess, onClose }) {
};

const handleLinkedInShare = () => {
navigator.clipboard?.writeText(linkedinCaption).then(() => setLinkedinCopied(true)).catch(() => {});
navigator.clipboard?.writeText(linkedinCaption).then(() => setCaptionCopiedFor('LinkedIn')).catch(() => {});
track('identity_card_shared', { login, channel: 'linkedin' });
window.open(shareLinks.linkedin, '_blank', 'noopener,noreferrer');
};

const handleSocialShare = channel => {
if (channel === 'facebook') {
navigator.clipboard?.writeText(facebookCaption).then(() => setCaptionCopiedFor('Facebook')).catch(() => {});
}
track('identity_card_shared', { login, channel });
};

Expand Down Expand Up @@ -849,28 +854,31 @@ function CardModal({ dev, claimSuccess, onClose }) {
</div>

<div className="card-modal__share">
<span className="card-modal__share-label">Share on:</span>
<div className="card-modal__share-heading">
<strong>Share your developer card</strong>
<span>Show your open-source identity to your network</span>
</div>
<button type="button" onClick={handleLinkedInShare} className="card-modal__social card-modal__social--linkedin" title="Copy caption and share on LinkedIn" aria-label="Copy caption and share on LinkedIn">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</button>
<a href={shareLinks.twitter} target="_blank" rel="noreferrer" className="card-modal__social card-modal__social--twitter" title="Share on X/Twitter" onClick={() => handleSocialShare('twitter')}>
<svg viewBox="0 0 16 16" width="18" height="18" fill="currentColor">
<path d="M13.5 1h-3.7L8 3.6 6.2 1H2.5L6.6 6.5 2.3 13h1.7l2.5-3.2L9 13h4.2l-4.5-6.7L13.5 1zm-1.1 11h-1L4.5 2h1l6.9 10z" />
</svg>
</a>
<a href={shareLinks.facebook} target="_blank" rel="noreferrer" className="card-modal__social card-modal__social--facebook" title="Share on Facebook" aria-label="Share on Facebook" onClick={() => handleSocialShare('facebook')}>
<a href={shareLinks.facebook} target="_blank" rel="noreferrer" className="card-modal__social card-modal__social--facebook" title="Copy caption and share on Facebook" aria-label="Copy caption and share on Facebook" onClick={() => handleSocialShare('facebook')}>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
<path d="M13.5 22v-9h3l.5-3.5h-3.5V7.25c0-1 .3-1.75 1.75-1.75H17V2.38A23.7 23.7 0 0014.44 2C11.9 2 10 3.55 10 6.4v3.1H7V13h3v9h3.5z" />
</svg>
</a>
<button type="button" onClick={handleLinkedInShare} className="card-modal__social card-modal__social--linkedin" title="Copy caption and share on LinkedIn" aria-label="Copy caption and share on LinkedIn">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</button>
<a href={shareLinks.reddit} target="_blank" rel="noreferrer" className="card-modal__social card-modal__social--reddit" title="Share on Reddit" onClick={() => handleSocialShare('reddit')}>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M12 0A12 12 0 000 12a12 12 0 0012 12 12 12 0 0012-12A12 12 0 0012 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 01-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 01.042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 014.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 01.14-.197.35.35 0 01.238-.042l2.906.617a1.214 1.214 0 011.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 00-.231.094.33.33 0 000 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 000-.463.327.327 0 00-.462 0c-.545.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 00-.205-.094z" />
</svg>
</a>
{linkedinCopied && <span className="card-modal__share-status" role="status">Caption and tags copied. Paste them into your LinkedIn post.</span>}
{captionCopiedFor && <span className="card-modal__share-status" role="status">Caption and tags copied. Paste them into your {captionCopiedFor} post.</span>}
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default function Header({ onHome, theme, onToggleTheme, user, onLogout, o
<div className="header__brand" onClick={onHome} style={{ cursor: 'pointer' }}>
<img src="/devglobe.png" alt="DevGlobe" className="header__logo" />
<h1 className="header__title">DevGlobe</h1>
<span className="header__subtitle">The discovery layer for the agentic web: connecting developers and AI agents to build together.</span>
</div>
<div className="header__actions">
<button type="button" onClick={onStartTour} className="btn btn--tour" aria-label="Start quick tour" title="Quick tour">
Expand Down
35 changes: 27 additions & 8 deletions lib/platform-activity.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
import { randomUUID } from 'crypto';

const FALLBACK_ACTIVITIES = [
{ login: 'codeatlas', type: 'generated_card', description: 'had their developer card generated' },
{ login: 'codeatlas', type: 'generated_card', description: 'revealed their open-source identity with a DevGlobe card - create yours' },
{ login: 'pixelbranch', type: 'logged_in', description: 'signed in to explore the developer globe' },
{ login: 'cloudsyntax', type: 'generated_card', description: 'had their developer card generated' },
{ login: 'readmeforge', type: 'generated_readme', description: 'generated their GitHub profile README' },
{ login: 'cloudsyntax', type: 'generated_card', description: 'revealed their open-source identity with a DevGlobe card - create yours' },
{ login: 'commitcraft', type: 'logged_in', description: 'signed in to explore the developer globe' },
];

const SELF_CARD_DESCRIPTION = 'revealed their open-source identity with a DevGlobe card - create yours';

export function normalizePlatformActivity(activity) {
if (activity.type !== 'generated_card') return activity;
if (activity.description === 'had their developer card generated') {
return { ...activity, description: SELF_CARD_DESCRIPTION };
}
const targetMatch = activity.description?.match(/^generated @(.+)'s developer card$/);
return targetMatch
? { ...activity, description: `revealed @${targetMatch[1]}'s open-source identity with a DevGlobe card - try it yourself` }
: activity;
}

export function createPlatformActivity({ type, login, avatarUrl, targetLogin, now = new Date() }) {
const isCard = type === 'generated_card';
const isReadme = type === 'generated_readme';
const target = targetLogin || login;
const description = isCard
? target !== login
? `revealed @${target}'s open-source identity with a DevGlobe card - try it yourself`
: SELF_CARD_DESCRIPTION
: isReadme
? target !== login ? `generated @${target}'s GitHub profile README` : 'generated their GitHub profile README'
: 'signed in to DevGlobe';
return {
id: `platform:${randomUUID()}`,
login,
avatarUrl: avatarUrl || null,
type,
description: isCard && targetLogin && targetLogin !== login
? `generated @${targetLogin}'s developer card`
: isCard
? 'had their developer card generated'
: 'signed in to DevGlobe',
description,
repo: null,
url: isCard ? `/developer/${encodeURIComponent(targetLogin || login)}` : `/developer/${encodeURIComponent(login)}`,
url: isCard || isReadme ? `/developer/${encodeURIComponent(target)}` : `/developer/${encodeURIComponent(login)}`,
createdAt: now.toISOString(),
documentType: 'platform-activity',
};
Expand Down
2 changes: 1 addition & 1 deletion lib/site.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const DEFAULT_SITE_URL = 'https://www.devglobe.dev';
export const SOCIAL_PREVIEW_VERSION = '11';
export const SOCIAL_PREVIEW_VERSION = '12';
export const IDENTITY_CARD_VERSION = '5';

export function getSiteUrl() {
Expand Down
Loading