From 2c5c9f43cdbd916281e8c27982b28dc3bb588ba0 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 2 Jan 2026 14:29:50 +0200 Subject: [PATCH 1/3] mock: profile example --- .../ActivityOverviewCard.tsx | 195 +++++++ .../MultiSourceHeatmap/MultiSourceHeatmap.tsx | 513 ++++++++++++++++++ .../components/MultiSourceHeatmap/index.ts | 3 + .../components/MultiSourceHeatmap/types.ts | 170 ++++++ .../experience/UserExperienceItem.tsx | 23 + .../experience-posts/ExperiencePostItem.tsx | 310 +++++++++++ .../ExperiencePostsSection.tsx | 221 ++++++++ .../experience-posts/ExperienceTimeline.tsx | 378 +++++++++++++ .../experience/experience-posts/index.ts | 4 + .../experience/experience-posts/types.ts | 183 +++++++ .../github-integration/ActivityChart.tsx | 151 ++++++ .../ExperienceWithGitStats.tsx | 94 ++++ .../GitIntegrationStats.tsx | 280 ++++++++++ .../github-integration/LanguageBar.tsx | 67 +++ .../experience/github-integration/index.ts | 5 + .../experience/github-integration/types.ts | 244 +++++++++ .../components/MultiSourceHeatmap.stories.tsx | 132 +++++ .../profile/ExperiencePosts.stories.tsx | 190 +++++++ .../profile/GitIntegration.stories.tsx | 118 ++++ 19 files changed, 3281 insertions(+) create mode 100644 packages/shared/src/components/MultiSourceHeatmap/ActivityOverviewCard.tsx create mode 100644 packages/shared/src/components/MultiSourceHeatmap/MultiSourceHeatmap.tsx create mode 100644 packages/shared/src/components/MultiSourceHeatmap/index.ts create mode 100644 packages/shared/src/components/MultiSourceHeatmap/types.ts create mode 100644 packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostItem.tsx create mode 100644 packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostsSection.tsx create mode 100644 packages/shared/src/features/profile/components/experience/experience-posts/ExperienceTimeline.tsx create mode 100644 packages/shared/src/features/profile/components/experience/experience-posts/index.ts create mode 100644 packages/shared/src/features/profile/components/experience/experience-posts/types.ts create mode 100644 packages/shared/src/features/profile/components/experience/github-integration/ActivityChart.tsx create mode 100644 packages/shared/src/features/profile/components/experience/github-integration/ExperienceWithGitStats.tsx create mode 100644 packages/shared/src/features/profile/components/experience/github-integration/GitIntegrationStats.tsx create mode 100644 packages/shared/src/features/profile/components/experience/github-integration/LanguageBar.tsx create mode 100644 packages/shared/src/features/profile/components/experience/github-integration/index.ts create mode 100644 packages/shared/src/features/profile/components/experience/github-integration/types.ts create mode 100644 packages/storybook/stories/components/MultiSourceHeatmap.stories.tsx create mode 100644 packages/storybook/stories/features/profile/ExperiencePosts.stories.tsx create mode 100644 packages/storybook/stories/features/profile/GitIntegration.stories.tsx diff --git a/packages/shared/src/components/MultiSourceHeatmap/ActivityOverviewCard.tsx b/packages/shared/src/components/MultiSourceHeatmap/ActivityOverviewCard.tsx new file mode 100644 index 0000000000..b0da3a4e23 --- /dev/null +++ b/packages/shared/src/components/MultiSourceHeatmap/ActivityOverviewCard.tsx @@ -0,0 +1,195 @@ +import type { ReactElement } from 'react'; +import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; +import { subDays } from 'date-fns'; +import { + Typography, + TypographyType, + TypographyColor, + TypographyTag, +} from '../typography/Typography'; +import { ArrowIcon, CalendarIcon } from '../icons'; +import { IconSize } from '../Icon'; +import { MultiSourceHeatmap } from './MultiSourceHeatmap'; +import type { DayActivityDetailed, ActivitySource } from './types'; +import { SOURCE_CONFIGS, generateMockMultiSourceActivity } from './types'; + +type TimeRange = '3m' | '6m' | '1y'; + +interface ActivityOverviewCardProps { + activities?: DayActivityDetailed[]; + initialTimeRange?: TimeRange; + initiallyOpen?: boolean; +} + +export function ActivityOverviewCard({ + activities: propActivities, + initialTimeRange = '1y', + initiallyOpen = false, +}: ActivityOverviewCardProps): ReactElement { + const [isOpen, setIsOpen] = useState(initiallyOpen); + const [timeRange, setTimeRange] = useState(initialTimeRange); + + // Memoize dates to prevent unnecessary recalculations + const { startDate, endDate } = useMemo(() => { + const end = new Date(); + let start: Date; + switch (timeRange) { + case '3m': + start = subDays(end, 90); + break; + case '6m': + start = subDays(end, 180); + break; + case '1y': + default: + start = subDays(end, 365); + } + return { startDate: start, endDate: end }; + }, [timeRange]); + + // Use provided activities or generate mock + const activities = useMemo(() => { + if (propActivities) { + return propActivities.filter((a) => { + const date = new Date(a.date); + return date >= startDate && date <= endDate; + }); + } + return generateMockMultiSourceActivity(startDate, endDate); + }, [propActivities, startDate, endDate]); + + // Calculate quick stats for header + const quickStats = useMemo(() => { + const total = activities.reduce((sum, a) => sum + a.total, 0); + const activeDays = activities.filter((a) => a.total > 0).length; + + // Get top sources + const sourceTotals: Partial> = {}; + activities.forEach((a) => { + Object.entries(a.sources).forEach(([source, count]) => { + sourceTotals[source as ActivitySource] = + (sourceTotals[source as ActivitySource] || 0) + count; + }); + }); + + const topSources = Object.entries(sourceTotals) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([source]) => source as ActivitySource); + + return { total, activeDays, topSources }; + }, [activities]); + + return ( +
+ {/* Header */} + + + {/* Collapsible content */} +
+
+ {/* Time range selector */} +
+ + Period + +
+ {(['3m', '6m', '1y'] as TimeRange[]).map((range) => ( + + ))} +
+
+ + {/* Heatmap */} + +
+
+
+ ); +} diff --git a/packages/shared/src/components/MultiSourceHeatmap/MultiSourceHeatmap.tsx b/packages/shared/src/components/MultiSourceHeatmap/MultiSourceHeatmap.tsx new file mode 100644 index 0000000000..8a930f1d57 --- /dev/null +++ b/packages/shared/src/components/MultiSourceHeatmap/MultiSourceHeatmap.tsx @@ -0,0 +1,513 @@ +import type { ReactElement, ReactNode } from 'react'; +import React, { useMemo, useState } from 'react'; +import { addDays, differenceInDays, endOfWeek, subDays } from 'date-fns'; +import classNames from 'classnames'; +import { Tooltip } from '../tooltip/Tooltip'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../typography/Typography'; +import { GitHubIcon, GitLabIcon } from '../icons'; +import { IconSize } from '../Icon'; +import type { ActivitySource, DayActivityDetailed } from './types'; +import { SOURCE_CONFIGS } from './types'; + +const DAYS_IN_WEEK = 7; +const SQUARE_SIZE = 10; +const GUTTER_SIZE = 3; +const SQUARE_SIZE_WITH_GUTTER = SQUARE_SIZE + GUTTER_SIZE; +const MONTH_LABELS = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + +// Intensity levels for the heatmap +const INTENSITY_COLORS = [ + 'var(--theme-float)', // 0 - no activity + 'var(--theme-overlay-quaternary-cabbage)', // 1 - low + 'var(--theme-accent-cabbage-subtler)', // 2 - medium-low + 'var(--theme-accent-cabbage-default)', // 3 - medium + 'var(--theme-accent-onion-default)', // 4 - high +]; + +// Multi-source gradient colors for pie segments +const getSourceColor = (source: ActivitySource): string => { + return SOURCE_CONFIGS[source]?.color || '#666'; +}; + +interface SourceIconProps { + source: ActivitySource; + size?: IconSize; + className?: string; +} + +function SourceIcon({ + source, + size = IconSize.XSmall, + className, +}: SourceIconProps): ReactElement | null { + switch (source) { + case 'github': + return ; + case 'gitlab': + return ; + default: + return ( +
+ ); + } +} + +interface DayTooltipProps { + activity: DayActivityDetailed | undefined; + date: Date; +} + +function DayTooltip({ activity, date }: DayTooltipProps): ReactElement { + const dateStr = date.toLocaleDateString('en-US', { + weekday: 'long', + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + if (!activity || activity.total === 0) { + return ( +
+ {dateStr} + No activity +
+ ); + } + + return ( +
+ {dateStr} +
+ + {activity.total} + + contributions +
+
+ {Object.entries(activity.sources).map(([source, count]) => ( +
+
+ + + {SOURCE_CONFIGS[source as ActivitySource]?.label} + +
+ {count} +
+ ))} +
+ {activity.breakdown && ( +
+ {activity.breakdown.commits > 0 && ( + {activity.breakdown.commits} commits + )} + {activity.breakdown.pullRequests > 0 && ( + {activity.breakdown.pullRequests} PRs + )} + {activity.breakdown.reads > 0 && ( + {activity.breakdown.reads} reads + )} + {activity.breakdown.answers > 0 && ( + {activity.breakdown.answers} answers + )} +
+ )} +
+ ); +} + +interface MultiSourceHeatmapProps { + activities: DayActivityDetailed[]; + startDate?: Date; + endDate?: Date; + enabledSources?: ActivitySource[]; + showLegend?: boolean; + showStats?: boolean; +} + +export function MultiSourceHeatmap({ + activities, + startDate: propStartDate, + endDate: propEndDate, + enabledSources, + showLegend = true, + showStats = true, +}: MultiSourceHeatmapProps): ReactElement { + const endDate = propEndDate || new Date(); + const startDate = propStartDate || subDays(endDate, 365); + + const [hoveredSource, setHoveredSource] = useState( + null, + ); + + // Build activity map by date + const activityMap = useMemo(() => { + const map: Record = {}; + activities.forEach((activity) => { + map[activity.date] = activity; + }); + return map; + }, [activities]); + + // Calculate totals and stats + const stats = useMemo(() => { + const sourceTotals: Partial> = {}; + let totalContributions = 0; + let activeDays = 0; + let currentStreak = 0; + let longestStreak = 0; + let tempStreak = 0; + + // Sort activities by date for streak calculation + const sortedActivities = [...activities].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + + sortedActivities.forEach((activity) => { + if (activity.total > 0) { + totalContributions += activity.total; + activeDays += 1; + tempStreak += 1; + longestStreak = Math.max(longestStreak, tempStreak); + + Object.entries(activity.sources).forEach(([source, count]) => { + sourceTotals[source as ActivitySource] = + (sourceTotals[source as ActivitySource] || 0) + count; + }); + } else { + tempStreak = 0; + } + }); + + // Calculate current streak (from most recent day) + for (let i = sortedActivities.length - 1; i >= 0; i -= 1) { + if (sortedActivities[i].total > 0) { + currentStreak += 1; + } else { + break; + } + } + + return { + totalContributions, + activeDays, + currentStreak, + longestStreak, + sourceTotals, + }; + }, [activities]); + + // Get active sources + const activeSources = useMemo(() => { + const sources = Object.keys(stats.sourceTotals) as ActivitySource[]; + if (enabledSources) { + return sources.filter((s) => enabledSources.includes(s)); + } + return sources; + }, [stats.sourceTotals, enabledSources]); + + // Calculate week data + const numEmptyDaysAtEnd = DAYS_IN_WEEK - 1 - endDate.getDay(); + const numEmptyDaysAtStart = startDate.getDay(); + const startDateWithEmptyDays = addDays(startDate, -numEmptyDaysAtStart); + const dateDifferenceInDays = differenceInDays(endDate, startDate); + const numDaysRoundedToWeek = + dateDifferenceInDays + numEmptyDaysAtStart + numEmptyDaysAtEnd; + const weekCount = Math.ceil(numDaysRoundedToWeek / DAYS_IN_WEEK); + + // Get intensity level (0-4) based on activity + const getIntensityLevel = (total: number): number => { + if (total === 0) { + return 0; + } + if (total <= 3) { + return 1; + } + if (total <= 8) { + return 2; + } + if (total <= 15) { + return 3; + } + return 4; + }; + + // Render a single day square + const renderDay = (weekIndex: number, dayIndex: number): ReactNode => { + const index = weekIndex * DAYS_IN_WEEK + dayIndex; + const isOutOfRange = + index < numEmptyDaysAtStart || + index >= numEmptyDaysAtStart + dateDifferenceInDays + 1; + + if (isOutOfRange) { + return null; + } + + const date = addDays(startDateWithEmptyDays, index); + const dateStr = date.toISOString().split('T')[0]; + const activity = activityMap[dateStr]; + const total = activity?.total || 0; + const intensity = getIntensityLevel(total); + + // Check if any source is hovered and this day has that source + const isHighlighted = + !hoveredSource || + (activity?.sources && hoveredSource in activity.sources); + const opacity = hoveredSource && !isHighlighted ? 0.2 : 1; + + // For multi-source days, create a gradient or show dominant source + const sources = activity?.sources + ? (Object.keys(activity.sources) as ActivitySource[]) + : []; + const hasMultipleSources = sources.length > 1; + + return ( + } + delayDuration={100} + > + + {/* Base square */} + + {/* Multi-source indicator - small colored dots */} + {hasMultipleSources && total > 0 && ( + + {sources.slice(0, 3).map((source, i) => ( + + ))} + + )} + {/* Single source color accent */} + {sources.length === 1 && total > 0 && ( + + )} + + + ); + }; + + // Render a week column + const renderWeek = (weekIndex: number): ReactNode => { + return ( + + {Array.from({ length: DAYS_IN_WEEK }, (_, dayIndex) => + renderDay(weekIndex, dayIndex), + )} + + ); + }; + + // Render month labels + const renderMonthLabels = (): ReactNode => { + const labels: ReactNode[] = []; + + for (let weekIndex = 0; weekIndex < weekCount; weekIndex += 1) { + const date = endOfWeek( + addDays(startDateWithEmptyDays, weekIndex * DAYS_IN_WEEK), + ); + if (date.getDate() >= 7 && date.getDate() <= 14) { + labels.push( + + {MONTH_LABELS[date.getMonth()]} + , + ); + } + } + + return labels; + }; + + const svgWidth = weekCount * SQUARE_SIZE_WITH_GUTTER; + const svgHeight = DAYS_IN_WEEK * SQUARE_SIZE_WITH_GUTTER + 20; + + return ( +
+ {/* Stats row */} + {showStats && ( +
+
+ + {stats.totalContributions.toLocaleString()} + + + contributions + +
+
+ + {stats.activeDays} + + + active days + +
+
+ + {stats.currentStreak} + + + day streak + +
+
+ + {stats.longestStreak} + + + longest streak + +
+
+ )} + + {/* Heatmap */} +
+ + {/* Month labels */} + {renderMonthLabels()} + {/* Weeks grid */} + + {Array.from({ length: weekCount }, (_, weekIndex) => + renderWeek(weekIndex), + )} + + +
+ + {/* Legend and source filters */} + {showLegend && ( +
+ {/* Intensity legend */} +
+ + Less + + {INTENSITY_COLORS.map((color) => ( +
+ ))} + + More + +
+ + {/* Source breakdown */} +
+ {activeSources.map((source) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/packages/shared/src/components/MultiSourceHeatmap/index.ts b/packages/shared/src/components/MultiSourceHeatmap/index.ts new file mode 100644 index 0000000000..45623929a7 --- /dev/null +++ b/packages/shared/src/components/MultiSourceHeatmap/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export { MultiSourceHeatmap } from './MultiSourceHeatmap'; +export { ActivityOverviewCard } from './ActivityOverviewCard'; diff --git a/packages/shared/src/components/MultiSourceHeatmap/types.ts b/packages/shared/src/components/MultiSourceHeatmap/types.ts new file mode 100644 index 0000000000..d96e188f52 --- /dev/null +++ b/packages/shared/src/components/MultiSourceHeatmap/types.ts @@ -0,0 +1,170 @@ +/** + * Types for multi-source activity heatmap + * Aggregates contributions from various platforms + */ + +export type ActivitySource = + | 'github' + | 'gitlab' + | 'dailydev' + | 'stackoverflow' + | 'linkedin' + | 'devto'; + +export interface SourceConfig { + id: ActivitySource; + label: string; + color: string; + icon: string; + enabled: boolean; +} + +export interface DayActivity { + date: string; + sources: Partial>; + total: number; +} + +export interface ActivityBreakdown { + commits: number; + pullRequests: number; + reviews: number; + issues: number; + posts: number; + comments: number; + reads: number; + upvotes: number; + answers: number; +} + +export interface DayActivityDetailed extends DayActivity { + breakdown?: Partial; +} + +export const SOURCE_CONFIGS: Record< + ActivitySource, + Omit +> = { + github: { + id: 'github', + label: 'GitHub', + color: '#238636', + icon: 'GitHub', + }, + gitlab: { + id: 'gitlab', + label: 'GitLab', + color: '#fc6d26', + icon: 'GitLab', + }, + dailydev: { + id: 'dailydev', + label: 'daily.dev', + color: '#ce3df3', + icon: 'Daily', + }, + stackoverflow: { + id: 'stackoverflow', + label: 'Stack Overflow', + color: '#f48024', + icon: 'StackOverflow', + }, + linkedin: { + id: 'linkedin', + label: 'LinkedIn', + color: '#0a66c2', + icon: 'LinkedIn', + }, + devto: { + id: 'devto', + label: 'DEV.to', + color: '#3b49df', + icon: 'DevTo', + }, +}; + +// Generate mock activity data +export const generateMockMultiSourceActivity = ( + startDate: Date, + endDate: Date, +): DayActivityDetailed[] => { + const activities: DayActivityDetailed[] = []; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dateStr = currentDate.toISOString().split('T')[0]; + const dayOfWeek = currentDate.getDay(); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + // Simulate realistic activity patterns + const baseActivity = isWeekend ? 0.3 : 1; + const randomFactor = Math.random(); + + // Some days have no activity + if (randomFactor < 0.15) { + activities.push({ + date: dateStr, + sources: {}, + total: 0, + }); + } else { + const sources: Partial> = {}; + let total = 0; + + // GitHub - most active + if (randomFactor > 0.2) { + const github = Math.floor(Math.random() * 15 * baseActivity) + 1; + sources.github = github; + total += github; + } + + // daily.dev - reading activity + if (randomFactor > 0.1) { + const dailydev = Math.floor(Math.random() * 8 * baseActivity) + 1; + sources.dailydev = dailydev; + total += dailydev; + } + + // GitLab - occasional + if (randomFactor > 0.6) { + const gitlab = Math.floor(Math.random() * 6 * baseActivity) + 1; + sources.gitlab = gitlab; + total += gitlab; + } + + // Stack Overflow - rare but valuable + if (randomFactor > 0.85) { + const stackoverflow = Math.floor(Math.random() * 3) + 1; + sources.stackoverflow = stackoverflow; + total += stackoverflow; + } + + // DEV.to - occasional posts/comments + if (randomFactor > 0.9) { + const devto = Math.floor(Math.random() * 2) + 1; + sources.devto = devto; + total += devto; + } + + activities.push({ + date: dateStr, + sources, + total, + breakdown: { + commits: sources.github ? Math.floor(sources.github * 0.6) : 0, + pullRequests: sources.github ? Math.floor(sources.github * 0.2) : 0, + reviews: sources.github ? Math.floor(sources.github * 0.2) : 0, + posts: (sources.dailydev || 0) + (sources.devto || 0), + reads: sources.dailydev ? sources.dailydev * 3 : 0, + comments: Math.floor(total * 0.1), + upvotes: Math.floor(total * 0.15), + answers: sources.stackoverflow || 0, + }, + }); + } + + currentDate.setDate(currentDate.getDate() + 1); + } + + return activities; +}; diff --git a/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx b/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx index 9192991540..d36d1e07bf 100644 --- a/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx +++ b/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx @@ -33,9 +33,22 @@ import { LazyModal } from '../../../../components/modals/common/types'; import { IconSize } from '../../../../components/Icon'; import { VerifiedBadge } from './VerifiedBadge'; import type { TLocation } from '../../../../graphql/autocomplete'; +import { + GitIntegrationStats, + generateMockGitStats, +} from './github-integration'; +import { + ExperienceTimeline, + generateMockExperiencePosts, +} from './experience-posts'; +import { ActivityOverviewCard } from '../../../../components/MultiSourceHeatmap'; const MAX_SKILLS = 3; +// TODO: Remove these mocks - for demo purposes only +const MOCK_GIT_DATA = generateMockGitStats('github'); +const MOCK_EXPERIENCE_POSTS = generateMockExperiencePosts(); + const getDisplayLocation = ( grouped: boolean | undefined, locationType: number | null | undefined, @@ -272,6 +285,16 @@ export function UserExperienceItem({ )}
)} + {/* GitHub/GitLab Integration - Mock for demo */} + {isWorkExperience && !grouped && ( + + )} + {/* Experience Posts Timeline - Mock for demo */} + {isWorkExperience && !grouped && ( + + )} + {/* Multi-source Activity Heatmap - Mock for demo */} + {isWorkExperience && !grouped && }
); diff --git a/packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostItem.tsx b/packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostItem.tsx new file mode 100644 index 0000000000..9375ffe3b5 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostItem.tsx @@ -0,0 +1,310 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../../../../components/typography/Typography'; +import { + FlagIcon, + LinkIcon, + TerminalIcon, + PlayIcon, + StarIcon, + GitHubIcon, + OpenLinkIcon, +} from '../../../../../components/icons'; +import { IconSize } from '../../../../../components/Icon'; +import { Pill, PillSize } from '../../../../../components/Pill'; +import { Image, ImageType } from '../../../../../components/image/Image'; +import type { + ExperiencePost, + ProjectPost, + PublicationPost, + MediaPost, + AchievementPost, + OpenSourcePost, +} from './types'; +import { ExperiencePostType } from './types'; + +const TYPE_ICONS = { + [ExperiencePostType.Milestone]: FlagIcon, + [ExperiencePostType.Publication]: LinkIcon, + [ExperiencePostType.Project]: TerminalIcon, + [ExperiencePostType.Media]: PlayIcon, + [ExperiencePostType.Achievement]: StarIcon, + [ExperiencePostType.OpenSource]: GitHubIcon, +}; + +const TYPE_COLORS = { + [ExperiencePostType.Milestone]: + 'bg-accent-onion-subtler text-accent-onion-default', + [ExperiencePostType.Publication]: + 'bg-accent-water-subtler text-accent-water-default', + [ExperiencePostType.Project]: + 'bg-accent-cabbage-subtler text-accent-cabbage-default', + [ExperiencePostType.Media]: + 'bg-accent-cheese-subtler text-accent-cheese-default', + [ExperiencePostType.Achievement]: + 'bg-accent-bun-subtler text-accent-bun-default', + [ExperiencePostType.OpenSource]: + 'bg-accent-blueCheese-subtler text-accent-blueCheese-default', +}; + +const TYPE_LABELS = { + [ExperiencePostType.Milestone]: 'Milestone', + [ExperiencePostType.Publication]: 'Publication', + [ExperiencePostType.Project]: 'Project', + [ExperiencePostType.Media]: 'Media', + [ExperiencePostType.Achievement]: 'Achievement', + [ExperiencePostType.OpenSource]: 'Open Source', +}; + +const formatDate = (dateStr: string): string => { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'short', + year: 'numeric', + }); +}; + +interface ExperiencePostItemProps { + post: ExperiencePost; +} + +export function ExperiencePostItem({ + post, +}: ExperiencePostItemProps): ReactElement { + const Icon = TYPE_ICONS[post.type]; + const colorClass = TYPE_COLORS[post.type]; + const label = TYPE_LABELS[post.type]; + + const renderMeta = (): ReactElement | null => { + switch (post.type) { + case ExperiencePostType.Publication: { + const pub = post as PublicationPost; + return ( +
+ {pub.publisher && ( + + {pub.publisher} + + )} + {pub.readTime && ( + <> + · + + {pub.readTime} min read + + + )} +
+ ); + } + case ExperiencePostType.Project: { + const proj = post as ProjectPost; + return ( +
+ {proj.technologies?.slice(0, 4).map((tech) => ( + + ))} + {proj.status && ( + + )} +
+ ); + } + case ExperiencePostType.Media: { + const media = post as MediaPost; + return ( +
+ {media.venue && ( + + {media.venue} + + )} + {media.duration && ( + <> + · + + {media.duration} min + + + )} +
+ ); + } + case ExperiencePostType.Achievement: { + const achievement = post as AchievementPost; + return ( +
+ {achievement.issuer && ( + + {achievement.issuer} + + )} + {achievement.credentialId && ( + <> + · + + ID: {achievement.credentialId} + + + )} +
+ ); + } + case ExperiencePostType.OpenSource: { + const oss = post as OpenSourcePost; + return ( +
+ {oss.stars !== undefined && ( +
+ + + {oss.stars.toLocaleString()} + +
+ )} + {oss.contributions !== undefined && ( + + {oss.contributions} contributions + + )} +
+ ); + } + default: + return null; + } + }; + + const content = ( +
+ {/* Thumbnail or icon */} + {post.image ? ( + {post.title} + ) : ( +
+ +
+ )} + + {/* Content */} +
+
+
+
+ + + {formatDate(post.date)} + +
+ + {post.title} + +
+ {post.url && ( + + )} +
+ + {post.description && ( + + {post.description} + + )} + + {renderMeta()} +
+
+ ); + + if (post.url) { + return ( + + {content} + + ); + } + + return
{content}
; +} diff --git a/packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostsSection.tsx b/packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostsSection.tsx new file mode 100644 index 0000000000..e3f6c971c7 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/experience-posts/ExperiencePostsSection.tsx @@ -0,0 +1,221 @@ +import type { ReactElement } from 'react'; +import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../../../../components/typography/Typography'; +import { ArrowIcon, HashtagIcon } from '../../../../../components/icons'; +import { IconSize } from '../../../../../components/Icon'; +import { Pill, PillSize } from '../../../../../components/Pill'; +import type { ExperiencePost } from './types'; +import { ExperiencePostType } from './types'; +import { ExperiencePostItem } from './ExperiencePostItem'; + +const TYPE_LABELS: Record = { + all: 'All', + [ExperiencePostType.Milestone]: 'Milestones', + [ExperiencePostType.Publication]: 'Publications', + [ExperiencePostType.Project]: 'Projects', + [ExperiencePostType.Media]: 'Media', + [ExperiencePostType.Achievement]: 'Achievements', + [ExperiencePostType.OpenSource]: 'Open Source', +}; + +interface ExperiencePostsSectionProps { + posts: ExperiencePost[]; + initiallyOpen?: boolean; +} + +export function ExperiencePostsSection({ + posts, + initiallyOpen = false, +}: ExperiencePostsSectionProps): ReactElement { + const [isOpen, setIsOpen] = useState(initiallyOpen); + const [activeFilter, setActiveFilter] = useState( + 'all', + ); + const [showAll, setShowAll] = useState(false); + + // Get unique post types for filter tabs + const availableTypes = useMemo(() => { + const types = new Set(posts.map((p) => p.type)); + return Array.from(types); + }, [posts]); + + // Filter posts by type + const filteredPosts = useMemo(() => { + if (activeFilter === 'all') { + return posts; + } + return posts.filter((p) => p.type === activeFilter); + }, [posts, activeFilter]); + + // Limit visible posts + const visiblePosts = showAll ? filteredPosts : filteredPosts.slice(0, 3); + const hiddenCount = filteredPosts.length - 3; + + // Count by type for preview + const typeCounts = useMemo(() => { + return posts.reduce((acc, post) => { + acc[post.type] = (acc[post.type] || 0) + 1; + return acc; + }, {} as Record); + }, [posts]); + + return ( +
+ {/* Header */} + + + {/* Collapsible content */} +
+
+ {/* Filter tabs */} + {availableTypes.length > 1 && ( +
+ + {availableTypes.map((type) => ( + + ))} +
+ )} + + {/* Posts list */} +
+ {visiblePosts.map((post, index) => ( +
0, + })} + > + +
+ ))} +
+ + {/* Show more/less */} + {hiddenCount > 0 && !showAll && ( + + )} + {showAll && filteredPosts.length > 3 && ( + + )} +
+
+
+ ); +} diff --git a/packages/shared/src/features/profile/components/experience/experience-posts/ExperienceTimeline.tsx b/packages/shared/src/features/profile/components/experience/experience-posts/ExperienceTimeline.tsx new file mode 100644 index 0000000000..73cc2c786b --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/experience-posts/ExperienceTimeline.tsx @@ -0,0 +1,378 @@ +import type { ReactElement } from 'react'; +import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; +import { + Typography, + TypographyType, + TypographyColor, + TypographyTag, +} from '../../../../../components/typography/Typography'; +import { + ArrowIcon, + FlagIcon, + LinkIcon, + TerminalIcon, + PlayIcon, + StarIcon, + GitHubIcon, + OpenLinkIcon, +} from '../../../../../components/icons'; +import { IconSize } from '../../../../../components/Icon'; +import { Image, ImageType } from '../../../../../components/image/Image'; +import type { + ExperiencePost, + ProjectPost, + PublicationPost, + MediaPost, + AchievementPost, + OpenSourcePost, +} from './types'; +import { ExperiencePostType } from './types'; + +const TYPE_ICONS = { + [ExperiencePostType.Milestone]: FlagIcon, + [ExperiencePostType.Publication]: LinkIcon, + [ExperiencePostType.Project]: TerminalIcon, + [ExperiencePostType.Media]: PlayIcon, + [ExperiencePostType.Achievement]: StarIcon, + [ExperiencePostType.OpenSource]: GitHubIcon, +}; + +const TYPE_COLORS = { + [ExperiencePostType.Milestone]: 'bg-accent-onion-default', + [ExperiencePostType.Publication]: 'bg-accent-water-default', + [ExperiencePostType.Project]: 'bg-accent-cabbage-default', + [ExperiencePostType.Media]: 'bg-accent-cheese-default', + [ExperiencePostType.Achievement]: 'bg-accent-bun-default', + [ExperiencePostType.OpenSource]: 'bg-accent-blueCheese-default', +}; + +const TYPE_LABELS = { + [ExperiencePostType.Milestone]: 'Milestone', + [ExperiencePostType.Publication]: 'Publication', + [ExperiencePostType.Project]: 'Project', + [ExperiencePostType.Media]: 'Media', + [ExperiencePostType.Achievement]: 'Achievement', + [ExperiencePostType.OpenSource]: 'Open Source', +}; + +const formatDate = (dateStr: string): string => { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { + month: 'short', + year: 'numeric', + }); +}; + +interface TimelineNodeProps { + post: ExperiencePost; + isLast: boolean; +} + +function TimelineNode({ post, isLast }: TimelineNodeProps): ReactElement { + const Icon = TYPE_ICONS[post.type]; + const color = TYPE_COLORS[post.type]; + const label = TYPE_LABELS[post.type]; + const dateStr = formatDate(post.date); + + const renderMeta = (): ReactElement | null => { + switch (post.type) { + case ExperiencePostType.Publication: { + const pub = post as PublicationPost; + return pub.publisher ? ( + + {pub.publisher} + + ) : null; + } + case ExperiencePostType.Project: { + const proj = post as ProjectPost; + return proj.technologies?.length ? ( +
+ {proj.technologies.slice(0, 3).map((tech) => ( + + {tech} + + ))} +
+ ) : null; + } + case ExperiencePostType.Media: { + const media = post as MediaPost; + return media.venue ? ( + + {media.venue} + + ) : null; + } + case ExperiencePostType.Achievement: { + const achievement = post as AchievementPost; + return achievement.issuer ? ( + + {achievement.issuer} + + ) : null; + } + case ExperiencePostType.OpenSource: { + const oss = post as OpenSourcePost; + return oss.stars ? ( +
+ + + {oss.stars.toLocaleString()} + +
+ ) : null; + } + default: + return null; + } + }; + + const content = ( +
+ {/* Timeline connector */} +
+ {/* Node */} +
+ +
+ {/* Connecting line */} + {!isLast && ( +
+ )} +
+ + {/* Content card */} +
+ {/* Header row */} +
+
+
+ + {label} + + · + + {dateStr} + +
+ + {post.title} + +
+ + {/* Thumbnail */} + {post.image && ( + {post.title} + )} +
+ + {/* Description */} + {post.description && ( + + {post.description} + + )} + + {/* Meta info */} +
+ {renderMeta()} + {post.url && ( + + )} +
+
+
+ ); + + if (post.url) { + return ( + + {content} + + ); + } + + return content; +} + +interface ExperienceTimelineProps { + posts: ExperiencePost[]; + initiallyOpen?: boolean; +} + +export function ExperienceTimeline({ + posts, + initiallyOpen = false, +}: ExperienceTimelineProps): ReactElement { + const [isOpen, setIsOpen] = useState(initiallyOpen); + const [showAll, setShowAll] = useState(false); + + // Sort posts by date descending + const sortedPosts = useMemo(() => { + return [...posts].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), + ); + }, [posts]); + + const visiblePosts = showAll ? sortedPosts : sortedPosts.slice(0, 4); + const hiddenCount = sortedPosts.length - 4; + const postCount = posts.length; + + return ( +
+ {/* Header */} + + + {/* Collapsible content */} +
+
+ {/* Timeline */} +
+ {visiblePosts.map((post, index) => ( + + ))} +
+ + {/* Show more/less */} + {hiddenCount > 0 && !showAll && ( + + )} + {showAll && sortedPosts.length > 4 && ( + + )} +
+
+
+ ); +} diff --git a/packages/shared/src/features/profile/components/experience/experience-posts/index.ts b/packages/shared/src/features/profile/components/experience/experience-posts/index.ts new file mode 100644 index 0000000000..f128807c50 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/experience-posts/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export { ExperiencePostItem } from './ExperiencePostItem'; +export { ExperiencePostsSection } from './ExperiencePostsSection'; +export { ExperienceTimeline } from './ExperienceTimeline'; diff --git a/packages/shared/src/features/profile/components/experience/experience-posts/types.ts b/packages/shared/src/features/profile/components/experience/experience-posts/types.ts new file mode 100644 index 0000000000..a68bc08dcc --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/experience-posts/types.ts @@ -0,0 +1,183 @@ +/** + * Types for experience-linked posts/content + * These represent content that users can attach to their work experiences + */ + +export enum ExperiencePostType { + Milestone = 'milestone', + Publication = 'publication', + Project = 'project', + Media = 'media', + Achievement = 'achievement', + OpenSource = 'opensource', +} + +export interface ExperiencePostBase { + id: string; + type: ExperiencePostType; + title: string; + description?: string; + date: string; + url?: string; + image?: string; +} + +export interface MilestonePost extends ExperiencePostBase { + type: ExperiencePostType.Milestone; + milestone: string; // e.g., "Promoted to Senior", "1 Year Anniversary" +} + +export interface PublicationPost extends ExperiencePostBase { + type: ExperiencePostType.Publication; + publisher?: string; // e.g., "Medium", "Dev.to", "Company Blog" + readTime?: number; // in minutes +} + +export interface ProjectPost extends ExperiencePostBase { + type: ExperiencePostType.Project; + technologies?: string[]; + status?: 'completed' | 'in-progress' | 'launched'; +} + +export interface MediaPost extends ExperiencePostBase { + type: ExperiencePostType.Media; + mediaType: 'video' | 'podcast' | 'presentation' | 'talk'; + duration?: number; // in minutes + venue?: string; // e.g., "React Conf 2024", "Company All-Hands" +} + +export interface AchievementPost extends ExperiencePostBase { + type: ExperiencePostType.Achievement; + issuer?: string; // e.g., "AWS", "Google", "Company" + credentialId?: string; +} + +export interface OpenSourcePost extends ExperiencePostBase { + type: ExperiencePostType.OpenSource; + repository?: string; + stars?: number; + contributions?: number; +} + +export type ExperiencePost = + | MilestonePost + | PublicationPost + | ProjectPost + | MediaPost + | AchievementPost + | OpenSourcePost; + +// Post type metadata for display +export const POST_TYPE_META: Record< + ExperiencePostType, + { label: string; icon: string; color: string } +> = { + [ExperiencePostType.Milestone]: { + label: 'Milestone', + icon: 'Flag', + color: 'var(--theme-accent-onion-default)', + }, + [ExperiencePostType.Publication]: { + label: 'Publication', + icon: 'Link', + color: 'var(--theme-accent-water-default)', + }, + [ExperiencePostType.Project]: { + label: 'Project', + icon: 'Terminal', + color: 'var(--theme-accent-cabbage-default)', + }, + [ExperiencePostType.Media]: { + label: 'Media', + icon: 'Play', + color: 'var(--theme-accent-cheese-default)', + }, + [ExperiencePostType.Achievement]: { + label: 'Achievement', + icon: 'Star', + color: 'var(--theme-accent-bun-default)', + }, + [ExperiencePostType.OpenSource]: { + label: 'Open Source', + icon: 'GitHub', + color: 'var(--theme-accent-blueCheese-default)', + }, +}; + +// Mock data generator +export const generateMockExperiencePosts = (): ExperiencePost[] => { + return [ + { + id: '1', + type: ExperiencePostType.Milestone, + title: 'Promoted to Senior Software Engineer', + description: + 'Recognized for technical leadership and mentoring contributions to the team.', + date: '2024-06-15', + milestone: 'Promotion', + image: + 'https://daily-now-res.cloudinary.com/image/upload/v1/placeholders/1', + }, + { + id: '2', + type: ExperiencePostType.Publication, + title: 'Building Scalable React Applications with Module Federation', + description: + 'A deep dive into micro-frontend architecture and how we scaled our platform.', + date: '2024-03-20', + url: 'https://engineering.example.com/module-federation', + publisher: 'Company Engineering Blog', + readTime: 12, + image: + 'https://daily-now-res.cloudinary.com/image/upload/v1/placeholders/2', + }, + { + id: '3', + type: ExperiencePostType.Project, + title: 'Platform Performance Optimization Initiative', + description: + 'Led a cross-functional team to improve page load times by 40% through code splitting and caching strategies.', + date: '2024-01-10', + technologies: ['React', 'Webpack', 'Redis', 'CloudFront'], + status: 'completed', + }, + { + id: '4', + type: ExperiencePostType.Media, + title: 'From Monolith to Microservices: Our Journey', + description: + 'Conference talk about our migration strategy and lessons learned.', + date: '2023-11-08', + url: 'https://youtube.com/watch?v=example', + mediaType: 'talk', + duration: 35, + venue: 'React Summit 2023', + image: + 'https://daily-now-res.cloudinary.com/image/upload/v1/placeholders/3', + }, + { + id: '5', + type: ExperiencePostType.Achievement, + title: 'AWS Solutions Architect Professional', + description: 'Achieved professional-level AWS certification.', + date: '2023-09-22', + issuer: 'Amazon Web Services', + credentialId: 'AWS-SAP-12345', + url: 'https://aws.amazon.com/verification', + image: + 'https://daily-now-res.cloudinary.com/image/upload/v1/placeholders/4', + }, + { + id: '6', + type: ExperiencePostType.OpenSource, + title: 'react-virtual-scroll', + description: + 'Created and maintain a high-performance virtualization library for React.', + date: '2023-07-15', + url: 'https://github.com/example/react-virtual-scroll', + repository: 'example/react-virtual-scroll', + stars: 1247, + contributions: 89, + }, + ]; +}; diff --git a/packages/shared/src/features/profile/components/experience/github-integration/ActivityChart.tsx b/packages/shared/src/features/profile/components/experience/github-integration/ActivityChart.tsx new file mode 100644 index 0000000000..cebcca3611 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/github-integration/ActivityChart.tsx @@ -0,0 +1,151 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import type { ActivityData } from './types'; + +interface ActivityChartProps { + data: ActivityData[]; + height?: number; +} + +const ACTIVITY_COLORS = { + commits: 'var(--theme-accent-onion-default)', + pullRequests: 'var(--theme-accent-water-default)', + reviews: 'var(--theme-accent-cheese-default)', + issues: 'var(--theme-accent-bacon-default)', +}; + +const formatDate = (dateStr: string): string => { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +}; + +interface TooltipPayloadItem { + value: number; + dataKey: string; + color: string; +} + +interface CustomTooltipProps { + active?: boolean; + payload?: TooltipPayloadItem[]; + label?: string; +} + +const CustomTooltip = ({ + active, + payload, + label, +}: CustomTooltipProps): ReactElement | null => { + if (!active || !payload?.length) { + return null; + } + + return ( +
+

{label}

+ {payload.map((entry) => ( +
+ + {entry.dataKey}: + {entry.value} +
+ ))} +
+ ); +}; + +export function ActivityChart({ + data, + height = 160, +}: ActivityChartProps): ReactElement { + const chartData = data.map((item) => ({ + ...item, + name: formatDate(item.date), + })); + + return ( +
+ + + + + + } cursor={false} /> + + + + + + + +
+ ); +} diff --git a/packages/shared/src/features/profile/components/experience/github-integration/ExperienceWithGitStats.tsx b/packages/shared/src/features/profile/components/experience/github-integration/ExperienceWithGitStats.tsx new file mode 100644 index 0000000000..a54d497127 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/github-integration/ExperienceWithGitStats.tsx @@ -0,0 +1,94 @@ +/** + * DEMO COMPONENT + * This is an example showing how GitIntegrationStats can be added to an experience item. + * For demo purposes only - showcases the collapsible GitHub/GitLab integration. + */ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../../../../components/typography/Typography'; +import { Image, ImageType } from '../../../../../components/image/Image'; +import { Pill, PillSize } from '../../../../../components/Pill'; +import { GitIntegrationStats } from './GitIntegrationStats'; +import { generateMockGitStats } from './types'; + +interface ExperienceWithGitStatsProps { + provider?: 'github' | 'gitlab'; +} + +/** + * Demo component showing an experience item with GitHub/GitLab integration + * Use this as reference for how to integrate into the actual UserExperienceItem + */ +export function ExperienceWithGitStats({ + provider = 'github', +}: ExperienceWithGitStatsProps): ReactElement { + // Generate mock data for demo + const gitData = generateMockGitStats(provider); + + return ( +
+ {/* Simulated experience header */} +
+ +
+
+ + Senior Software Engineer + + +
+ + Acme Technologies + + + Jan 2022 - Present | San Francisco, CA + + + {/* Description */} + + Building scalable infrastructure and developer tools. Leading the + platform team responsible for CI/CD pipelines and deployment + automation. + + + {/* Skills */} +
+ {['TypeScript', 'React', 'Node.js'].map((skill) => ( + + ))} +
+
+
+ + {/* GitHub/GitLab Integration - Collapsible section */} + +
+ ); +} diff --git a/packages/shared/src/features/profile/components/experience/github-integration/GitIntegrationStats.tsx b/packages/shared/src/features/profile/components/experience/github-integration/GitIntegrationStats.tsx new file mode 100644 index 0000000000..b9a05c393b --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/github-integration/GitIntegrationStats.tsx @@ -0,0 +1,280 @@ +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { + Typography, + TypographyType, + TypographyColor, + TypographyTag, +} from '../../../../../components/typography/Typography'; +import { + GitHubIcon, + GitLabIcon, + ArrowIcon, + LockIcon, + SourceIcon, +} from '../../../../../components/icons'; +import { IconSize } from '../../../../../components/Icon'; +import type { GitIntegrationData, Repository } from './types'; +import { LanguageBar } from './LanguageBar'; +import { ActivityChart } from './ActivityChart'; + +interface StatItemProps { + label: string; + value: number | string; +} + +function StatItem({ label, value }: StatItemProps): ReactElement { + return ( +
+ + {typeof value === 'number' ? value.toLocaleString() : value} + + + {label} + +
+ ); +} + +interface RepoItemProps { + repo: Repository; +} + +function RepoItem({ repo }: RepoItemProps): ReactElement { + return ( + +
+ + + {repo.name} + + {repo.isPrivate && ( + + )} +
+
+ + {repo.commits.toLocaleString()} commits + +
+ {repo.languages.slice(0, 3).map((lang) => ( + + ))} +
+
+
+ ); +} + +interface GitIntegrationStatsProps { + data: GitIntegrationData; + initiallyOpen?: boolean; +} + +export function GitIntegrationStats({ + data, + initiallyOpen = false, +}: GitIntegrationStatsProps): ReactElement { + const [isOpen, setIsOpen] = useState(initiallyOpen); + const [showAllRepos, setShowAllRepos] = useState(false); + + const ProviderIcon = data.provider === 'github' ? GitHubIcon : GitLabIcon; + const providerName = data.provider === 'github' ? 'GitHub' : 'GitLab'; + + const repoCount = data.repositories.length; + const visibleRepos = showAllRepos + ? data.repositories + : data.repositories.slice(0, 3); + const hiddenRepoCount = repoCount - 3; + + return ( +
+ {/* Header */} + + + {/* Collapsible content */} +
+
+ {/* Stats grid */} +
+ + + + +
+ + {/* Languages */} +
+ + Languages + + +
+ + {/* Activity chart */} +
+ + Activity (12 weeks) + + +
+ + {/* Repositories */} +
+ + Linked Repositories + +
+ {visibleRepos.map((repo, index) => ( +
0, + })} + > + +
+ ))} + {hiddenRepoCount > 0 && !showAllRepos && ( + + )} + {showAllRepos && repoCount > 3 && ( + + )} +
+
+ + {/* Streak */} +
+ + Current streak + + + {data.aggregatedStats.contributionStreak} days + +
+
+
+
+ ); +} diff --git a/packages/shared/src/features/profile/components/experience/github-integration/LanguageBar.tsx b/packages/shared/src/features/profile/components/experience/github-integration/LanguageBar.tsx new file mode 100644 index 0000000000..baa01838c8 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/github-integration/LanguageBar.tsx @@ -0,0 +1,67 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../../../../components/typography/Typography'; +import { Tooltip } from '../../../../../components/tooltip/Tooltip'; +import type { RepositoryLanguage } from './types'; + +interface LanguageBarProps { + languages: RepositoryLanguage[]; + showLegend?: boolean; +} + +export function LanguageBar({ + languages, + showLegend = true, +}: LanguageBarProps): ReactElement { + return ( +
+ {/* Progress bar */} +
+ {languages.map((lang) => ( + +
+ + ))} +
+ + {/* Legend */} + {showLegend && ( +
+ {languages.map((lang) => ( +
+ + + {lang.name} + + + {lang.percentage.toFixed(1)}% + +
+ ))} +
+ )} +
+ ); +} diff --git a/packages/shared/src/features/profile/components/experience/github-integration/index.ts b/packages/shared/src/features/profile/components/experience/github-integration/index.ts new file mode 100644 index 0000000000..3d47c8f9fd --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/github-integration/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export { LanguageBar } from './LanguageBar'; +export { ActivityChart } from './ActivityChart'; +export { GitIntegrationStats } from './GitIntegrationStats'; +export { ExperienceWithGitStats } from './ExperienceWithGitStats'; diff --git a/packages/shared/src/features/profile/components/experience/github-integration/types.ts b/packages/shared/src/features/profile/components/experience/github-integration/types.ts new file mode 100644 index 0000000000..b60195bd45 --- /dev/null +++ b/packages/shared/src/features/profile/components/experience/github-integration/types.ts @@ -0,0 +1,244 @@ +/** + * Mock types for GitHub/GitLab integration + * These represent the data we would fetch from GitHub/GitLab APIs + */ + +export interface RepositoryLanguage { + name: string; + percentage: number; + color: string; +} + +export interface ActivityData { + date: string; + commits: number; + pullRequests: number; + reviews: number; + issues: number; +} + +export interface Repository { + name: string; + url: string; + commits: number; + pullRequests: number; + reviews: number; + issues: number; + languages: RepositoryLanguage[]; + isPrivate?: boolean; +} + +export interface AggregatedStats { + totalCommits: number; + totalPullRequests: number; + totalReviews: number; + totalIssues: number; + languages: RepositoryLanguage[]; + activityHistory: ActivityData[]; + contributionStreak: number; + lastActivityDate: string; +} + +export interface GitIntegrationData { + provider: 'github' | 'gitlab'; + username: string; + repositories: Repository[]; + aggregatedStats: AggregatedStats; + connected: boolean; + lastSynced?: string; +} + +// Language colors for common languages +const LANGUAGE_COLORS: Record = { + TypeScript: '#3178c6', + JavaScript: '#f7df1e', + Python: '#3572A5', + Go: '#00ADD8', + Rust: '#dea584', + Java: '#b07219', + 'C++': '#f34b7d', + C: '#555555', + Ruby: '#701516', + PHP: '#4F5D95', + Swift: '#F05138', + Kotlin: '#A97BFF', + CSS: '#563d7c', + HTML: '#e34c26', + Shell: '#89e051', + Dockerfile: '#384d54', + Other: '#8b8b8b', +}; + +// Aggregate languages from multiple repos into combined percentages +const aggregateLanguages = (repos: Repository[]): RepositoryLanguage[] => { + const languageTotals: Record = {}; + let totalWeight = 0; + + repos.forEach((repo) => { + // Weight by repo activity (commits) + const repoWeight = repo.commits; + totalWeight += repoWeight; + + repo.languages.forEach((lang) => { + const contribution = (lang.percentage / 100) * repoWeight; + languageTotals[lang.name] = + (languageTotals[lang.name] || 0) + contribution; + }); + }); + + // Convert to percentages and sort + const languages = Object.entries(languageTotals) + .map(([name, weight]) => ({ + name, + percentage: totalWeight > 0 ? (weight / totalWeight) * 100 : 0, + color: LANGUAGE_COLORS[name] || LANGUAGE_COLORS.Other, + })) + .sort((a, b) => b.percentage - a.percentage); + + // Keep top 5, group rest as "Other" + if (languages.length > 5) { + const top5 = languages.slice(0, 5); + const otherPercentage = languages + .slice(5) + .reduce((sum, l) => sum + l.percentage, 0); + + if (otherPercentage > 0) { + const existingOther = top5.find((l) => l.name === 'Other'); + if (existingOther) { + existingOther.percentage += otherPercentage; + } else { + top5.push({ + name: 'Other', + percentage: otherPercentage, + color: LANGUAGE_COLORS.Other, + }); + } + } + return top5; + } + + return languages; +}; + +// Generate mock activity history +const generateActivityHistory = (): ActivityData[] => { + const activityHistory: ActivityData[] = []; + const now = new Date(); + + for (let i = 11; i >= 0; i -= 1) { + const date = new Date(now); + date.setDate(date.getDate() - i * 7); + activityHistory.push({ + date: date.toISOString().split('T')[0], + commits: Math.floor(Math.random() * 45) + 10, + pullRequests: Math.floor(Math.random() * 15) + 2, + reviews: Math.floor(Math.random() * 20) + 5, + issues: Math.floor(Math.random() * 8) + 1, + }); + } + + return activityHistory; +}; + +// Mock data generator for demo purposes - now with multiple repos +export const generateMockGitStats = ( + provider: 'github' | 'gitlab' = 'github', +): GitIntegrationData => { + // Simulate multiple repositories for a company + const repositories: Repository[] = [ + { + name: 'webapp', + url: `https://${provider}.com/acme/webapp`, + commits: 847, + pullRequests: 56, + reviews: 89, + issues: 23, + languages: [ + { name: 'TypeScript', percentage: 65, color: '#3178c6' }, + { name: 'CSS', percentage: 20, color: '#563d7c' }, + { name: 'JavaScript', percentage: 10, color: '#f7df1e' }, + { name: 'HTML', percentage: 5, color: '#e34c26' }, + ], + }, + { + name: 'api-gateway', + url: `https://${provider}.com/acme/api-gateway`, + commits: 423, + pullRequests: 34, + reviews: 67, + issues: 12, + languages: [ + { name: 'Go', percentage: 85, color: '#00ADD8' }, + { name: 'Dockerfile', percentage: 10, color: '#384d54' }, + { name: 'Shell', percentage: 5, color: '#89e051' }, + ], + }, + { + name: 'shared-components', + url: `https://${provider}.com/acme/shared-components`, + commits: 312, + pullRequests: 28, + reviews: 45, + issues: 8, + languages: [ + { name: 'TypeScript', percentage: 80, color: '#3178c6' }, + { name: 'CSS', percentage: 15, color: '#563d7c' }, + { name: 'JavaScript', percentage: 5, color: '#f7df1e' }, + ], + }, + { + name: 'ml-pipeline', + url: `https://${provider}.com/acme/ml-pipeline`, + commits: 189, + pullRequests: 15, + reviews: 22, + issues: 6, + languages: [ + { name: 'Python', percentage: 90, color: '#3572A5' }, + { name: 'Shell', percentage: 7, color: '#89e051' }, + { name: 'Dockerfile', percentage: 3, color: '#384d54' }, + ], + }, + { + name: 'infrastructure', + url: `https://${provider}.com/acme/infrastructure`, + commits: 156, + pullRequests: 12, + reviews: 18, + issues: 4, + isPrivate: true, + languages: [ + { name: 'Shell', percentage: 45, color: '#89e051' }, + { name: 'Python', percentage: 35, color: '#3572A5' }, + { name: 'Dockerfile', percentage: 20, color: '#384d54' }, + ], + }, + ]; + + // Aggregate stats across all repos + const totalCommits = repositories.reduce((sum, r) => sum + r.commits, 0); + const totalPullRequests = repositories.reduce( + (sum, r) => sum + r.pullRequests, + 0, + ); + const totalReviews = repositories.reduce((sum, r) => sum + r.reviews, 0); + const totalIssues = repositories.reduce((sum, r) => sum + r.issues, 0); + + return { + provider, + username: 'johndoe', + repositories, + connected: true, + lastSynced: new Date().toISOString(), + aggregatedStats: { + totalCommits, + totalPullRequests, + totalReviews, + totalIssues, + languages: aggregateLanguages(repositories), + activityHistory: generateActivityHistory(), + contributionStreak: 23, + lastActivityDate: new Date().toISOString(), + }, + }; +}; diff --git a/packages/storybook/stories/components/MultiSourceHeatmap.stories.tsx b/packages/storybook/stories/components/MultiSourceHeatmap.stories.tsx new file mode 100644 index 0000000000..a7f1c0b003 --- /dev/null +++ b/packages/storybook/stories/components/MultiSourceHeatmap.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import { subDays } from 'date-fns'; +import { + MultiSourceHeatmap, + ActivityOverviewCard, + generateMockMultiSourceActivity, +} from '@dailydotdev/shared/src/components/MultiSourceHeatmap'; + +const meta: Meta = { + title: 'Components/MultiSourceHeatmap', + parameters: { + layout: 'padded', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +const endDate = new Date(); +const startDate = subDays(endDate, 365); +const mockActivities = generateMockMultiSourceActivity(startDate, endDate); + +/** + * The full activity overview card with collapsible heatmap. + * Aggregates data from GitHub, GitLab, daily.dev, and more. + */ +export const ActivityCard: StoryObj = { + render: () => , +}; + +/** + * Activity card in collapsed state - shows quick stats preview. + */ +export const ActivityCardCollapsed: StoryObj = { + render: () => , +}; + +/** + * Standalone heatmap component with all features. + */ +export const HeatmapFull: StoryObj = { + render: () => ( + + ), +}; + +/** + * Heatmap without stats - just the grid and legend. + */ +export const HeatmapNoStats: StoryObj = { + render: () => ( + + ), +}; + +/** + * Minimal heatmap - just the grid. + */ +export const HeatmapMinimal: StoryObj = { + render: () => ( + + ), +}; + +/** + * 6-month view of activity. + */ +export const SixMonthView: StoryObj = { + render: () => { + const sixMonthStart = subDays(endDate, 180); + const sixMonthActivities = generateMockMultiSourceActivity( + sixMonthStart, + endDate, + ); + return ( + + ); + }, +}; + +/** + * 3-month view - compact. + */ +export const ThreeMonthView: StoryObj = { + render: () => { + const threeMonthStart = subDays(endDate, 90); + const threeMonthActivities = generateMockMultiSourceActivity( + threeMonthStart, + endDate, + ); + return ( + + ); + }, +}; diff --git a/packages/storybook/stories/features/profile/ExperiencePosts.stories.tsx b/packages/storybook/stories/features/profile/ExperiencePosts.stories.tsx new file mode 100644 index 0000000000..33d4b202b2 --- /dev/null +++ b/packages/storybook/stories/features/profile/ExperiencePosts.stories.tsx @@ -0,0 +1,190 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import { + ExperiencePostsSection, + ExperiencePostItem, + ExperienceTimeline, + generateMockExperiencePosts, + ExperiencePostType, +} from '@dailydotdev/shared/src/features/profile/components/experience/experience-posts'; +import type { ExperiencePost } from '@dailydotdev/shared/src/features/profile/components/experience/experience-posts'; + +const meta: Meta = { + title: 'Features/Profile/ExperiencePosts', + parameters: { + layout: 'padded', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +const mockPosts = generateMockExperiencePosts(); + +/** + * Full experience posts section with all post types. + * Features filter tabs for different categories. + */ +export const AllPosts: StoryObj = { + render: () => , +}; + +/** + * Collapsed state showing preview counts. + */ +export const Collapsed: StoryObj = { + render: () => ( + + ), +}; + +/** + * Single milestone post item. + */ +export const MilestonePost: StoryObj = { + render: () => { + const post = mockPosts.find( + (p) => p.type === ExperiencePostType.Milestone, + ) as ExperiencePost; + return ( +
+ +
+ ); + }, +}; + +/** + * Single publication post item. + */ +export const PublicationPost: StoryObj = { + render: () => { + const post = mockPosts.find( + (p) => p.type === ExperiencePostType.Publication, + ) as ExperiencePost; + return ( +
+ +
+ ); + }, +}; + +/** + * Single project post item with technologies. + */ +export const ProjectPost: StoryObj = { + render: () => { + const post = mockPosts.find( + (p) => p.type === ExperiencePostType.Project, + ) as ExperiencePost; + return ( +
+ +
+ ); + }, +}; + +/** + * Single media post item (talk/video). + */ +export const MediaPost: StoryObj = { + render: () => { + const post = mockPosts.find( + (p) => p.type === ExperiencePostType.Media, + ) as ExperiencePost; + return ( +
+ +
+ ); + }, +}; + +/** + * Single achievement post item (certification). + */ +export const AchievementPost: StoryObj = { + render: () => { + const post = mockPosts.find( + (p) => p.type === ExperiencePostType.Achievement, + ) as ExperiencePost; + return ( +
+ +
+ ); + }, +}; + +/** + * Single open source post item with stars. + */ +export const OpenSourcePost: StoryObj = { + render: () => { + const post = mockPosts.find( + (p) => p.type === ExperiencePostType.OpenSource, + ) as ExperiencePost; + return ( +
+ +
+ ); + }, +}; + +/** + * Section with only a few posts (no "show more"). + */ +export const FewPosts: StoryObj = { + render: () => ( + + ), +}; + +/** + * Section with single post type (no filter tabs). + */ +export const SingleType: StoryObj = { + render: () => { + const milestonesOnly = mockPosts.filter( + (p) => p.type === ExperiencePostType.Milestone, + ); + return ; + }, +}; + +// ============================================ +// TIMELINE VIEW (New funky design!) +// ============================================ + +/** + * Timeline view - The new funky design with vertical timeline, + * colored nodes, connecting lines, and hover effects. + */ +export const Timeline: StoryObj = { + render: () => , +}; + +/** + * Timeline collapsed state - Shows mini colored dots preview. + */ +export const TimelineCollapsed: StoryObj = { + render: () => , +}; + +/** + * Timeline with fewer posts - No "show more" button needed. + */ +export const TimelineFewPosts: StoryObj = { + render: () => ( + + ), +}; diff --git a/packages/storybook/stories/features/profile/GitIntegration.stories.tsx b/packages/storybook/stories/features/profile/GitIntegration.stories.tsx new file mode 100644 index 0000000000..a74f6d71a9 --- /dev/null +++ b/packages/storybook/stories/features/profile/GitIntegration.stories.tsx @@ -0,0 +1,118 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import { + GitIntegrationStats, + LanguageBar, + ActivityChart, + ExperienceWithGitStats, + generateMockGitStats, +} from '@dailydotdev/shared/src/features/profile/components/experience/github-integration'; + +const meta: Meta = { + title: 'Features/Profile/GitIntegration', + parameters: { + layout: 'padded', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +/** + * Full GitHub integration stats in a collapsible section. + * This is the main component to use for displaying GitHub/GitLab activity. + */ +export const GitHubStats: StoryObj = { + render: () => { + const mockData = generateMockGitStats('github'); + return ; + }, +}; + +/** + * GitLab variant of the integration stats. + */ +export const GitLabStats: StoryObj = { + render: () => { + const mockData = generateMockGitStats('gitlab'); + return ; + }, +}; + +/** + * Collapsed state - shows preview stats. + * Click to expand and see full details. + */ +export const Collapsed: StoryObj = { + render: () => { + const mockData = generateMockGitStats('github'); + return ; + }, +}; + +/** + * Language percentage bar standalone component. + * Shows programming language distribution like on GitHub. + */ +export const LanguageBarOnly: StoryObj = { + render: () => { + const mockData = generateMockGitStats('github'); + return ; + }, +}; + +/** + * Language bar without legend - more compact view. + */ +export const LanguageBarCompact: StoryObj = { + render: () => { + const mockData = generateMockGitStats('github'); + return ; + }, +}; + +/** + * Activity chart standalone component. + * Shows commits, PRs, reviews, and issues over time. + */ +export const ActivityChartOnly: StoryObj = { + render: () => { + const mockData = generateMockGitStats('github'); + return ; + }, +}; + +/** + * Full experience card with GitHub integration. + * This demonstrates how the GitIntegrationStats component + * would look when integrated into an actual experience item. + */ +export const ExperienceWithGitHub: StoryObj = { + render: () => , +}; + +/** + * Full experience card with GitLab integration. + */ +export const ExperienceWithGitLab: StoryObj = { + render: () => , +}; + +/** + * Multiple experiences stacked. + * Shows how multiple experiences with different integrations would look. + */ +export const MultipleExperiences: StoryObj = { + render: () => ( +
+ + +
+ ), +}; From c5c392e2f131e548f163b4027567a4fbd77e264a Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 6 Jan 2026 13:30:42 +0200 Subject: [PATCH 2/3] fix: more mocks, go wild --- .../components/mocks/CurrentlySection.tsx | 218 +++++++++++ .../components/mocks/LearningJourney.tsx | 262 +++++++++++++ .../components/mocks/OpenSourceImpact.tsx | 196 ++++++++++ .../components/mocks/SetupShowcase.tsx | 346 ++++++++++++++++++ .../profile/components/mocks/StackDNA.tsx | 218 +++++++++++ .../profile/components/mocks/index.ts | 5 + packages/webapp/pages/[userId]/index.tsx | 45 +++ 7 files changed, 1290 insertions(+) create mode 100644 packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx create mode 100644 packages/shared/src/features/profile/components/mocks/LearningJourney.tsx create mode 100644 packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx create mode 100644 packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx create mode 100644 packages/shared/src/features/profile/components/mocks/StackDNA.tsx create mode 100644 packages/shared/src/features/profile/components/mocks/index.ts diff --git a/packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx b/packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx new file mode 100644 index 0000000000..f170277e02 --- /dev/null +++ b/packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx @@ -0,0 +1,218 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyType, + TypographyTag, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { IconSize } from '../../../../components/Icon'; +import { + TerminalIcon, + BookmarkIcon, + SpotifyIcon, + EarthIcon, +} from '../../../../components/icons'; + +interface CurrentlyItemProps { + icon: ReactElement; + label: string; + value: string; + subtext?: string; + link?: string; + accentColor?: string; +} + +const CurrentlyItem = ({ + icon, + label, + value, + subtext, + link, + accentColor = 'bg-surface-float', +}: CurrentlyItemProps): ReactElement => { + const content = ( +
+
+ {icon} +
+
+ {label} + + {value} + + {subtext && ( + {subtext} + )} +
+
+ ); + + if (link) { + return ( + + {content} + + ); + } + + return content; +}; + +interface StatusBadgeProps { + status: 'available' | 'busy' | 'away'; + text: string; +} + +const StatusBadge = ({ status, text }: StatusBadgeProps): ReactElement => { + const statusStyles = { + available: 'bg-status-success/20 text-status-success', + busy: 'bg-status-error/20 text-status-error', + away: 'bg-status-warning/20 text-status-warning', + }; + + const dotStyles = { + available: 'bg-status-success', + busy: 'bg-status-error', + away: 'bg-status-warning', + }; + + return ( +
+ + {text} +
+ ); +}; + +export const CurrentlySection = (): ReactElement => { + return ( +
+
+ + Currently + + +
+ +
+ {/* Working On */} + + } + label="Building" + value="daily.dev v4.0" + subtext="New profile experience" + /> + + {/* Listening To */} + + } + label="Listening to" + value="Lo-fi Beats" + subtext="Chillhop Music" + link="https://open.spotify.com" + accentColor="bg-status-success/5" + /> + + {/* Reading */} + + } + label="Reading" + value="Designing Data-Intensive Apps" + subtext="Chapter 7: Transactions" + /> + + {/* Location */} + + } + label="Based in" + value="Tel Aviv, Israel" + subtext="GMT+3 • 2:34 PM local time" + /> +
+ + {/* What I'm thinking about */} +
+ + On my mind + +
+

