diff --git a/AGENTS.md b/AGENTS.md index a333fc0fd2..3e221b9e7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -245,4 +245,12 @@ Example: Adding a video to the jobs page ## Pull Requests -Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations. \ No newline at end of file +Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations. + +## Code Review Guidelines + +When reviewing code (or writing code that will be reviewed): +- **Delete dead code** - Remove unused components, functions, exports, and files. Don't leave code "for later" +- **Avoid confusing naming** - Don't create multiple components with the same name in different locations (e.g., two `AboutMe` components) +- **Remove unused exports** - If a function/constant is only used internally, don't export it +- **Clean up duplicates** - If the same interface/type is defined in multiple places, consolidate to one location and import \ No newline at end of file diff --git a/packages/shared/src/components/profile/SocialLinksInput.tsx b/packages/shared/src/components/profile/SocialLinksInput.tsx new file mode 100644 index 0000000000..c577623759 --- /dev/null +++ b/packages/shared/src/components/profile/SocialLinksInput.tsx @@ -0,0 +1,221 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useController, useFormContext } from 'react-hook-form'; +import { TextField } from '../fields/TextField'; +import { Typography, TypographyType } from '../typography/Typography'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; +import { PlusIcon, MiniCloseIcon, VIcon } from '../icons'; +import { IconSize } from '../Icon'; +import type { UserSocialLink } from '../../lib/user'; +import type { SocialLinkDisplay } from '../../lib/socialLink'; +import { + detectUserPlatform, + getPlatformIcon, + getPlatformLabel, + PLATFORM_LABELS, +} from '../../lib/socialLink'; +import { useToastNotification } from '../../hooks/useToastNotification'; + +export interface SocialLinksInputProps { + name: string; + label?: string; + hint?: string; +} + +/** + * Get display info for a social link + */ +const getSocialLinkDisplay = (link: UserSocialLink): SocialLinkDisplay => { + return { + id: link.platform, + url: link.url, + platform: link.platform, + icon: getPlatformIcon(link.platform, IconSize.Small), + label: getPlatformLabel(link.platform), + }; +}; + +export function SocialLinksInput({ + name, + label = 'Links', + hint = 'Connect your profiles across the web', +}: SocialLinksInputProps): ReactElement { + const { control } = useFormContext(); + const { + field: { value = [], onChange }, + fieldState: { error }, + } = useController({ + name, + control, + defaultValue: [], + }); + + const [url, setUrl] = useState(''); + const { displayToast } = useToastNotification(); + + const links: UserSocialLink[] = useMemo(() => value || [], [value]); + + // Detect platform as user types + const detectedPlatform = detectUserPlatform(url); + const detectedLabel = detectedPlatform + ? PLATFORM_LABELS[detectedPlatform] + : null; + + const handleUrlChange = (e: React.ChangeEvent) => { + setUrl(e.target.value); + }; + + const handleAdd = useCallback(() => { + const trimmedUrl = url.trim(); + if (!trimmedUrl) { + return; + } + + // Basic URL validation + try { + const parsedUrl = new URL( + trimmedUrl.startsWith('http') ? trimmedUrl : `https://${trimmedUrl}`, + ); + // Normalize URL by removing trailing slash for consistency + const normalizedUrl = parsedUrl.href.replace(/\/$/, ''); + + // Check if URL already exists + if ( + links.some( + (link) => + link.url.toLowerCase().replace(/\/$/, '') === + normalizedUrl.toLowerCase(), + ) + ) { + displayToast('This link has already been added'); + return; + } + + const newLink: UserSocialLink = { + url: normalizedUrl, + platform: detectedPlatform || 'other', + }; + + onChange([...links, newLink]); + setUrl(''); + } catch { + displayToast('Please enter a valid URL'); + } + }, [url, detectedPlatform, links, onChange, displayToast]); + + const handleRemove = useCallback( + (index: number) => { + const newLinks = [...links]; + newLinks.splice(index, 1); + onChange(newLinks); + }, + [links, onChange], + ); + + const displayLinks = useMemo(() => links.map(getSocialLinkDisplay), [links]); + + return ( +
+ {/* Header */} +
+ + {label} + + + {hint} + +
+ + {/* URL input */} + { + if (e.key === 'Enter') { + e.preventDefault(); + handleAdd(); + } + }} + fieldType="secondary" + actionButton={ + + } + /> + + {/* Detection feedback */} + {detectedLabel && ( +
+ + + {detectedLabel} detected + +
+ )} + + {/* Link list */} + {displayLinks.length > 0 && ( +
+ {displayLinks.map((link, index) => ( +
+ {/* Platform icon */} +
+ {link.icon} +
+ + {/* Content */} +
+ + {link.label} + + + {link.url} + +
+ + {/* Remove button */} + +
+ ))} +
+ )} + + {error?.message && ( + + {error.message} + + )} +
+ ); +} diff --git a/packages/shared/src/features/organizations/types.ts b/packages/shared/src/features/organizations/types.ts index 66b0d0c843..7ce838b952 100644 --- a/packages/shared/src/features/organizations/types.ts +++ b/packages/shared/src/features/organizations/types.ts @@ -34,6 +34,14 @@ export enum SocialMediaType { Medium = 'medium', DevTo = 'devto', StackOverflow = 'stackoverflow', + // User profile platforms + Threads = 'threads', + Bluesky = 'bluesky', + Mastodon = 'mastodon', + Roadmap = 'roadmap', + Codepen = 'codepen', + Reddit = 'reddit', + Hashnode = 'hashnode', } export type OrganizationMember = { diff --git a/packages/shared/src/features/organizations/utils/platformDetection.tsx b/packages/shared/src/features/organizations/utils/platformDetection.tsx index 6201b460c3..7222ee2c2b 100644 --- a/packages/shared/src/features/organizations/utils/platformDetection.tsx +++ b/packages/shared/src/features/organizations/utils/platformDetection.tsx @@ -1,8 +1,15 @@ import type { ReactElement } from 'react'; import React from 'react'; import { SocialMediaType, OrganizationLinkType } from '../types'; -import { GitHubIcon, LinkedInIcon, LinkIcon } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; +import { LinkIcon } from '../../../components/icons'; +import type { OrgPlatformId } from '../../../lib/platforms'; +import { + ORG_PLATFORMS, + detectPlatformFromUrl, + getPlatformIconElement, + getPlatformLabel as getGenericPlatformLabel, +} from '../../../lib/platforms'; export type LinkItem = { type: OrganizationLinkType; @@ -18,134 +25,73 @@ export type PlatformMatch = { defaultLabel?: string; }; -export const PLATFORM_MATCHERS: Array<{ - domains: string[]; - match: PlatformMatch; -}> = [ - { - domains: ['linkedin.com'], - match: { - platform: 'LinkedIn', - socialType: SocialMediaType.LinkedIn, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['github.com'], - match: { - platform: 'GitHub', - socialType: SocialMediaType.GitHub, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['twitter.com', 'x.com'], - match: { - platform: 'X', - socialType: SocialMediaType.X, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['wellfound.com', 'angel.co'], - match: { - platform: 'Wellfound', - socialType: SocialMediaType.Wellfound, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['glassdoor.com'], - match: { - platform: 'Glassdoor', - socialType: SocialMediaType.Glassdoor, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['crunchbase.com'], - match: { - platform: 'Crunchbase', - socialType: SocialMediaType.Crunchbase, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['facebook.com', 'fb.com'], - match: { - platform: 'Facebook', - socialType: SocialMediaType.Facebook, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['instagram.com'], - match: { - platform: 'Instagram', - socialType: SocialMediaType.Instagram, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['youtube.com', 'youtu.be'], - match: { - platform: 'YouTube', - socialType: SocialMediaType.YouTube, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['gitlab.com'], - match: { - platform: 'GitLab', - socialType: SocialMediaType.GitLab, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['medium.com'], - match: { - platform: 'Medium', - socialType: SocialMediaType.Medium, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['dev.to'], - match: { - platform: 'Dev.to', - socialType: SocialMediaType.DevTo, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['stackoverflow.com', 'stackexchange.com'], - match: { - platform: 'Stack Overflow', - socialType: SocialMediaType.StackOverflow, - linkType: OrganizationLinkType.Social, - }, - }, - { - domains: ['techcrunch.com'], - match: { - platform: 'TechCrunch', - socialType: null, - linkType: OrganizationLinkType.Press, - defaultLabel: 'Press Release', - }, - }, -]; +/** + * Map platform IDs to SocialMediaType enum values + */ +const PLATFORM_TO_SOCIAL_TYPE: Record = { + github: SocialMediaType.GitHub, + linkedin: SocialMediaType.LinkedIn, + twitter: SocialMediaType.X, + youtube: SocialMediaType.YouTube, + stackoverflow: SocialMediaType.StackOverflow, + reddit: SocialMediaType.Reddit, + mastodon: SocialMediaType.Mastodon, + bluesky: SocialMediaType.Bluesky, + threads: SocialMediaType.Threads, + hashnode: SocialMediaType.Hashnode, + codepen: SocialMediaType.Codepen, + roadmap: SocialMediaType.Roadmap, + facebook: SocialMediaType.Facebook, + instagram: SocialMediaType.Instagram, + gitlab: SocialMediaType.GitLab, + medium: SocialMediaType.Medium, + devto: SocialMediaType.DevTo, + crunchbase: SocialMediaType.Crunchbase, + glassdoor: SocialMediaType.Glassdoor, + wellfound: SocialMediaType.Wellfound, +}; /** - * Normalize URL for consistent matching + * Map SocialMediaType to platform ID */ -export const normalizeUrl = (url: string): string => { +const SOCIAL_TYPE_TO_PLATFORM: Record = { + [SocialMediaType.GitHub]: 'github', + [SocialMediaType.LinkedIn]: 'linkedin', + [SocialMediaType.X]: 'twitter', + [SocialMediaType.YouTube]: 'youtube', + [SocialMediaType.StackOverflow]: 'stackoverflow', + [SocialMediaType.Reddit]: 'reddit', + [SocialMediaType.Mastodon]: 'mastodon', + [SocialMediaType.Bluesky]: 'bluesky', + [SocialMediaType.Threads]: 'threads', + [SocialMediaType.Hashnode]: 'hashnode', + [SocialMediaType.Codepen]: 'codepen', + [SocialMediaType.Roadmap]: 'roadmap', + [SocialMediaType.Facebook]: 'facebook', + [SocialMediaType.Instagram]: 'instagram', + [SocialMediaType.GitLab]: 'gitlab', + [SocialMediaType.Medium]: 'medium', + [SocialMediaType.DevTo]: 'devto', + [SocialMediaType.Crunchbase]: 'crunchbase', + [SocialMediaType.Glassdoor]: 'glassdoor', + [SocialMediaType.Wellfound]: 'wellfound', +}; + +/** + * Press domains that aren't social platforms + */ +const PRESS_DOMAINS = ['techcrunch.com']; + +/** + * Check if URL is a press link + */ +const isPressUrl = (url: string): boolean => { try { const parsed = new URL(url.startsWith('http') ? url : `https://${url}`); - return parsed.hostname.replace(/^(www\.|m\.|mobile\.)/, ''); + const hostname = parsed.hostname.replace(/^(www\.|m\.|mobile\.)/, ''); + return PRESS_DOMAINS.some((domain) => hostname.includes(domain)); } catch { - return url.toLowerCase(); + return false; } }; @@ -156,11 +102,32 @@ export const detectPlatform = (url: string): PlatformMatch | null => { if (!url.trim()) { return null; } - const hostname = normalizeUrl(url); - const matcher = PLATFORM_MATCHERS.find(({ domains }) => - domains.some((d) => hostname.includes(d)), - ); - return matcher?.match ?? null; + + // Check for press links first + if (isPressUrl(url)) { + return { + platform: 'TechCrunch', + socialType: null, + linkType: OrganizationLinkType.Press, + defaultLabel: 'Press Release', + }; + } + + // Use generic platform detection + const platformId = detectPlatformFromUrl(url, ORG_PLATFORMS); + + if (platformId) { + const config = ORG_PLATFORMS[platformId]; + const socialType = PLATFORM_TO_SOCIAL_TYPE[platformId] || null; + + return { + platform: config.label, + socialType, + linkType: OrganizationLinkType.Social, + }; + } + + return null; }; /** @@ -170,19 +137,23 @@ export const getLinkDisplayName = (link: LinkItem): string => { if (link.title) { return link.title; } - // Find platform from matchers and use defaultLabel if available - const matchedPlatform = PLATFORM_MATCHERS.find( - ({ match }) => - (link.socialType && match.socialType === link.socialType) || - (!link.socialType && match.linkType === link.type && !match.socialType), - ); - if (matchedPlatform) { - return matchedPlatform.match.defaultLabel || matchedPlatform.match.platform; + + // If it's a press link without a title + if (link.type === OrganizationLinkType.Press) { + return 'Press Release'; } + + // Get label from socialType if (link.socialType) { + const platformId = + SOCIAL_TYPE_TO_PLATFORM[link.socialType as SocialMediaType]; + if (platformId) { + return getGenericPlatformLabel(platformId, ORG_PLATFORMS); + } // Fallback: capitalize socialType return link.socialType.charAt(0).toUpperCase() + link.socialType.slice(1); } + return 'Link'; }; @@ -192,13 +163,18 @@ export const getLinkDisplayName = (link: LinkItem): string => { export const getPlatformIcon = ( link: LinkItem, ): ReactElement<{ size?: IconSize; className?: string }> => { - if (link.socialType === SocialMediaType.GitHub) { - return ; - } - if (link.socialType === SocialMediaType.LinkedIn) { - return ( - - ); + if (link.socialType) { + const platformId = + SOCIAL_TYPE_TO_PLATFORM[link.socialType as SocialMediaType]; + if (platformId) { + return ( + + {getPlatformIconElement(platformId, ORG_PLATFORMS, IconSize.Small)} + + ); + } } + + // Default link icon return ; }; diff --git a/packages/shared/src/features/profile/components/AboutMe.tsx b/packages/shared/src/features/profile/components/AboutMe.tsx index 37a41cd66f..3744d79193 100644 --- a/packages/shared/src/features/profile/components/AboutMe.tsx +++ b/packages/shared/src/features/profile/components/AboutMe.tsx @@ -13,20 +13,6 @@ import { TypographyType, TypographyColor, } from '../../../components/typography/Typography'; -import { - BlueskyIcon, - CodePenIcon, - GitHubIcon, - LinkedInIcon, - LinkIcon, - MastodonIcon, - RedditIcon, - RoadmapIcon, - StackOverflowIcon, - ThreadsIcon, - TwitterIcon, - YoutubeIcon, -} from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { SimpleTooltip } from '../../../components/tooltips/SimpleTooltip'; import { ExpandableContent } from '../../../components/ExpandableContent'; @@ -34,19 +20,13 @@ import { useLogContext } from '../../../contexts/LogContext'; import { combinedClicks } from '../../../lib/click'; import { LogEvent, TargetType } from '../../../lib/log'; import { anchorDefaultRel } from '../../../lib/strings'; +import { getUserSocialLinks } from '../../../lib/socialLink'; export interface AboutMeProps { user: PublicProfile; className?: string; } -interface SocialLink { - id: string; - url: string; - icon: ReactElement; - label: string; -} - export function AboutMe({ user, className, @@ -57,82 +37,10 @@ export function AboutMe({ // Markdown is supported only in the client due to sanitization const isClient = typeof window !== 'undefined'; - const socialLinks = useMemo(() => { - return [ - user.github && { - id: 'github', - url: `https://github.com/${user.github}`, - icon: , - label: 'GitHub', - }, - user.linkedin && { - id: 'linkedin', - url: `https://linkedin.com/in/${user.linkedin}`, - icon: , - label: 'LinkedIn', - }, - user.portfolio && { - id: 'portfolio', - url: user.portfolio, - icon: , - label: 'Portfolio', - }, - user.twitter && { - id: 'twitter', - url: `https://x.com/${user.twitter}`, - icon: , - label: 'Twitter', - }, - user.youtube && { - id: 'youtube', - url: `https://youtube.com/@${user.youtube}`, - icon: , - label: 'YouTube', - }, - user.stackoverflow && { - id: 'stackoverflow', - url: `https://stackoverflow.com/users/${user.stackoverflow}`, - icon: , - label: 'Stack Overflow', - }, - user.reddit && { - id: 'reddit', - url: `https://reddit.com/user/${user.reddit}`, - icon: , - label: 'Reddit', - }, - user.roadmap && { - id: 'roadmap', - url: `https://roadmap.sh/u/${user.roadmap}`, - icon: , - label: 'Roadmap.sh', - }, - user.codepen && { - id: 'codepen', - url: `https://codepen.io/${user.codepen}`, - icon: , - label: 'CodePen', - }, - user.mastodon && { - id: 'mastodon', - url: user.mastodon, - icon: , - label: 'Mastodon', - }, - user.bluesky && { - id: 'bluesky', - url: `https://bsky.app/profile/${user.bluesky}`, - icon: , - label: 'Bluesky', - }, - user.threads && { - id: 'threads', - url: `https://threads.net/@${user.threads}`, - icon: , - label: 'Threads', - }, - ].filter(Boolean) as SocialLink[]; - }, [user]); + const socialLinks = useMemo( + () => getUserSocialLinks(user, IconSize.XSmall), + [user], + ); const shouldShowReadme = readme && isClient; const shouldShowSocialLinks = socialLinks.length > 0; diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AboutMe.spec.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AboutMe.spec.tsx deleted file mode 100644 index 81126650c3..0000000000 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AboutMe.spec.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import type { PublicProfile } from '../../../../lib/user'; -import { AboutMe } from './AboutMe'; -import { getLogContextStatic } from '../../../../contexts/LogContext'; - -const LogContext = getLogContextStatic(); - -const mockCopyLink = jest.fn(); -const mockLogEvent = jest.fn(); - -// Mock useCopyLink hook -jest.mock('../../../../hooks/useCopy', () => ({ - useCopyLink: () => [jest.fn(), mockCopyLink], -})); - -beforeEach(() => { - jest.clearAllMocks(); - // Mock window for client-side check - Object.defineProperty(window, 'innerWidth', { - writable: true, - configurable: true, - value: 1024, - }); -}); - -const baseUser: PublicProfile = { - id: 'u1', - name: 'Test User', - username: 'testuser', - image: 'https://daily.dev/user.png', - permalink: 'https://daily.dev/testuser', - reputation: 100, - createdAt: '2020-01-01T00:00:00.000Z', - premium: false, -}; - -const userWithReadme: PublicProfile = { - ...baseUser, - readmeHtml: 'This is my awesome bio with some **markdown**!', -}; - -const userWithSocialLinks: PublicProfile = { - ...userWithReadme, - twitter: 'testuser', - github: 'testuser', - linkedin: 'https://linkedin.com/in/testuser', - portfolio: 'https://testuser.com', - youtube: 'testuser', - stackoverflow: '123456/testuser', - reddit: 'testuser', - roadmap: 'testuser', - codepen: 'testuser', - mastodon: 'https://mastodon.social/@testuser', - bluesky: 'testuser.bsky.social', - threads: 'testuser', -}; - -const renderComponent = (user: Partial = {}) => { - const mergedUser = { ...baseUser, ...user }; - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); - return render( - - - - - , - ); -}; - -describe('AboutMe', () => { - describe('Rendering', () => { - it('should not render when both readme and social links are not present', () => { - const { container } = renderComponent(); - expect(container).toBeEmptyDOMElement(); - }); - - it('should render when readme is present', () => { - renderComponent(userWithReadme); - expect(screen.getByText('About me')).toBeInTheDocument(); - expect( - screen.getByText('This is my awesome bio with some **markdown**!'), - ).toBeInTheDocument(); - }); - - it('should render with social links when user has them', () => { - renderComponent(userWithSocialLinks); - expect(screen.getByTestId('social-link-github')).toBeInTheDocument(); - expect(screen.getByTestId('social-link-linkedin')).toBeInTheDocument(); - expect(screen.getByTestId('social-link-portfolio')).toBeInTheDocument(); - }); - }); - - describe('Social Links', () => { - it('should show only first 3 social links initially', () => { - renderComponent(userWithSocialLinks); - const allLinks = screen.getAllByTestId(/^social-link-/); - expect(allLinks.length).toBe(3); - }); - - it('should show "+N" button when more than 3 links', () => { - renderComponent(userWithSocialLinks); - const showAllButton = screen.getByTestId('show-all-links'); - expect(showAllButton).toBeInTheDocument(); - // 12 total links - 3 visible = 9 more - expect(showAllButton).toHaveTextContent('+9'); - }); - - it('should show all links when "+N" button is clicked', async () => { - renderComponent(userWithSocialLinks); - const showAllButton = screen.getByTestId('show-all-links'); - fireEvent.click(showAllButton); - - await waitFor(() => { - const allLinks = screen.getAllByTestId(/^social-link-/); - expect(allLinks.length).toBe(12); // All 12 social links - }); - }); - - it('should hide "+N" button after showing all links', async () => { - renderComponent(userWithSocialLinks); - const showAllButton = screen.getByTestId('show-all-links'); - fireEvent.click(showAllButton); - - await waitFor(() => { - expect(screen.queryByTestId('show-all-links')).not.toBeInTheDocument(); - }); - }); - }); - - describe('Analytics', () => { - it('should log click event for social links', () => { - renderComponent(userWithSocialLinks); - const githubLink = screen.getByTestId('social-link-github'); - fireEvent.click(githubLink); - - expect(mockLogEvent).toHaveBeenCalledWith({ - event_name: 'click', - target_type: 'social link', - target_id: 'github', - }); - }); - }); -}); diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AboutMe.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AboutMe.tsx deleted file mode 100644 index 29fbb20180..0000000000 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AboutMe.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import type { ReactElement } from 'react'; -import React, { useMemo, useState } from 'react'; -import classNames from 'classnames'; -import type { PublicProfile } from '../../../../lib/user'; -import Markdown from '../../../../components/Markdown'; -import { - Button, - ButtonSize, - ButtonVariant, -} from '../../../../components/buttons/Button'; -import { - Typography, - TypographyType, - TypographyColor, -} from '../../../../components/typography/Typography'; -import { - BlueskyIcon, - CodePenIcon, - GitHubIcon, - LinkedInIcon, - LinkIcon, - MastodonIcon, - RedditIcon, - RoadmapIcon, - StackOverflowIcon, - ThreadsIcon, - TwitterIcon, - YoutubeIcon, -} from '../../../../components/icons'; -import { IconSize } from '../../../../components/Icon'; -import { SimpleTooltip } from '../../../../components/tooltips/SimpleTooltip'; -import { ExpandableContent } from '../../../../components/ExpandableContent'; -import { useLogContext } from '../../../../contexts/LogContext'; -import { combinedClicks } from '../../../../lib/click'; -import { LogEvent, TargetType } from '../../../../lib/log'; -import { anchorDefaultRel } from '../../../../lib/strings'; - -export interface AboutMeProps { - user: PublicProfile; - className?: string; -} - -const MAX_VISIBLE_LINKS = 3; - -interface SocialLink { - id: string; - url: string; - icon: ReactElement; - label: string; -} - -export function AboutMe({ - user, - className, -}: AboutMeProps): ReactElement | null { - const [showAllLinks, setShowAllLinks] = useState(false); - const readme = user?.readmeHtml; - const { logEvent } = useLogContext(); - - // Markdown is supported only in the client due to sanitization - const isClient = typeof window !== 'undefined'; - - const socialLinks = useMemo(() => { - return [ - user.github && { - id: 'github', - url: `https://github.com/${user.github}`, - icon: , - label: 'GitHub', - }, - user.linkedin && { - id: 'linkedin', - url: user.linkedin, - icon: , - label: 'LinkedIn', - }, - user.portfolio && { - id: 'portfolio', - url: user.portfolio, - icon: , - label: 'Portfolio', - }, - user.twitter && { - id: 'twitter', - url: `https://x.com/${user.twitter}`, - icon: , - label: 'Twitter', - }, - user.youtube && { - id: 'youtube', - url: `https://youtube.com/@${user.youtube}`, - icon: , - label: 'YouTube', - }, - user.stackoverflow && { - id: 'stackoverflow', - url: `https://stackoverflow.com/users/${user.stackoverflow}`, - icon: , - label: 'Stack Overflow', - }, - user.reddit && { - id: 'reddit', - url: `https://reddit.com/user/${user.reddit}`, - icon: , - label: 'Reddit', - }, - user.roadmap && { - id: 'roadmap', - url: `https://roadmap.sh/u/${user.roadmap}`, - icon: , - label: 'Roadmap.sh', - }, - user.codepen && { - id: 'codepen', - url: `https://codepen.io/${user.codepen}`, - icon: , - label: 'CodePen', - }, - user.mastodon && { - id: 'mastodon', - url: user.mastodon, - icon: , - label: 'Mastodon', - }, - user.bluesky && { - id: 'bluesky', - url: `https://bsky.app/profile/${user.bluesky}`, - icon: , - label: 'Bluesky', - }, - user.threads && { - id: 'threads', - url: `https://threads.net/@${user.threads}`, - icon: , - label: 'Threads', - }, - ].filter(Boolean) as SocialLink[]; - }, [user]); - - const visibleLinks = showAllLinks - ? socialLinks - : socialLinks.slice(0, MAX_VISIBLE_LINKS); - const hasMoreLinks = socialLinks.length > MAX_VISIBLE_LINKS; - - const shouldShowReadme = readme && isClient; - const shouldShowSocialLinks = socialLinks.length > 0; - - if (!shouldShowReadme && !shouldShowSocialLinks) { - return null; - } - - return ( -
- -
- - About me - - -
- {shouldShowSocialLinks && ( -
- {visibleLinks.map((link) => ( - - - - )} -
- )} - - {shouldShowReadme && ( -
- -
- )} -
-
-
-
- ); -} diff --git a/packages/shared/src/graphql/users.ts b/packages/shared/src/graphql/users.ts index 7cc836b20d..d97e406457 100644 --- a/packages/shared/src/graphql/users.ts +++ b/packages/shared/src/graphql/users.ts @@ -63,6 +63,10 @@ export const USER_BY_ID_STATIC_FIELDS_QUERY = ` readmeHtml isPlus experienceLevel + socialLinks { + platform + url + } location { city subdivision @@ -328,6 +332,10 @@ export const UPDATE_USER_PROFILE_MUTATION = gql` timezone experienceLevel language + socialLinks { + platform + url + } } } `; @@ -365,6 +373,10 @@ export const UPDATE_USER_INFO_MUTATION = gql` experienceLevel hideExperience language + socialLinks { + platform + url + } } } `; diff --git a/packages/shared/src/hooks/useUserInfoForm.ts b/packages/shared/src/hooks/useUserInfoForm.ts index e80241f4a9..1813aae6ac 100644 --- a/packages/shared/src/hooks/useUserInfoForm.ts +++ b/packages/shared/src/hooks/useUserInfoForm.ts @@ -1,4 +1,4 @@ -import { useContext, useEffect, useRef } from 'react'; +import { useContext, useEffect, useMemo, useRef } from 'react'; import { useForm } from 'react-hook-form'; import type { UseFormReturn } from 'react-hook-form'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -13,6 +13,7 @@ import { useDirtyForm } from './useDirtyForm'; import { useLogContext } from '../contexts/LogContext'; import { LogEvent } from '../lib/log'; import { useProfile } from './profile/useProfile'; +import { buildUserSocialLinksFromLegacy } from '../lib/socialLink'; export interface ProfileFormHint { portfolio?: string; @@ -64,6 +65,14 @@ const useUserInfoForm = (): UseUserInfoForm => { const { displayToast } = useToastNotification(); const router = useRouter(); + // Build initial socialLinks from user data or legacy fields + const initialSocialLinks = useMemo(() => { + if (user?.socialLinks && user.socialLinks.length > 0) { + return user.socialLinks; + } + return user ? buildUserSocialLinksFromLegacy(user) : []; + }, [user]); + useEffect(() => { const searchParams = new URLSearchParams(window?.location?.search); const field = searchParams?.get('field'); @@ -82,22 +91,11 @@ const useUserInfoForm = (): UseUserInfoForm => { image: user?.image, cover: user?.cover, bio: user?.bio, - github: user?.github, externalLocationId: user?.location?.externalId, - linkedin: user?.linkedin, - portfolio: user?.portfolio, - twitter: user?.twitter, - youtube: user?.youtube, - stackoverflow: user?.stackoverflow, - reddit: user?.reddit, - roadmap: user?.roadmap, - codepen: user?.codepen, - mastodon: user?.mastodon, - bluesky: user?.bluesky, - threads: user?.threads, experienceLevel: user?.experienceLevel, hideExperience: user?.hideExperience, readme: user?.readme || '', + socialLinks: initialSocialLinks, }, }); diff --git a/packages/shared/src/lib/platforms.tsx b/packages/shared/src/lib/platforms.tsx new file mode 100644 index 0000000000..88be588761 --- /dev/null +++ b/packages/shared/src/lib/platforms.tsx @@ -0,0 +1,338 @@ +import type { ComponentType, ReactElement } from 'react'; +import React from 'react'; +import type { IconProps, IconSize } from '../components/Icon'; +import { + BlueskyIcon, + CodePenIcon, + CrunchbaseIcon, + FacebookIcon, + GitHubIcon, + GitLabIcon, + HashnodeIcon, + LinkedInIcon, + LinkIcon, + MastodonIcon, + RedditIcon, + RoadmapIcon, + StackOverflowIcon, + ThreadsIcon, + TwitterIcon, + YoutubeIcon, +} from '../components/icons'; + +/** + * Configuration for a social/link platform + */ +export type PlatformConfig = { + /** Unique identifier for the platform */ + id: string; + /** Display label */ + label: string; + /** Domains used to detect this platform from URLs */ + domains: string[]; + /** Icon component */ + icon: ComponentType; + /** Build full URL from username (for legacy field migration) */ + urlBuilder?: (username: string) => string; +}; + +/** + * Core platforms shared across all contexts (organizations and user profiles) + */ +export const CORE_PLATFORMS = { + github: { + id: 'github', + label: 'GitHub', + domains: ['github.com'], + icon: GitHubIcon, + urlBuilder: (u: string) => `https://github.com/${u}`, + }, + linkedin: { + id: 'linkedin', + label: 'LinkedIn', + domains: ['linkedin.com'], + icon: LinkedInIcon, + urlBuilder: (u: string) => `https://linkedin.com/in/${u}`, + }, + twitter: { + id: 'twitter', + label: 'X', + domains: ['twitter.com', 'x.com'], + icon: TwitterIcon, + urlBuilder: (u: string) => `https://x.com/${u}`, + }, + youtube: { + id: 'youtube', + label: 'YouTube', + domains: ['youtube.com', 'youtu.be'], + icon: YoutubeIcon, + urlBuilder: (u: string) => `https://youtube.com/@${u}`, + }, + stackoverflow: { + id: 'stackoverflow', + label: 'Stack Overflow', + domains: ['stackoverflow.com', 'stackexchange.com'], + icon: StackOverflowIcon, + urlBuilder: (u: string) => `https://stackoverflow.com/users/${u}`, + }, + reddit: { + id: 'reddit', + label: 'Reddit', + domains: ['reddit.com'], + icon: RedditIcon, + urlBuilder: (u: string) => `https://reddit.com/user/${u}`, + }, + mastodon: { + id: 'mastodon', + label: 'Mastodon', + // Common instances - federated nature makes detection tricky + domains: [ + 'mastodon.social', + 'mastodon.online', + 'fosstodon.org', + 'hachyderm.io', + 'mstdn.social', + ], + icon: MastodonIcon, + // Mastodon URLs are full URLs, not usernames + }, + bluesky: { + id: 'bluesky', + label: 'Bluesky', + domains: ['bsky.app'], + icon: BlueskyIcon, + urlBuilder: (u: string) => `https://bsky.app/profile/${u}`, + }, + threads: { + id: 'threads', + label: 'Threads', + domains: ['threads.net'], + icon: ThreadsIcon, + urlBuilder: (u: string) => `https://threads.net/@${u}`, + }, + hashnode: { + id: 'hashnode', + label: 'Hashnode', + domains: ['hashnode.com', 'hashnode.dev'], + icon: HashnodeIcon, + urlBuilder: (u: string) => `https://hashnode.com/@${u}`, + }, + codepen: { + id: 'codepen', + label: 'CodePen', + domains: ['codepen.io'], + icon: CodePenIcon, + urlBuilder: (u: string) => `https://codepen.io/${u}`, + }, + roadmap: { + id: 'roadmap', + label: 'Roadmap.sh', + domains: ['roadmap.sh'], + icon: RoadmapIcon, + urlBuilder: (u: string) => `https://roadmap.sh/u/${u}`, + }, +} satisfies Record; + +/** + * Organization-specific platforms (extends core) + */ +export const ORG_ONLY_PLATFORMS = { + facebook: { + id: 'facebook', + label: 'Facebook', + domains: ['facebook.com', 'fb.com'], + icon: FacebookIcon, + }, + instagram: { + id: 'instagram', + label: 'Instagram', + domains: ['instagram.com'], + icon: LinkIcon, // No specific icon available + }, + gitlab: { + id: 'gitlab', + label: 'GitLab', + domains: ['gitlab.com'], + icon: GitLabIcon, + }, + medium: { + id: 'medium', + label: 'Medium', + domains: ['medium.com'], + icon: LinkIcon, // No specific icon available + }, + devto: { + id: 'devto', + label: 'Dev.to', + domains: ['dev.to'], + icon: LinkIcon, // No specific icon + }, + crunchbase: { + id: 'crunchbase', + label: 'Crunchbase', + domains: ['crunchbase.com'], + icon: CrunchbaseIcon, + }, + glassdoor: { + id: 'glassdoor', + label: 'Glassdoor', + domains: ['glassdoor.com'], + icon: LinkIcon, // No specific icon + }, + wellfound: { + id: 'wellfound', + label: 'Wellfound', + domains: ['wellfound.com', 'angel.co'], + icon: LinkIcon, // No specific icon + }, +} satisfies Record; + +/** + * User profile-specific platforms (extends core) + */ +export const USER_ONLY_PLATFORMS = { + portfolio: { + id: 'portfolio', + label: 'Website', + domains: [], // No specific domains - catch-all for personal sites + icon: LinkIcon, + // Portfolio URLs are full URLs + }, + other: { + id: 'other', + label: 'Link', + domains: [], // Fallback for unrecognized platforms + icon: LinkIcon, + }, +} satisfies Record; + +/** + * All platforms for organizations + */ +export const ORG_PLATFORMS = { + ...CORE_PLATFORMS, + ...ORG_ONLY_PLATFORMS, +} satisfies Record; + +/** + * All platforms for user profiles + */ +export const USER_PLATFORMS = { + ...CORE_PLATFORMS, + ...USER_ONLY_PLATFORMS, +} satisfies Record; + +export type CorePlatformId = keyof typeof CORE_PLATFORMS; +export type OrgPlatformId = keyof typeof ORG_PLATFORMS; +export type UserPlatformId = keyof typeof USER_PLATFORMS; + +/** + * Normalize URL hostname for matching + */ +const normalizeHostname = (url: string): string => { + try { + const parsed = new URL(url.startsWith('http') ? url : `https://${url}`); + return parsed.hostname.replace(/^(www\.|m\.|mobile\.)/, ''); + } catch { + return url.toLowerCase(); + } +}; + +/** + * Check if URL matches Mastodon pattern (/@username) + */ +const isMastodonUrl = (url: string): boolean => { + try { + const parsed = new URL(url.startsWith('http') ? url : `https://${url}`); + return parsed.pathname.includes('/@'); + } catch { + return false; + } +}; + +/** + * Detect platform from URL using provided platform config + */ +export function detectPlatformFromUrl>( + url: string, + platforms: T, +): keyof T | null { + if (!url.trim()) { + return null; + } + + const hostname = normalizeHostname(url); + + // Check each platform's domains + const matchedEntry = Object.entries(platforms).find(([, config]) => + config.domains.some((domain) => hostname.includes(domain)), + ); + + if (matchedEntry) { + return matchedEntry[0] as keyof T; + } + + // Special case: Mastodon detection by URL pattern + if ('mastodon' in platforms && isMastodonUrl(url)) { + return 'mastodon' as keyof T; + } + + return null; +} + +/** + * Get platform config by ID + */ +export function getPlatformConfig>( + platformId: string, + platforms: T, +): PlatformConfig | null { + return platforms[platformId] || null; +} + +/** + * Get platform icon element + */ +export function getPlatformIconElement< + T extends Record, +>( + platformId: string, + platforms: T, + size: IconSize, + fallback: PlatformConfig = USER_ONLY_PLATFORMS.other, +): ReactElement { + const config = platforms[platformId] || fallback; + const IconComponent = config.icon; + return ; +} + +/** + * Get platform label + */ +export function getPlatformLabel>( + platformId: string, + platforms: T, + fallback = 'Link', +): string { + const config = platforms[platformId]; + if (config) { + return config.label; + } + // Capitalize first letter as fallback + return platformId.charAt(0).toUpperCase() + platformId.slice(1) || fallback; +} + +/** + * Build URL from username using platform's urlBuilder + */ +export function buildPlatformUrl>( + platformId: string, + username: string, + platforms: T, +): string | null { + const config = platforms[platformId]; + if (config?.urlBuilder) { + return config.urlBuilder(username); + } + return null; +} diff --git a/packages/shared/src/lib/socialLink.tsx b/packages/shared/src/lib/socialLink.tsx new file mode 100644 index 0000000000..d583f8beda --- /dev/null +++ b/packages/shared/src/lib/socialLink.tsx @@ -0,0 +1,181 @@ +import type { ReactElement } from 'react'; +import type { IconSize } from '../components/Icon'; +import type { PublicProfile, UserSocialLink } from './user'; +import type { UserPlatformId } from './platforms'; +import { + USER_PLATFORMS, + detectPlatformFromUrl, + getPlatformIconElement, + getPlatformLabel as getGenericPlatformLabel, + buildPlatformUrl, +} from './platforms'; + +// Re-export types for backward compatibility +export type { UserPlatformId as SocialPlatform } from './platforms'; + +/** + * Platform labels for display (backward compatibility export) + */ +export const PLATFORM_LABELS: Record = Object.fromEntries( + Object.entries(USER_PLATFORMS).map(([id, config]) => [id, config.label]), +); + +/** + * Type for user objects that have legacy social fields + * Works with both PublicProfile and LoggedUser + */ +type UserWithLegacySocials = Pick< + PublicProfile, + | 'github' + | 'linkedin' + | 'portfolio' + | 'twitter' + | 'youtube' + | 'stackoverflow' + | 'reddit' + | 'roadmap' + | 'codepen' + | 'mastodon' + | 'bluesky' + | 'threads' + | 'hashnode' + | 'socialLinks' +>; + +export interface SocialLinkDisplay { + id: string; + url: string; + platform: string; + icon: ReactElement; + label: string; +} + +/** + * Get icon element for a platform + */ +export const getPlatformIcon = ( + platform: string, + size: IconSize, +): ReactElement => { + return getPlatformIconElement(platform, USER_PLATFORMS, size); +}; + +/** + * Get label for a platform + */ +export const getPlatformLabel = (platform: string): string => { + return getGenericPlatformLabel(platform, USER_PLATFORMS); +}; + +/** + * Detect user platform from URL + */ +export const detectUserPlatform = (url: string): UserPlatformId | null => { + return detectPlatformFromUrl(url, USER_PLATFORMS); +}; + +/** + * Convert UserSocialLink array to display format with icons + */ +const mapSocialLinksToDisplay = ( + links: UserSocialLink[], + iconSize: IconSize, +): SocialLinkDisplay[] => { + return links.map(({ platform, url }) => ({ + id: platform, + url, + platform, + icon: getPlatformIcon(platform, iconSize), + label: getPlatformLabel(platform), + })); +}; + +/** + * Legacy field to platform ID mapping + */ +const LEGACY_FIELDS: Array<{ + field: keyof UserWithLegacySocials; + platform: UserPlatformId; + isFullUrl?: boolean; +}> = [ + { field: 'github', platform: 'github' }, + { field: 'linkedin', platform: 'linkedin' }, + { field: 'portfolio', platform: 'portfolio', isFullUrl: true }, + { field: 'twitter', platform: 'twitter' }, + { field: 'youtube', platform: 'youtube' }, + { field: 'stackoverflow', platform: 'stackoverflow' }, + { field: 'reddit', platform: 'reddit' }, + { field: 'roadmap', platform: 'roadmap' }, + { field: 'codepen', platform: 'codepen' }, + { field: 'mastodon', platform: 'mastodon', isFullUrl: true }, + { field: 'bluesky', platform: 'bluesky' }, + { field: 'threads', platform: 'threads' }, + { field: 'hashnode', platform: 'hashnode' }, +]; + +/** + * Build social links from legacy individual user fields (fallback) + * @deprecated This will be removed once all users have socialLinks populated + */ +const buildSocialLinksFromLegacy = ( + user: UserWithLegacySocials, + iconSize: IconSize, +): SocialLinkDisplay[] => { + return LEGACY_FIELDS.reduce( + (links, { field, platform, isFullUrl }) => { + const value = user[field]; + if (value && typeof value === 'string') { + const url = isFullUrl + ? value + : buildPlatformUrl(platform, value, USER_PLATFORMS) || value; + + links.push({ + id: platform, + platform, + url, + icon: getPlatformIcon(platform, iconSize), + label: getPlatformLabel(platform), + }); + } + return links; + }, + [], + ); +}; + +/** + * Build UserSocialLink array from legacy individual user fields + * Used for form initialization when user has legacy data + * @deprecated This will be removed once all users have socialLinks populated + */ +export const buildUserSocialLinksFromLegacy = ( + user: UserWithLegacySocials, +): UserSocialLink[] => { + return LEGACY_FIELDS.reduce( + (links, { field, platform, isFullUrl }) => { + const value = user[field]; + if (value && typeof value === 'string') { + const url = isFullUrl + ? value + : buildPlatformUrl(platform, value, USER_PLATFORMS) || value; + + links.push({ platform, url }); + } + return links; + }, + [], + ); +}; + +/** + * Get display social links from user, preferring socialLinks array over legacy fields + */ +export const getUserSocialLinks = ( + user: UserWithLegacySocials, + iconSize: IconSize, +): SocialLinkDisplay[] => { + if (user.socialLinks && user.socialLinks.length > 0) { + return mapSocialLinksToDisplay(user.socialLinks, iconSize); + } + return buildSocialLinksFromLegacy(user, iconSize); +}; diff --git a/packages/shared/src/lib/user.ts b/packages/shared/src/lib/user.ts index 3f4c8c46bb..9464970027 100644 --- a/packages/shared/src/lib/user.ts +++ b/packages/shared/src/lib/user.ts @@ -16,6 +16,11 @@ export enum Roles { Moderator = 'moderator', } +export interface UserSocialLink { + platform: string; + url: string; +} + export interface AnonymousUser { id: string; firstVisit?: string; @@ -31,19 +36,33 @@ export interface PublicProfile { id: string; name: string; username?: string; + /** @deprecated Use socialLinks instead */ twitter?: string; + /** @deprecated Use socialLinks instead */ github?: string; + /** @deprecated Use socialLinks instead */ hashnode?: string; + /** @deprecated Use socialLinks instead */ portfolio?: string; + /** @deprecated Use socialLinks instead */ roadmap?: string; + /** @deprecated Use socialLinks instead */ threads?: string; + /** @deprecated Use socialLinks instead */ codepen?: string; + /** @deprecated Use socialLinks instead */ reddit?: string; + /** @deprecated Use socialLinks instead */ stackoverflow?: string; + /** @deprecated Use socialLinks instead */ youtube?: string; + /** @deprecated Use socialLinks instead */ linkedin?: string; + /** @deprecated Use socialLinks instead */ mastodon?: string; + /** @deprecated Use socialLinks instead */ bluesky?: string; + socialLinks?: UserSocialLink[]; bio?: string; createdAt: string; premium: boolean; @@ -102,19 +121,33 @@ export interface UserProfile { username?: string; company?: string; title?: string; + /** @deprecated Use socialLinks instead */ twitter?: string; + /** @deprecated Use socialLinks instead */ github?: string; + /** @deprecated Use socialLinks instead */ hashnode?: string; + /** @deprecated Use socialLinks instead */ roadmap?: string; + /** @deprecated Use socialLinks instead */ threads?: string; + /** @deprecated Use socialLinks instead */ codepen?: string; + /** @deprecated Use socialLinks instead */ reddit?: string; + /** @deprecated Use socialLinks instead */ stackoverflow?: string; + /** @deprecated Use socialLinks instead */ youtube?: string; + /** @deprecated Use socialLinks instead */ linkedin?: string; + /** @deprecated Use socialLinks instead */ mastodon?: string; + /** @deprecated Use socialLinks instead */ bluesky?: string; + /** @deprecated Use socialLinks instead */ portfolio?: string; + socialLinks?: UserSocialLink[]; bio?: string; acceptedMarketing?: boolean; timezone?: string; diff --git a/packages/webapp/components/layouts/SettingsLayout/Profile/index.tsx b/packages/webapp/components/layouts/SettingsLayout/Profile/index.tsx index 4541c4ea5b..06a2365001 100644 --- a/packages/webapp/components/layouts/SettingsLayout/Profile/index.tsx +++ b/packages/webapp/components/layouts/SettingsLayout/Profile/index.tsx @@ -9,19 +9,7 @@ import ControlledTextField from '@dailydotdev/shared/src/components/fields/Contr import ControlledTextarea from '@dailydotdev/shared/src/components/fields/ControlledTextarea'; import { AtIcon, - CodePenIcon, - GitHubIcon, - LinkedInIcon, - LinkIcon, - MastodonIcon, - BlueskyIcon, - RedditIcon, - RoadmapIcon, - StackOverflowIcon, - ThreadsIcon, - TwitterIcon, UserIcon, - YoutubeIcon, TerminalIcon, } from '@dailydotdev/shared/src/components/icons'; import { @@ -40,6 +28,7 @@ import ControlledCoverUpload from '@dailydotdev/shared/src/components/profile/Co import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext'; import useUserInfoForm from '@dailydotdev/shared/src/hooks/useUserInfoForm'; import ControlledSwitch from '@dailydotdev/shared/src/components/fields/ControlledSwitch'; +import { SocialLinksInput } from '@dailydotdev/shared/src/components/profile/SocialLinksInput'; import { AccountPageContainer } from '../AccountPageContainer'; const Section = classed('section', 'flex flex-col gap-7'); @@ -133,88 +122,10 @@ const ProfileIndex = (): ReactElement => {
-
- - Links - - - Connect your profiles across the web. - -
- } - placeholder="Username or profile URL" - /> - } - placeholder="Username or profile URL" - /> - } - placeholder="https://" - /> - } - placeholder="Handle or profile URL" - /> - } - placeholder="Channel name or URL" - /> - } - placeholder="Profile URL" - /> - } - placeholder="Username or profile URL" - /> - } - placeholder="Username or profile URL" - /> - } - placeholder="Username or profile URL" - /> - } - placeholder="Full handle (user@instance) or URL" - /> - } - placeholder="Handle or profile URL" - /> - } - placeholder="Handle or profile URL" +
diff --git a/packages/webapp/pages/jobs/[id]/index.tsx b/packages/webapp/pages/jobs/[id]/index.tsx index ecb050858e..922fd355c5 100644 --- a/packages/webapp/pages/jobs/[id]/index.tsx +++ b/packages/webapp/pages/jobs/[id]/index.tsx @@ -26,16 +26,23 @@ import { ButtonVariant, } from '@dailydotdev/shared/src/components/buttons/Button'; import { + BlueskyIcon, + CodePenIcon, CrunchbaseIcon, FacebookIcon, GitHubIcon, GitLabIcon, + HashnodeIcon, InfoIcon, LinkedInIcon, MagicIcon, + MastodonIcon, MoveToIcon, OpenLinkIcon, + RedditIcon, + RoadmapIcon, StackOverflowIcon, + ThreadsIcon, TwitterIcon, YoutubeIcon, } from '@dailydotdev/shared/src/components/icons'; @@ -146,6 +153,13 @@ const socialMediaIconMap: SocialMediaIconMap = { [SocialMediaType.Medium]: , [SocialMediaType.DevTo]: , [SocialMediaType.StackOverflow]: , + [SocialMediaType.Threads]: , + [SocialMediaType.Bluesky]: , + [SocialMediaType.Mastodon]: , + [SocialMediaType.Roadmap]: , + [SocialMediaType.Codepen]: , + [SocialMediaType.Reddit]: , + [SocialMediaType.Hashnode]: , }; const locationTypeMap = {