diff --git a/app/api/auth/claim/route.js b/app/api/auth/claim/route.js index f3dc4d6..ecf73fd 100644 --- a/app/api/auth/claim/route.js +++ b/app/api/auth/claim/route.js @@ -3,8 +3,9 @@ import { NextResponse } from 'next/server'; import { getSession } from '../../../../lib/auth.js'; import { promises as fs } from 'fs'; import path from 'path'; -import { buildClaimWelcomeEmail, sendLifecycleEmail } from '../../../../lib/lifecycle-email.js'; -import { saveDeveloperContact } from '../../../../lib/developer-contact-store.js'; +import { buildClaimApprovedEmail, buildClaimWelcomeEmail, sendLifecycleEmail } from '../../../../lib/lifecycle-email.js'; +import { getDeveloperContact, saveDeveloperContact } from '../../../../lib/developer-contact-store.js'; +import { approvePendingNominationFromClaim, isPublicDeveloper } from '../../../../lib/nominate.js'; const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT; const COSMOS_KEY = process.env.COSMOS_KEY; @@ -120,6 +121,10 @@ export async function POST() { } } + const autoApprovedDev = approvePendingNominationFromClaim(dev); + const autoApproved = Boolean(autoApprovedDev); + if (autoApprovedDev) dev = autoApprovedDev; + await container.items.upsert(dev); if (session.email) { @@ -136,12 +141,21 @@ export async function POST() { } } - if (!wasClaimed) { + if (!wasClaimed || autoApproved) { try { + let recipient = session.email; + if (!recipient && autoApproved) { + const contact = await getDeveloperContact(dev.login); + if (contact?.transactionalEmailsEnabled) recipient = contact.email; + } await sendLifecycleEmail({ - to: session.email, - message: buildClaimWelcomeEmail({ login: dev.login, name: dev.name }), - idempotencyKey: `profile-claimed-${dev.login.toLowerCase()}`, + to: recipient, + message: autoApproved + ? buildClaimApprovedEmail({ login: dev.login, name: dev.name }) + : buildClaimWelcomeEmail({ login: dev.login, name: dev.name }), + idempotencyKey: autoApproved + ? `nomination-auto-approved-${dev.login.toLowerCase()}-${Date.parse(dev.nomination.submittedAt)}` + : `profile-claimed-${dev.login.toLowerCase()}`, }); } catch (emailError) { console.error('Claim email delivery failed:', emailError.message); @@ -153,6 +167,8 @@ export async function POST() { login, created: resources.length === 0, claimedAt: dev.claimedAt, + profileStatus: isPublicDeveloper(dev) ? 'public' : dev.nomination.status, + autoApproved, }); } catch (err) { console.error('Claim error:', err); @@ -171,6 +187,7 @@ export async function POST() { login, created: !dev, claimedAt: new Date().toISOString(), + profileStatus: 'public', note: 'Claim recorded (dev mode — not persisted without Cosmos DB)', }); } diff --git a/app/page.jsx b/app/page.jsx index 7e374bc..380febf 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -8,6 +8,7 @@ import DetailPanel from '../components/DetailPanel.jsx'; import ComparePanel from '../components/ComparePanel.jsx'; import LoadingOverlay from '../components/LoadingOverlay.jsx'; import AddMeModal from '../components/AddMeModal.jsx'; +import ClaimStatusModal from '../components/ClaimStatusModal.jsx'; import AiProfileModal from '../components/AiProfileModal.jsx'; import IntroductionInboxModal from '../components/IntroductionInboxModal.jsx'; import QuickTour from '../components/QuickTour.jsx'; @@ -31,13 +32,14 @@ export default function Home() { const [compareDevs, setCompareDevs] = useState([]); const [theme, setTheme] = useState('dark'); const [user, setUser] = useState(null); - const [claimStatus, setClaimStatus] = useState('unclaimed'); // 'unclaimed' | 'claimed' | 'no_match' + const [claimStatus, setClaimStatus] = useState('unclaimed'); // 'unclaimed' | 'pending' | 'claimed' | 'no_match' const [claimedLogins, setClaimedLogins] = useState(new Set()); const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarView, setSidebarView] = useState('leaderboard'); const [cardRequest, setCardRequest] = useState(0); const [cardContext, setCardContext] = useState(null); const [showAddMe, setShowAddMe] = useState(false); + const [showClaimPending, setShowClaimPending] = useState(false); const [showAiProfile, setShowAiProfile] = useState(false); const [showIntroductions, setShowIntroductions] = useState(false); const [agentGlobeLayerVisible, setAgentGlobeLayerVisible] = useState(false); @@ -109,11 +111,20 @@ export default function Home() { const res = await fetch('/api/auth/claim', { method: 'POST' }); if (res.ok) { const result = await res.json(); + if (result.profileStatus !== 'public') { + setClaimStatus('pending'); + setSelectedDev(null); + setCardContext(null); + setCardRequest(0); + setShowClaimPending(true); + setSidebarOpen(false); + return; + } setClaimStatus('claimed'); setClaimedLogins(prev => new Set(prev).add(user.login)); let claimedDeveloper = developers.find(developer => developer.login === user.login); // If a new profile was created, reload developers to include it - if (result.created) { + if (result.created || result.autoApproved) { const devRes = await fetch('/api/developers', { cache: 'no-store' }); if (devRes.ok) { const raw = await devRes.json(); @@ -479,6 +490,7 @@ export default function Home() { )} {showAddMe && } + {showClaimPending && setShowClaimPending(false)} />} {showAiProfile && ( setShowAiProfile(false)} diff --git a/components/ClaimStatusModal.jsx b/components/ClaimStatusModal.jsx new file mode 100644 index 0000000..13f2727 --- /dev/null +++ b/components/ClaimStatusModal.jsx @@ -0,0 +1,29 @@ +'use client'; + +import React from 'react'; + +export default function ClaimStatusModal({ onClose }) { + return ( +
+
event.stopPropagation()} + > + + +

Profile claimed and pending review

+

Your profile is still being reviewed. We'll email you when it is approved and visible on the globe, usually within a week.

+

Your identity card will be available after approval.

+ +
+
+ ); +} \ No newline at end of file diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx index 3f5951d..bb8918b 100644 --- a/components/DetailPanel.jsx +++ b/components/DetailPanel.jsx @@ -567,14 +567,21 @@ function CardModal({ dev, claimSuccess, onClose }) { const handleDownload = async () => { try { const res = await fetch(cardUrl); + if (!res.ok || !res.headers.get('content-type')?.startsWith('image/')) { + throw new Error('Card image is unavailable'); + } const blob = await res.blob(); + if (!blob.size) throw new Error('Card image is empty'); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `devglobe-${login}.png`; a.click(); URL.revokeObjectURL(url); - } catch { /* ignore */ } + } catch { + setLoading(false); + setError(true); + } }; const handleCopyLink = () => { @@ -623,19 +630,19 @@ function CardModal({ dev, claimSuccess, onClose }) {
{loading && !error &&
Generating card...
} - {error &&
Failed to generate card
} + {error &&
Card unavailable. Please try again later.
} {`DevAgent setLoading(false)} onError={() => { setLoading(false); setError(true); }} />
-