a tool for shared writing and social publishing

parse and render description

+58 -4
+58 -4
app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
··· 9 9 import { Json } from "supabase/database.types"; 10 10 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 11 11 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 12 + import { SpeedyLink } from "components/SpeedyLink"; 13 + import { ReactNode } from "react"; 12 14 13 15 export const ProfileHeader = (props: { 14 16 profile: ProfileViewDetailed; 15 17 publications: { record: Json; uri: string }[]; 16 18 }) => { 17 - console.log(props.profile); 18 19 let profileRecord = props.profile; 19 20 20 21 return ( ··· 37 38 @{props.profile.handle} 38 39 </div> 39 40 )} 40 - <div className="text-secondary px-3 sm:px-4 "> 41 - {profileRecord.description} 42 - </div> 41 + <pre className="text-secondary px-3 sm:px-4 "> 42 + {profileRecord.description 43 + ? parseDescription(profileRecord.description) 44 + : null} 45 + </pre> 43 46 <div className=" w-full overflow-x-scroll mt-3 mb-6 "> 44 47 <div className="grid grid-flow-col auto-cols-[164px] sm:auto-cols-[240px] gap-2 mx-auto w-fit px-3 sm:px-4 "> 45 48 {/*<div className="spacer "/>*/} ··· 103 106 </a> 104 107 ); 105 108 }; 109 + 110 + function parseDescription(description: string): ReactNode[] { 111 + const combinedRegex = /(@\S+|https?:\/\/\S+)/g; 112 + 113 + const parts: ReactNode[] = []; 114 + let lastIndex = 0; 115 + let match; 116 + let key = 0; 117 + 118 + while ((match = combinedRegex.exec(description)) !== null) { 119 + // Add text before this match 120 + if (match.index > lastIndex) { 121 + parts.push(description.slice(lastIndex, match.index)); 122 + } 123 + 124 + const matched = match[0]; 125 + 126 + if (matched.startsWith("@")) { 127 + // It's a mention 128 + const handle = matched.slice(1); 129 + parts.push( 130 + <SpeedyLink key={key++} href={`/p/${handle}`}> 131 + {matched} 132 + </SpeedyLink>, 133 + ); 134 + } else { 135 + // It's a URL 136 + const urlWithoutProtocol = matched 137 + .replace(/^https?:\/\//, "") 138 + .replace(/\/+$/, ""); 139 + const displayText = 140 + urlWithoutProtocol.length > 50 141 + ? urlWithoutProtocol.slice(0, 50) + "…" 142 + : urlWithoutProtocol; 143 + parts.push( 144 + <a key={key++} href={matched} target="_blank" rel="noopener noreferrer"> 145 + {displayText} 146 + </a>, 147 + ); 148 + } 149 + 150 + lastIndex = match.index + matched.length; 151 + } 152 + 153 + // Add remaining text after last match 154 + if (lastIndex < description.length) { 155 + parts.push(description.slice(lastIndex)); 156 + } 157 + 158 + return parts; 159 + }