+ “Exploring how we can make developer profiles more meaningful + than just a list of skills. What if your profile actually showed how + you think, learn, and grow as a developer?” +

+ + Updated 2 hours ago + +
+
+ + {/* Quick Links / Social Presence */} +
+ + Find me elsewhere + +
+ {[ + { label: 'Blog', url: '#', active: true }, + { label: 'Newsletter', url: '#', active: true }, + { label: 'YouTube', url: '#', active: false }, + { label: 'Podcast', url: '#', active: true }, + ].map((link) => ( + + {link.label} + {link.active && ( + + )} + + ))} +
+
+
+ ); +}; diff --git a/packages/shared/src/features/profile/components/mocks/LearningJourney.tsx b/packages/shared/src/features/profile/components/mocks/LearningJourney.tsx new file mode 100644 index 0000000000..f71cef47c0 --- /dev/null +++ b/packages/shared/src/features/profile/components/mocks/LearningJourney.tsx @@ -0,0 +1,262 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyType, + TypographyTag, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { IconSize } from '../../../../components/Icon'; +import { + BookmarkIcon, + StarIcon, + TrendingIcon, + LinkIcon, +} from '../../../../components/icons'; + +interface LearningGoalProps { + title: string; + progress: number; + articlesRead: number; + target: number; +} + +const LearningGoal = ({ + title, + progress, + articlesRead, + target, +}: LearningGoalProps): ReactElement => ( +
+
+ + {title} + + + {articlesRead}/{target} articles + +
+
+
+
+
+); + +interface SavedArticleProps { + title: string; + source: string; + readTime: number; + tags: string[]; +} + +const SavedArticle = ({ + title, + source, + readTime, + tags, +}: SavedArticleProps): ReactElement => ( +
+ + {title} + +
+ {source} + + {readTime} min read +
+
+ {tags.map((tag) => ( + + #{tag} + + ))} +
+
+); + +interface TopicExpertiseProps { + topic: string; + level: number; + articles: number; +} + +const TopicExpertise = ({ + topic, + level, + articles, +}: TopicExpertiseProps): ReactElement => { + const levelLabels = ['Beginner', 'Intermediate', 'Advanced', 'Expert']; + const levelColors = [ + 'bg-text-quaternary', + 'bg-status-warning', + 'bg-brand-default', + 'bg-status-success', + ]; + + return ( +
+
+ + {topic} + +
+ {[0, 1, 2, 3].map((i) => ( +
+ ))} +
+
+
+ + {levelLabels[level - 1]} + + + {articles} articles + +
+
+ ); +}; + +export const LearningJourney = (): ReactElement => { + return ( +
+ + Learning Journey + + + {/* Current Learning Goals */} +
+
+ + + Learning Goals + +
+
+ + +
+
+ + {/* Knowledge Areas */} +
+
+ + + Knowledge Areas + +
+
+ + + + + + +
+
+ + {/* Reading List / Saved Articles */} +
+
+
+ + + Reading List + +
+ + View all (47) + + +
+
+ + + +
+
+ + {/* Reading Stats */} +
+
+ 847 + + Articles read + +
+
+
+ 142h + Reading time +
+
+
+ 23 + + Topics explored + +
+
+
+ ); +}; diff --git a/packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx b/packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx new file mode 100644 index 0000000000..b683c93321 --- /dev/null +++ b/packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx @@ -0,0 +1,196 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyType, + TypographyTag, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { IconSize } from '../../../../components/Icon'; +import { + GitHubIcon, + StarIcon, + MergeIcon, + LinkIcon, +} from '../../../../components/icons'; + +interface ContributionProps { + repo: string; + owner: string; + description: string; + stars: number; + prs: number; + contributorRole: 'maintainer' | 'contributor' | 'author'; +} + +const ContributionCard = ({ + repo, + owner, + description, + stars, + prs, + contributorRole, +}: ContributionProps): ReactElement => { + const roleStyles = { + maintainer: 'bg-status-success text-white', + contributor: 'bg-brand-default text-white', + author: 'bg-accent-bacon-default text-white', + }; + + return ( +
+
+
+ +
+ + {repo} + + {owner} +
+
+ + {contributorRole} + +
+

+ {description} +

+
+
+ + {stars.toLocaleString()} +
+
+ + {prs} PRs merged +
+
+
+ ); +}; + +interface StatBoxProps { + value: string | number; + label: string; + icon: ReactElement; +} + +const StatBox = ({ value, label, icon }: StatBoxProps): ReactElement => ( +
+ {icon} + {value} + {label} +
+); + +export const OpenSourceImpact = (): ReactElement => { + return ( +
+
+ + Open Source Impact + + + + @idoshamun + + +
+ + {/* Impact Stats */} +
+ } + /> + } + /> + } + /> +
+ + {/* Top Contributions */} +
+ + Top Contributions + +
+ + + +
+
+ + {/* Contribution Activity */} +
+ + This Year + +
+
+ + 847 + +
+
+ + Contributions in 2024 + + + Top 5% most active contributor + +
+
+
+
+ ); +}; diff --git a/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx b/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx new file mode 100644 index 0000000000..5149222df7 --- /dev/null +++ b/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx @@ -0,0 +1,346 @@ +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; +import { + Typography, + TypographyType, + TypographyTag, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { IconSize } from '../../../../components/Icon'; +import { + TerminalIcon, + SettingsIcon, + LinkIcon, + CameraIcon, +} from '../../../../components/icons'; + +interface SetupImageProps { + src: string; + alt: string; + caption?: string; +} + +const SetupImage = ({ src, alt, caption }: SetupImageProps): ReactElement => ( +
+
+ {alt} +
+ {caption && ( +
+ {caption} +
+ )} +
+); + +interface ToolItemProps { + name: string; + category: string; + iconUrl?: string; + iconBg?: string; +} + +const ToolItem = ({ + name, + category, + iconUrl, + iconBg = 'bg-surface-secondary', +}: ToolItemProps): ReactElement => ( +
+
+ {iconUrl ? ( + {name} + ) : ( + + )} +
+
+ {name} + {category} +
+
+); + +type TabType = 'workspace' | 'terminal' | 'tools'; + +export const SetupShowcase = (): ReactElement => { + const [activeTab, setActiveTab] = useState('workspace'); + + const tabs: { id: TabType; label: string; icon: ReactElement }[] = [ + { + id: 'workspace', + label: 'Workspace', + icon: , + }, + { + id: 'terminal', + label: 'Terminal', + icon: , + }, + { + id: 'tools', + label: 'Tools', + icon: , + }, + ]; + + return ( +
+ + My Setup + + + {/* Tab Navigation */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Workspace Tab */} + {activeTab === 'workspace' && ( +
+ +
+ + +
+
+ + Gear Highlights + +
+ {[ + 'MacBook Pro 16"', + 'LG 27" 4K Monitor', + 'Keychron K2', + 'Logitech MX Master 3', + 'Elgato Wave:3', + 'Herman Miller Aeron', + ].map((item) => ( + + {item} + + ))} +
+
+
+ )} + + {/* Terminal Tab */} + {activeTab === 'terminal' && ( +
+ {/* Mock Terminal Screenshot */} +
+
+
+ + + +
+ + ~ zsh — idoshamun@macbook + +
+
+
❯ neofetch
+
+
+                  {`    .:'
+  _ :'_
+.'  \`' \`.
+:  ._   :
+.  : ;_  ;
+\`. ^^/ .'
+ \`'--'\``}
+                
+
+
+ OS: macOS 14.2 + Sonoma +
+
+ Shell: zsh 5.9 +
+
+ Terminal: WezTerm +
+
+ Editor: Neovim +
+
+ Theme: Tokyo Night +
+
+ Font: JetBrains Mono +
+
+
+
+ ❯ _ +
+
+
+ + {/* Dotfiles Link */} + +
+ +
+ + My Dotfiles + + + github.com/idoshamun/dotfiles + +
+
+ +
+
+ )} + + {/* Tools Tab */} + {activeTab === 'tools' && ( +
+
+ + Development + +
+ + + + +
+
+ +
+ + Productivity + +
+ + + + +
+
+ +
+ + Design + +
+ + +
+
+
+ )} +
+ ); +}; diff --git a/packages/shared/src/features/profile/components/mocks/StackDNA.tsx b/packages/shared/src/features/profile/components/mocks/StackDNA.tsx new file mode 100644 index 0000000000..64c791e448 --- /dev/null +++ b/packages/shared/src/features/profile/components/mocks/StackDNA.tsx @@ -0,0 +1,218 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyType, + TypographyTag, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { IconSize } from '../../../../components/Icon'; +import { + TerminalIcon, + AiIcon, + ShieldIcon, + AppIcon, + CodePenIcon, +} from '../../../../components/icons'; + +interface StackItemProps { + icon: ReactElement; + name: string; + level?: 'expert' | 'proficient' | 'learning'; + years?: number; +} + +const StackItem = ({ + icon, + name, + level = 'proficient', + years, +}: StackItemProps): ReactElement => { + const levelColors = { + expert: 'text-status-success', + proficient: 'text-brand-default', + learning: 'text-text-quaternary', + }; + + return ( +
+ {icon} + {name} + {years && ( + {years}y + )} + + {level} + +
+ ); +}; + +interface PassionTagProps { + label: string; + icon: ReactElement; +} + +const PassionTag = ({ label, icon }: PassionTagProps): ReactElement => ( +
+ {icon} + {label} +
+); + +export const StackDNA = (): ReactElement => { + return ( +
+ + Stack & Tools DNA + + + {/* Primary Stack */} +
+ + Primary Stack + +
+ } + name="TypeScript" + level="expert" + years={5} + /> + } + name="React" + level="expert" + years={6} + /> + } + name="Node.js" + level="proficient" + years={4} + /> + } + name="PostgreSQL" + level="proficient" + years={3} + /> +
+
+ + {/* Currently Learning */} +
+ + Currently Learning + +
+ } + name="Rust" + level="learning" + /> + } + name="Go" + level="learning" + /> +
+
+ + {/* Hot Takes */} +
+ + Hot Takes + +
+
+ +
+ + Favorite + + + TypeScript + React + Tailwind - the holy trinity + +
+
+
+
+ +
+ + Would Rather Not + + + PHP - sorry, I've moved on. Also XML configs can stay in + 2010. + +
+
+
+
+ +
+ + Guilty Pleasure + + + Still write bash scripts for everything. No regrets. + +
+
+
+
+ + {/* Passions & Interests */} +
+ + Passions & Interests + +
+ } + /> + } + /> + } + /> + } + /> + } + /> +
+
+
+ ); +}; diff --git a/packages/shared/src/features/profile/components/mocks/index.ts b/packages/shared/src/features/profile/components/mocks/index.ts new file mode 100644 index 0000000000..75fe37e9b5 --- /dev/null +++ b/packages/shared/src/features/profile/components/mocks/index.ts @@ -0,0 +1,5 @@ +export { StackDNA } from './StackDNA'; +export { OpenSourceImpact } from './OpenSourceImpact'; +export { LearningJourney } from './LearningJourney'; +export { CurrentlySection } from './CurrentlySection'; +export { SetupShowcase } from './SetupShowcase'; diff --git a/packages/webapp/pages/[userId]/index.tsx b/packages/webapp/pages/[userId]/index.tsx index 5433ef323a..0889fcad43 100644 --- a/packages/webapp/pages/[userId]/index.tsx +++ b/packages/webapp/pages/[userId]/index.tsx @@ -23,6 +23,7 @@ import { Header } from '@dailydotdev/shared/src/components/profile/Header'; import classNames from 'classnames'; import { ProfileCompletion } from '@dailydotdev/shared/src/features/profile/components/ProfileWidgets/ProfileCompletion'; import { Share } from '@dailydotdev/shared/src/features/profile/components/ProfileWidgets/Share'; +import dynamic from 'next/dynamic'; import { getLayout as getProfileLayout, getProfileSeoDefaults, @@ -31,6 +32,43 @@ import { } from '../../components/layouts/ProfileLayout'; import type { ProfileLayoutProps } from '../../components/layouts/ProfileLayout'; +// Dynamically import mock components to avoid SSR issues +const CurrentlySection = dynamic( + () => + import( + '@dailydotdev/shared/src/features/profile/components/mocks/CurrentlySection' + ).then((mod) => mod.CurrentlySection), + { ssr: false }, +); +const StackDNA = dynamic( + () => + import( + '@dailydotdev/shared/src/features/profile/components/mocks/StackDNA' + ).then((mod) => mod.StackDNA), + { ssr: false }, +); +const OpenSourceImpact = dynamic( + () => + import( + '@dailydotdev/shared/src/features/profile/components/mocks/OpenSourceImpact' + ).then((mod) => mod.OpenSourceImpact), + { ssr: false }, +); +const LearningJourney = dynamic( + () => + import( + '@dailydotdev/shared/src/features/profile/components/mocks/LearningJourney' + ).then((mod) => mod.LearningJourney), + { ssr: false }, +); +const SetupShowcase = dynamic( + () => + import( + '@dailydotdev/shared/src/features/profile/components/mocks/SetupShowcase' + ).then((mod) => mod.SetupShowcase), + { ssr: false }, +); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const ProfilePage = ({ user: initialUser, @@ -81,6 +119,13 @@ const ProfilePage = ({ )} {!shouldShowBanner &&
} + {/* Mock Profile Sections - for preview/demo purposes */} + + + + + + {/* End Mock Profile Sections */} {isUserSame && ( From c786ff43e87c03eaa066efa1351a08d58da3a9b4 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 8 Jan 2026 10:25:26 +0200 Subject: [PATCH 3/3] fix: some more mocking --- .../experience/UserExperienceItem.tsx | 23 -- .../components/mocks/CurrentlySection.tsx | 218 ------------- .../components/mocks/LearningJourney.tsx | 262 --------------- .../components/mocks/OpenSourceImpact.tsx | 196 ----------- .../components/mocks/SetupShowcase.tsx | 305 ++++++++---------- .../profile/components/mocks/StackDNA.tsx | 211 ++++++------ .../profile/components/mocks/index.ts | 3 - .../layouts/ProfileLayout/index.tsx | 21 +- .../webapp/hooks/useProfileSidebarCollapse.ts | 56 ---- packages/webapp/pages/[userId]/index.tsx | 52 --- 10 files changed, 237 insertions(+), 1110 deletions(-) delete mode 100644 packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx delete mode 100644 packages/shared/src/features/profile/components/mocks/LearningJourney.tsx delete mode 100644 packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx delete mode 100644 packages/webapp/hooks/useProfileSidebarCollapse.ts diff --git a/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx b/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx index d36d1e07bf..9192991540 100644 --- a/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx +++ b/packages/shared/src/features/profile/components/experience/UserExperienceItem.tsx @@ -33,22 +33,9 @@ import { LazyModal } from '../../../../components/modals/common/types'; import { IconSize } from '../../../../components/Icon'; import { VerifiedBadge } from './VerifiedBadge'; import type { TLocation } from '../../../../graphql/autocomplete'; -import { - GitIntegrationStats, - generateMockGitStats, -} from './github-integration'; -import { - ExperienceTimeline, - generateMockExperiencePosts, -} from './experience-posts'; -import { ActivityOverviewCard } from '../../../../components/MultiSourceHeatmap'; const MAX_SKILLS = 3; -// TODO: Remove these mocks - for demo purposes only -const MOCK_GIT_DATA = generateMockGitStats('github'); -const MOCK_EXPERIENCE_POSTS = generateMockExperiencePosts(); - const getDisplayLocation = ( grouped: boolean | undefined, locationType: number | null | undefined, @@ -285,16 +272,6 @@ export function UserExperienceItem({ )}
)} - {/* GitHub/GitLab Integration - Mock for demo */} - {isWorkExperience && !grouped && ( - - )} - {/* Experience Posts Timeline - Mock for demo */} - {isWorkExperience && !grouped && ( - - )} - {/* Multi-source Activity Heatmap - Mock for demo */} - {isWorkExperience && !grouped && }
); diff --git a/packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx b/packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx deleted file mode 100644 index f170277e02..0000000000 --- a/packages/shared/src/features/profile/components/mocks/CurrentlySection.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import { - Typography, - TypographyType, - TypographyTag, - TypographyColor, -} from '../../../../components/typography/Typography'; -import { IconSize } from '../../../../components/Icon'; -import { - TerminalIcon, - BookmarkIcon, - SpotifyIcon, - EarthIcon, -} from '../../../../components/icons'; - -interface CurrentlyItemProps { - icon: ReactElement; - label: string; - value: string; - subtext?: string; - link?: string; - accentColor?: string; -} - -const CurrentlyItem = ({ - icon, - label, - value, - subtext, - link, - accentColor = 'bg-surface-float', -}: CurrentlyItemProps): ReactElement => { - const content = ( -
-
- {icon} -
-
- {label} - - {value} - - {subtext && ( - {subtext} - )} -
-
- ); - - if (link) { - return ( - - {content} - - ); - } - - return content; -}; - -interface StatusBadgeProps { - status: 'available' | 'busy' | 'away'; - text: string; -} - -const StatusBadge = ({ status, text }: StatusBadgeProps): ReactElement => { - const statusStyles = { - available: 'bg-status-success/20 text-status-success', - busy: 'bg-status-error/20 text-status-error', - away: 'bg-status-warning/20 text-status-warning', - }; - - const dotStyles = { - available: 'bg-status-success', - busy: 'bg-status-error', - away: 'bg-status-warning', - }; - - return ( -
- - {text} -
- ); -}; - -export const CurrentlySection = (): ReactElement => { - return ( -
-
- - Currently - - -
- -
- {/* Working On */} - - } - label="Building" - value="daily.dev v4.0" - subtext="New profile experience" - /> - - {/* Listening To */} - - } - label="Listening to" - value="Lo-fi Beats" - subtext="Chillhop Music" - link="https://open.spotify.com" - accentColor="bg-status-success/5" - /> - - {/* Reading */} - - } - label="Reading" - value="Designing Data-Intensive Apps" - subtext="Chapter 7: Transactions" - /> - - {/* Location */} - - } - label="Based in" - value="Tel Aviv, Israel" - subtext="GMT+3 • 2:34 PM local time" - /> -
- - {/* What I'm thinking about */} -
- - On my mind - -
-

