From 60363b78a6fc8215227ef4868663edab9e6e7e44 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Thu, 4 Jun 2026 18:30:43 +0530 Subject: [PATCH 1/2] feat: enhance profile devcard identity --- src/components/layout/Footer.tsx | 1 + src/components/profile/DevCard.module.css | 103 ++++++++++++++++++++++ src/components/profile/DevCard.tsx | 71 ++++++++++++++- 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 0132df68..5adf9513 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -8,6 +8,7 @@ import { RefreshCw, Code, MessageSquare, + Shield, } from "lucide-react"; import logo from "@/assets/logo.webp"; import styles from "./Footer.module.css"; diff --git a/src/components/profile/DevCard.module.css b/src/components/profile/DevCard.module.css index fbdaebea..da0aa08f 100644 --- a/src/components/profile/DevCard.module.css +++ b/src/components/profile/DevCard.module.css @@ -359,6 +359,99 @@ white-space: nowrap; } +.identityStrip { + display: grid; + grid-template-columns: 32px 1fr; + align-items: center; + gap: 9px; + width: 100%; + margin-top: 4px; + padding: 8px; + border: 1px solid var(--devcard-panel-border); + border-radius: 14px; + background: + linear-gradient(135deg, rgba(0, 212, 255, 0.08), rgba(52, 211, 153, 0.06)), + var(--devcard-panel); +} + +.logoMark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(15, 23, 42, 0.08); + overflow: hidden; + box-shadow: 0 8px 18px -12px rgba(15, 23, 42, 0.45); +} + +:global(.dark) .logoMark { + background: rgba(255, 255, 255, 0.92); +} + +.identityText { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.identityTitle { + font-size: 10px; + font-weight: 800; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--devcard-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.identitySubtext { + font-size: 10px; + color: var(--devcard-text-soft); +} + +.socialLinks { + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + width: 100%; +} + +.socialLink { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 10px; + color: var(--devcard-text-muted); + background: var(--devcard-badge-bg); + border: 1px solid var(--devcard-badge-border); + transition: + color 0.2s ease, + border-color 0.2s ease, + background 0.2s ease, + transform 0.2s ease; +} + +.socialLink:hover, +.socialLink:focus-visible { + color: #00d4ff; + border-color: rgba(0, 212, 255, 0.32); + background: rgba(0, 212, 255, 0.10); + transform: translateY(-2px); + outline: none; +} + +.socialLinkProfile { + color: #34d399; +} + /* ── Level progress ────────────────────────────────────────────────── */ .progressSection { width: 100%; @@ -705,6 +798,16 @@ align-items: flex-start; } + .identityStrip { + width: calc(100% - 96px); + min-width: 180px; + margin-top: 0; + } + + .socialLinks { + justify-content: flex-start; + } + .name { text-align: left; } diff --git a/src/components/profile/DevCard.tsx b/src/components/profile/DevCard.tsx index 0c3b2548..35876478 100644 --- a/src/components/profile/DevCard.tsx +++ b/src/components/profile/DevCard.tsx @@ -7,13 +7,14 @@ import { Download, Link2, Check, MapPin, Calendar, Trophy, Zap, Flame, Star, Award, Github, Layers, Code2, Globe, Users, Brain, - ChevronRight, Sparkles, Medal, + ChevronRight, Sparkles, Medal, Linkedin, Instagram, ExternalLink, } from 'lucide-react'; import { calculateLevel } from '@/lib/points'; import { copyToClipboard } from '@/lib/clipboard'; import { collection, query, where, getCountFromServer } from 'firebase/firestore'; import { db } from '@/lib/firebase'; import { useNotificationActions } from '@/stores/ui-store'; +import { getSafeSocialUrl } from '@/lib/safe-social-url'; import styles from './DevCard.module.css'; // ── Badge registry ──────────────────────────────────────────────────────────── @@ -44,6 +45,7 @@ const LANG_COLORS: Record = { Shell: '#89e051', Vue: '#41b883', }; const getLangColor = (lang: string) => LANG_COLORS[lang] ?? '#94a3b8'; +const DEVPATH_LOGO_URL = 'https://devpath-website.web.app/_next/static/media/logo.0lp58p9nludg2.webp'; function resolveLevelColor(colorClass: string): string { const map: Record = { @@ -105,6 +107,21 @@ function fmtDate(raw: any): string { } catch { return 'Recent Member'; } } +function resolveSocialUrl(value: string | undefined, platform: 'github' | 'linkedin' | 'instagram'): string | null { + const trimmed = value?.trim(); + if (!trimmed) return null; + + if (/^https?:\/\//i.test(trimmed)) { + return getSafeSocialUrl(trimmed, platform); + } + + if (platform === 'github' && /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i.test(trimmed)) { + return `https://github.com/${trimmed}`; + } + + return null; +} + export default function DevCard({ user }: { user: any }) { const IMAGE_WAIT_TIMEOUT_MS = 5000; const cardRef = useRef(null); @@ -158,6 +175,21 @@ export default function DevCard({ user }: { user: any }) { const extraCount = Math.max(0, earnedBadges.length - 4); const topLangs = ((user?.githubStats?.topLanguages ?? []) as { language: string; count: number }[]).slice(0, 4); const totalLang = topLangs.reduce((s, l) => s + l.count, 0); + const socialLinks = { + github: resolveSocialUrl(user?.github ?? user?.githubStats?.username, 'github'), + linkedin: resolveSocialUrl(user?.linkedin, 'linkedin'), + instagram: resolveSocialUrl(user?.instagram, 'instagram'), + }; + const socialCount = Object.values(socialLinks).filter(Boolean).length; + const profileSignals = [ + user?.photoURL, + user?.bio || user?.aboutMarkdown, + user?.city || user?.state, + user?.githubStats?.connected, + socialCount > 0, + earnedBadges.length > 0, + ].filter(Boolean).length; + const profileCompletion = Math.round((profileSignals / 6) * 100); const animXP = useAnimatedCount(user?.points ?? 0); const animStreak = useAnimatedCount(user?.streak ?? 0, 900); @@ -326,6 +358,43 @@ export default function DevCard({ user }: { user: any }) { Joined {fmtDate(user?.createdAt)} {user?.githubStats?.username && {user.githubStats.username}} + + + DevPath Community logo + + + DevPath Verified + {profileCompletion}% profile ready + + + {socialCount > 0 && ( + + {socialLinks.github && ( + + + + )} + {socialLinks.linkedin && ( + + + + )} + {socialLinks.instagram && ( + + + + )} + + + + + )}
Level Progress{Math.round(levelInfo.progress)}%
From ffc7f7f9acb1f35059a5dc6176f6d31d4895d011 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Sat, 6 Jun 2026 17:48:26 +0530 Subject: [PATCH 2/2] fix: stabilize devcard typing and effects --- src/components/profile/DevCard.tsx | 120 ++++++++++++++--------------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/src/components/profile/DevCard.tsx b/src/components/profile/DevCard.tsx index 35876478..a09c64c5 100644 --- a/src/components/profile/DevCard.tsx +++ b/src/components/profile/DevCard.tsx @@ -8,6 +8,7 @@ import { Trophy, Zap, Flame, Star, Award, Github, Layers, Code2, Globe, Users, Brain, ChevronRight, Sparkles, Medal, Linkedin, Instagram, ExternalLink, + type LucideIcon, } from 'lucide-react'; import { calculateLevel } from '@/lib/points'; import { copyToClipboard } from '@/lib/clipboard'; @@ -18,7 +19,39 @@ import { getSafeSocialUrl } from '@/lib/safe-social-url'; import styles from './DevCard.module.css'; // ── Badge registry ──────────────────────────────────────────────────────────── -const BADGE_REGISTRY: Record = { +type BadgeInfo = { name: string; Icon: LucideIcon; color: string }; +type TimestampLike = { toDate: () => Date }; +type TopLanguage = { language: string; count: number }; +type MaybeString = string | null | undefined; +type DevCardUser = { + uid?: MaybeString; + name?: MaybeString; + photoURL?: MaybeString; + points?: number; + streak?: number; + achievements?: string[]; + github?: MaybeString; + linkedin?: MaybeString; + instagram?: MaybeString; + bio?: MaybeString; + aboutMarkdown?: MaybeString; + city?: MaybeString; + state?: MaybeString; + createdAt?: string | number | Date | TimestampLike; + completedQuizzes?: unknown[]; + followers?: unknown[]; + githubStats?: { + connected?: boolean; + username?: MaybeString; + topLanguages?: TopLanguage[]; + totalStars?: number; + stars?: number; + }; +}; + +const EASE_OUT: [number, number, number, number] = [0.19, 1, 0.22, 1]; + +const BADGE_REGISTRY: Record = { 'early-adopter': { name: 'Early Adopter', Icon: Sparkles, color: '#60a5fa' }, 'profile-perfect': { name: 'Profile Perfect', Icon: Check, color: '#34d399' }, 'builder-1': { name: 'Builder', Icon: Layers, color: '#fb923c' }, @@ -73,7 +106,10 @@ function resolveLevelBg(bgClass: string): string { function useAnimatedCount(target: number, duration = 1400) { const [count, setCount] = useState(0); useEffect(() => { - if (target === 0) { setCount(0); return; } + if (target === 0) { + const resetTimer = window.setTimeout(() => setCount(0), 0); + return () => clearTimeout(resetTimer); + } let current = 0; const step = target / (duration / 16); const timer = setInterval(() => { @@ -91,23 +127,25 @@ function fmtPoints(n: number) { if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); } -function fmtDate(raw: any): string { +function fmtDate(raw: DevCardUser['createdAt']): string { if (raw === null || raw === undefined) return 'Recent Member'; try { let d: Date; if (typeof raw === 'string' || typeof raw === 'number') { d = new Date(raw); - } else if (typeof raw.toDate === 'function') { + } else if (raw instanceof Date) { + d = raw; + } else if ('toDate' in raw && typeof raw.toDate === 'function') { d = raw.toDate(); } else { - d = new Date(raw); + return 'Recent Member'; } if (isNaN(d.getTime())) return 'Recent Member'; return d.toLocaleDateString('en-IN', { month: 'short', year: 'numeric' }); } catch { return 'Recent Member'; } } -function resolveSocialUrl(value: string | undefined, platform: 'github' | 'linkedin' | 'instagram'): string | null { +function resolveSocialUrl(value: MaybeString, platform: 'github' | 'linkedin' | 'instagram'): string | null { const trimmed = value?.trim(); if (!trimmed) return null; @@ -122,14 +160,12 @@ function resolveSocialUrl(value: string | undefined, platform: 'github' | 'linke return null; } -export default function DevCard({ user }: { user: any }) { - const IMAGE_WAIT_TIMEOUT_MS = 5000; +export default function DevCard({ user }: { user: DevCardUser }) { const cardRef = useRef(null); const [showSkeleton, setShowSkeleton] = useState(true); const [rank, setRank] = useState(null); const [rankLoading, setRankLoading] = useState(true); const [copied, setCopied] = useState(false); - const [downloading, setDownloading] = useState(false); const [langMounted, setLangMounted] = useState(false); const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); const { showSuccess, showError } = useNotificationActions(); @@ -148,9 +184,12 @@ export default function DevCard({ user }: { user: any }) { }, [user?.points]); useEffect(() => { - setShowSkeleton(true); - const timer = setTimeout(() => setShowSkeleton(false), 650); - return () => clearTimeout(timer); + const resetTimer = window.setTimeout(() => setShowSkeleton(true), 0); + const doneTimer = window.setTimeout(() => setShowSkeleton(false), 650); + return () => { + clearTimeout(resetTimer); + clearTimeout(doneTimer); + }; }, [user?.uid]); useEffect(() => { @@ -159,7 +198,8 @@ export default function DevCard({ user }: { user: any }) { }, []); useEffect(() => { - setAvatarLoadFailed(false); + const resetTimer = window.setTimeout(() => setAvatarLoadFailed(false), 0); + return () => clearTimeout(resetTimer); }, [user?.photoURL]); const levelInfo = calculateLevel(user?.points ?? 0); @@ -173,7 +213,7 @@ export default function DevCard({ user }: { user: any }) { const topBadges = earnedBadges.slice(0, 4); const extraCount = Math.max(0, earnedBadges.length - 4); - const topLangs = ((user?.githubStats?.topLanguages ?? []) as { language: string; count: number }[]).slice(0, 4); + const topLangs = (user?.githubStats?.topLanguages ?? []).slice(0, 4); const totalLang = topLangs.reduce((s, l) => s + l.count, 0); const socialLinks = { github: resolveSocialUrl(user?.github ?? user?.githubStats?.username, 'github'), @@ -198,48 +238,6 @@ export default function DevCard({ user }: { user: any }) { ? `${window.location.origin}/u/${user?.uid}` : `devpath.in/u/${user?.uid}`; - const waitForCardImages = async (root: HTMLElement) => { - const imgs = Array.from(root.querySelectorAll('img')); - await Promise.all( - imgs.map(async (img) => { - if (img.complete) { - // complete + naturalWidth 0 means a failed image; treat as terminal. - if (img.naturalWidth === 0) return; - return; - } - if (typeof img.decode === 'function') { - try { - await Promise.race([ - img.decode(), - new Promise((resolve) => { - setTimeout(resolve, IMAGE_WAIT_TIMEOUT_MS); - }), - ]); - return; - } catch { - // Fallback to load/error listeners if decode rejects. - } - } - - await new Promise((resolve) => { - const timeoutId = setTimeout(() => { - done(); - }, IMAGE_WAIT_TIMEOUT_MS); - - const done = () => { - img.removeEventListener('load', done); - img.removeEventListener('error', done); - clearTimeout(timeoutId); - resolve(); - }; - - img.addEventListener('load', done, { once: true }); - img.addEventListener('error', done, { once: true }); - }); - }) - ); - }; - const handleDownload = async () => { // Download functionality temporarily disabled in build environment. // Restoring this requires bundler-compatible html2canvas integration. @@ -323,7 +321,7 @@ export default function DevCard({ user }: { user: any }) { className={styles.card} initial={{ opacity: 0, y: 28, scale: 0.97 }} animate={{ opacity: 1, y: 0, scale: 1 }} - transition={{ duration: 0.65, ease: [0.19, 1, 0.22, 1] as any }} + transition={{ duration: 0.65, ease: EASE_OUT }} id="devcard-render" >
@@ -398,7 +396,7 @@ export default function DevCard({ user }: { user: any }) {
Level Progress{Math.round(levelInfo.progress)}%
- +
@@ -418,7 +416,7 @@ export default function DevCard({ user }: { user: any }) { Top Achievements
- {topBadges.map((b: { id: string; name: string; Icon: any; color: string }) => { + {topBadges.map((b: { id: string } & BadgeInfo) => { const BadgeIcon = b.Icon; return ( @@ -450,8 +448,8 @@ export default function DevCard({ user }: { user: any }) {
- - + +