- “Exploring how we can make developer profiles more meaningful - than just a list of skills. What if your profile actually showed how - you think, learn, and grow as a developer?” -

- - Updated 2 hours ago - -
-
- - {/* Quick Links / Social Presence */} -
- - Find me elsewhere - -
- {[ - { label: 'Blog', url: '#', active: true }, - { label: 'Newsletter', url: '#', active: true }, - { label: 'YouTube', url: '#', active: false }, - { label: 'Podcast', url: '#', active: true }, - ].map((link) => ( - - {link.label} - {link.active && ( - - )} - - ))} -
-
-
- ); -}; diff --git a/packages/shared/src/features/profile/components/mocks/LearningJourney.tsx b/packages/shared/src/features/profile/components/mocks/LearningJourney.tsx deleted file mode 100644 index f71cef47c0..0000000000 --- a/packages/shared/src/features/profile/components/mocks/LearningJourney.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import { - Typography, - TypographyType, - TypographyTag, - TypographyColor, -} from '../../../../components/typography/Typography'; -import { IconSize } from '../../../../components/Icon'; -import { - BookmarkIcon, - StarIcon, - TrendingIcon, - LinkIcon, -} from '../../../../components/icons'; - -interface LearningGoalProps { - title: string; - progress: number; - articlesRead: number; - target: number; -} - -const LearningGoal = ({ - title, - progress, - articlesRead, - target, -}: LearningGoalProps): ReactElement => ( -
-
- - {title} - - - {articlesRead}/{target} articles - -
-
-
-
-
-); - -interface SavedArticleProps { - title: string; - source: string; - readTime: number; - tags: string[]; -} - -const SavedArticle = ({ - title, - source, - readTime, - tags, -}: SavedArticleProps): ReactElement => ( -
- - {title} - -
- {source} - - {readTime} min read -
-
- {tags.map((tag) => ( - - #{tag} - - ))} -
-
-); - -interface TopicExpertiseProps { - topic: string; - level: number; - articles: number; -} - -const TopicExpertise = ({ - topic, - level, - articles, -}: TopicExpertiseProps): ReactElement => { - const levelLabels = ['Beginner', 'Intermediate', 'Advanced', 'Expert']; - const levelColors = [ - 'bg-text-quaternary', - 'bg-status-warning', - 'bg-brand-default', - 'bg-status-success', - ]; - - return ( -
-
- - {topic} - -
- {[0, 1, 2, 3].map((i) => ( -
- ))} -
-
-
- - {levelLabels[level - 1]} - - - {articles} articles - -
-
- ); -}; - -export const LearningJourney = (): ReactElement => { - return ( -
- - Learning Journey - - - {/* Current Learning Goals */} -
-
- - - Learning Goals - -
-
- - -
-
- - {/* Knowledge Areas */} -
-
- - - Knowledge Areas - -
-
- - - - - - -
-
- - {/* Reading List / Saved Articles */} -
-
-
- - - Reading List - -
- - View all (47) - - -
-
- - - -
-
- - {/* Reading Stats */} -
-
- 847 - - Articles read - -
-
-
- 142h - Reading time -
-
-
- 23 - - Topics explored - -
-
-
- ); -}; diff --git a/packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx b/packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx deleted file mode 100644 index b683c93321..0000000000 --- a/packages/shared/src/features/profile/components/mocks/OpenSourceImpact.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import { - Typography, - TypographyType, - TypographyTag, - TypographyColor, -} from '../../../../components/typography/Typography'; -import { IconSize } from '../../../../components/Icon'; -import { - GitHubIcon, - StarIcon, - MergeIcon, - LinkIcon, -} from '../../../../components/icons'; - -interface ContributionProps { - repo: string; - owner: string; - description: string; - stars: number; - prs: number; - contributorRole: 'maintainer' | 'contributor' | 'author'; -} - -const ContributionCard = ({ - repo, - owner, - description, - stars, - prs, - contributorRole, -}: ContributionProps): ReactElement => { - const roleStyles = { - maintainer: 'bg-status-success text-white', - contributor: 'bg-brand-default text-white', - author: 'bg-accent-bacon-default text-white', - }; - - return ( -
-
-
- -
- - {repo} - - {owner} -
-
- - {contributorRole} - -
-

- {description} -

-
-
- - {stars.toLocaleString()} -
-
- - {prs} PRs merged -
-
-
- ); -}; - -interface StatBoxProps { - value: string | number; - label: string; - icon: ReactElement; -} - -const StatBox = ({ value, label, icon }: StatBoxProps): ReactElement => ( -
- {icon} - {value} - {label} -
-); - -export const OpenSourceImpact = (): ReactElement => { - return ( -
-
- - Open Source Impact - - - - @idoshamun - - -
- - {/* Impact Stats */} -
- } - /> - } - /> - } - /> -
- - {/* Top Contributions */} -
- - Top Contributions - -
- - - -
-
- - {/* Contribution Activity */} -
- - This Year - -
-
- - 847 - -
-
- - Contributions in 2024 - - - Top 5% most active contributor - -
-
-
-
- ); -}; diff --git a/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx b/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx index 5149222df7..0ce3f919e0 100644 --- a/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx +++ b/packages/shared/src/features/profile/components/mocks/SetupShowcase.tsx @@ -10,9 +10,15 @@ import { IconSize } from '../../../../components/Icon'; import { TerminalIcon, SettingsIcon, - LinkIcon, CameraIcon, + EditIcon, + PlusIcon, } from '../../../../components/icons'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '../../../../components/buttons/Button'; interface SetupImageProps { src: string; @@ -21,18 +27,20 @@ interface SetupImageProps { } const SetupImage = ({ src, alt, caption }: SetupImageProps): ReactElement => ( -
+
{alt}
{caption && ( -
- {caption} +
+ + {caption} +
)}
@@ -42,33 +50,54 @@ interface ToolItemProps { name: string; category: string; iconUrl?: string; - iconBg?: string; } -const ToolItem = ({ - name, - category, - iconUrl, - iconBg = 'bg-surface-secondary', -}: ToolItemProps): ReactElement => ( -
-
+const ToolItem = ({ name, category, iconUrl }: ToolItemProps): ReactElement => ( +
+
{iconUrl ? ( - {name} + {name} ) : ( - + )}
-
- {name} - {category} +
+ + {name} + + + {category} +
); -type TabType = 'workspace' | 'terminal' | 'tools'; +type TabType = 'workspace' | 'tools'; + +interface CategoryHeaderProps { + title: string; +} + +const CategoryHeader = ({ title }: CategoryHeaderProps): ReactElement => ( +
+ + {title} + +
+); export const SetupShowcase = (): ReactElement => { const [activeTab, setActiveTab] = useState('workspace'); @@ -79,11 +108,6 @@ export const SetupShowcase = (): ReactElement => { label: 'Workspace', icon: , }, - { - id: 'terminal', - label: 'Terminal', - icon: , - }, { id: 'tools', label: 'Tools', @@ -93,30 +117,41 @@ export const SetupShowcase = (): ReactElement => { return (
- - My Setup - +
+ + My Setup + + +
{/* Tab Navigation */} -
+
{tabs.map((tab) => ( ))}
@@ -129,7 +164,7 @@ export const SetupShowcase = (): ReactElement => { alt="My workspace" caption="Home office setup - where the magic happens" /> -
+
{ alt="Mechanical keyboard" caption="Keychron K2 with custom keycaps" /> +
-
+
Gear Highlights @@ -155,113 +199,34 @@ export const SetupShowcase = (): ReactElement => { 'LG 27" 4K Monitor', 'Keychron K2', 'Logitech MX Master 3', - 'Elgato Wave:3', 'Herman Miller Aeron', ].map((item) => ( - {item} + + {item} + ))} +
)} - {/* Terminal Tab */} - {activeTab === 'terminal' && ( -
- {/* Mock Terminal Screenshot */} -
-
-
- - - -
- - ~ zsh — idoshamun@macbook - -
-
-
❯ neofetch
-
-
-                  {`    .:'
-  _ :'_
-.'  \`' \`.
-:  ._   :
-.  : ;_  ;
-\`. ^^/ .'
- \`'--'\``}
-                
-
-
- OS: macOS 14.2 - Sonoma -
-
- Shell: zsh 5.9 -
-
- Terminal: WezTerm -
-
- Editor: Neovim -
-
- Theme: Tokyo Night -
-
- Font: JetBrains Mono -
-
-
-
- ❯ _ -
-
-
- - {/* Dotfiles Link */} - -
- -
- - My Dotfiles - - - github.com/idoshamun/dotfiles - -
-
- -
-
- )} - {/* Tools Tab */} {activeTab === 'tools' && ( -
-
- - Development - +
+
+
{ category="Terminal Editor" iconUrl="https://cdn.jsdelivr.net/gh/devicons/devicon/icons/neovim/neovim-original.svg" /> - +
-
- - Productivity - +
+
- - +
-
- - Design - -
- - -
-
+
)}
diff --git a/packages/shared/src/features/profile/components/mocks/StackDNA.tsx b/packages/shared/src/features/profile/components/mocks/StackDNA.tsx index 64c791e448..fab713b594 100644 --- a/packages/shared/src/features/profile/components/mocks/StackDNA.tsx +++ b/packages/shared/src/features/profile/components/mocks/StackDNA.tsx @@ -3,22 +3,20 @@ import React from 'react'; import { Typography, TypographyType, - TypographyTag, TypographyColor, } from '../../../../components/typography/Typography'; import { IconSize } from '../../../../components/Icon'; +import { TerminalIcon, EditIcon, PlusIcon } from '../../../../components/icons'; import { - TerminalIcon, - AiIcon, - ShieldIcon, - AppIcon, - CodePenIcon, -} from '../../../../components/icons'; + Button, + ButtonSize, + ButtonVariant, +} from '../../../../components/buttons/Button'; interface StackItemProps { icon: ReactElement; name: string; - level?: 'expert' | 'proficient' | 'learning'; + level?: 'expert' | 'proficient' | 'hobby'; years?: number; } @@ -28,49 +26,87 @@ const StackItem = ({ level = 'proficient', years, }: StackItemProps): ReactElement => { - const levelColors = { - expert: 'text-status-success', - proficient: 'text-brand-default', - learning: 'text-text-quaternary', + const levelStyles = { + expert: 'bg-action-upvote-float text-action-upvote-default', + proficient: 'bg-accent-cabbage-float text-accent-cabbage-default', + hobby: 'bg-accent-blueCheese-float text-accent-blueCheese-default', }; return ( -
- {icon} - {name} +
+ {icon} + + {name} + {years && ( - {years}y + + {years}y + )} - + {level}
); }; -interface PassionTagProps { +interface HotTakeItemProps { + icon: string; label: string; - icon: ReactElement; + content: string; } -const PassionTag = ({ label, icon }: PassionTagProps): ReactElement => ( -
- {icon} - {label} +const HotTakeItem = ({ + icon, + label, + content, +}: HotTakeItemProps): ReactElement => ( +
+ + {icon} + +
+
+ + {label} + +
+ + {content} + +
); export const StackDNA = (): ReactElement => { return (
- - Stack & Tools DNA - +
+ + Stack & Tools + + +
{/* Primary Stack */}
@@ -106,29 +142,43 @@ export const StackDNA = (): ReactElement => { level="proficient" years={3} /> +
- {/* Currently Learning */} + {/* Just for Fun */}
- Currently Learning + Just for Fun
} name="Rust" - level="learning" + level="hobby" /> } name="Go" - level="learning" + level="hobby" /> +
@@ -141,76 +191,33 @@ export const StackDNA = (): ReactElement => { > Hot Takes -
-
- -
- - Favorite - - - TypeScript + React + Tailwind - the holy trinity - -
-
-
-
- -
- - Would Rather Not - - - PHP - sorry, I've moved on. Also XML configs can stay in - 2010. - -
-
-
-
- -
- - Guilty Pleasure - - - Still write bash scripts for everything. No regrets. - -
-
-
-
- - {/* Passions & Interests */} -
- - Passions & Interests - -
- } - /> - } +
+ - } - /> - } +
+ - } +
+ +
+
diff --git a/packages/shared/src/features/profile/components/mocks/index.ts b/packages/shared/src/features/profile/components/mocks/index.ts index 75fe37e9b5..9809ede5c1 100644 --- a/packages/shared/src/features/profile/components/mocks/index.ts +++ b/packages/shared/src/features/profile/components/mocks/index.ts @@ -1,5 +1,2 @@ export { StackDNA } from './StackDNA'; -export { OpenSourceImpact } from './OpenSourceImpact'; -export { LearningJourney } from './LearningJourney'; -export { CurrentlySection } from './CurrentlySection'; export { SetupShowcase } from './SetupShowcase'; diff --git a/packages/webapp/components/layouts/ProfileLayout/index.tsx b/packages/webapp/components/layouts/ProfileLayout/index.tsx index 7c523516a7..212eb7f400 100644 --- a/packages/webapp/components/layouts/ProfileLayout/index.tsx +++ b/packages/webapp/components/layouts/ProfileLayout/index.tsx @@ -25,8 +25,6 @@ import { usePostReferrerContext } from '@dailydotdev/shared/src/contexts/PostRef import { getLayout as getFooterNavBarLayout } from '../FooterNavBarLayout'; import { getLayout as getMainLayout } from '../MainLayout'; import { getTemplatedTitle } from '../utils'; -import { ProfileWidgets } from '../../../../shared/src/features/profile/components/ProfileWidgets/ProfileWidgets'; -import { useProfileSidebarCollapse } from '../../../hooks/useProfileSidebarCollapse'; const Custom404 = dynamic( () => import(/* webpackChunkName: "404" */ '../../../pages/404'), @@ -69,8 +67,6 @@ export const getProfileSeoDefaults = ( export default function ProfileLayout({ user: initialUser, - userStats, - sources, children, }: ProfileLayoutProps): ReactElement { const router = useRouter(); @@ -80,9 +76,6 @@ export default function ProfileLayout({ const { logEvent } = useLogContext(); const { referrerPost } = usePostReferrerContext(); - // Auto-collapse sidebar on small screens - useProfileSidebarCollapse(); - useEffect(() => { if (trackedView || !user) { return; @@ -111,21 +104,11 @@ export default function ProfileLayout({ } return ( -
+
-
- {children} -
- +
{children}
); } diff --git a/packages/webapp/hooks/useProfileSidebarCollapse.ts b/packages/webapp/hooks/useProfileSidebarCollapse.ts deleted file mode 100644 index cf2a4d0738..0000000000 --- a/packages/webapp/hooks/useProfileSidebarCollapse.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext'; - -const COLLAPSE_BREAKPOINT = 1360; - -/** - * Auto-collapses sidebar on profile pages when screen is below 1360px - * */ - -export const useProfileSidebarCollapse = () => { - const { sidebarExpanded, toggleSidebarExpanded, loadedSettings } = - useSettingsContext(); - // Refs to avoid stale closures and unnecessary effect re-runs - const sidebarExpandedRef = useRef(sidebarExpanded); - const toggleSidebarExpandedRef = useRef(toggleSidebarExpanded); - - // Keep refs in sync - useEffect(() => { - sidebarExpandedRef.current = sidebarExpanded; - toggleSidebarExpandedRef.current = toggleSidebarExpanded; - }, [sidebarExpanded, toggleSidebarExpanded]); - - // Auto-collapse on mount (runs once when settings load) - useEffect(() => { - if (!loadedSettings) { - return; - } - - const isSmallScreen = window.matchMedia( - `(max-width: ${COLLAPSE_BREAKPOINT - 1}px)`, - ).matches; - - if (isSmallScreen && sidebarExpanded) { - toggleSidebarExpanded(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadedSettings]); - - // Handle resize from big to small (runs once on mount, listener persists) - useEffect(() => { - const mediaQuery = window.matchMedia( - `(max-width: ${COLLAPSE_BREAKPOINT - 1}px)`, - ); - - const handleBreakpointChange = (e: MediaQueryListEvent) => { - // Use refs to access current values without re-creating listener - if (e.matches && sidebarExpandedRef.current) { - toggleSidebarExpandedRef.current(); - } - }; - - mediaQuery.addEventListener('change', handleBreakpointChange); - return () => - mediaQuery.removeEventListener('change', handleBreakpointChange); - }, []); // Empty deps = runs once, listener stays active -}; diff --git a/packages/webapp/pages/[userId]/index.tsx b/packages/webapp/pages/[userId]/index.tsx index 0889fcad43..cff38b0be5 100644 --- a/packages/webapp/pages/[userId]/index.tsx +++ b/packages/webapp/pages/[userId]/index.tsx @@ -1,7 +1,6 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { AboutMe } from '@dailydotdev/shared/src/features/profile/components/AboutMe'; -import { Activity } from '@dailydotdev/shared/src/features/profile/components/Activity'; import { useProfile } from '@dailydotdev/shared/src/hooks/profile/useProfile'; import { useActions, useJoinReferral } from '@dailydotdev/shared/src/hooks'; import { NextSeo } from 'next-seo'; @@ -11,13 +10,6 @@ import { AutofillProfileBanner } from '@dailydotdev/shared/src/features/profile/ import { ProfileUserExperiences } from '@dailydotdev/shared/src/features/profile/components/experience/ProfileUserExperiences'; import { useUploadCv } from '@dailydotdev/shared/src/features/profile/hooks/useUploadCv'; import { ActionType } from '@dailydotdev/shared/src/graphql/actions'; -import { ProfileWidgets } from '@dailydotdev/shared/src/features/profile/components/ProfileWidgets/ProfileWidgets'; -import { - TypographyType, - TypographyTag, - TypographyColor, - Typography, -} from '@dailydotdev/shared/src/components/typography/Typography'; import { useDynamicHeader } from '@dailydotdev/shared/src/useDynamicHeader'; import { Header } from '@dailydotdev/shared/src/components/profile/Header'; import classNames from 'classnames'; @@ -33,13 +25,6 @@ import { import type { ProfileLayoutProps } from '../../components/layouts/ProfileLayout'; // Dynamically import mock components to avoid SSR issues -const CurrentlySection = dynamic( - () => - import( - '@dailydotdev/shared/src/features/profile/components/mocks/CurrentlySection' - ).then((mod) => mod.CurrentlySection), - { ssr: false }, -); const StackDNA = dynamic( () => import( @@ -47,20 +32,6 @@ const StackDNA = dynamic( ).then((mod) => mod.StackDNA), { ssr: false }, ); -const OpenSourceImpact = dynamic( - () => - import( - '@dailydotdev/shared/src/features/profile/components/mocks/OpenSourceImpact' - ).then((mod) => mod.OpenSourceImpact), - { ssr: false }, -); -const LearningJourney = dynamic( - () => - import( - '@dailydotdev/shared/src/features/profile/components/mocks/LearningJourney' - ).then((mod) => mod.LearningJourney), - { ssr: false }, -); const SetupShowcase = dynamic( () => import( @@ -69,12 +40,10 @@ const SetupShowcase = dynamic( { ssr: false }, ); -// eslint-disable-next-line @typescript-eslint/no-unused-vars const ProfilePage = ({ user: initialUser, noindex, userStats, - sources, }: ProfileLayoutProps): ReactElement => { useJoinReferral(); const { status, onUpload, shouldShow } = useUploadCv(); @@ -120,33 +89,12 @@ const ProfilePage = ({ {!shouldShowBanner &&
} {/* Mock Profile Sections - for preview/demo purposes */} - - - {/* End Mock Profile Sections */} - {isUserSame && ( )} -
- - Highlights - - -