|
| 1 | +import { BookOpen, ExternalLink, Github, Rss } from 'lucide-react' |
| 2 | +import Link from 'next/link' |
| 3 | +import ReactMarkdown from 'react-markdown' |
| 4 | +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' |
| 5 | +import { Badge } from '@/components/ui/badge' |
| 6 | +import { Card, CardContent, CardHeader } from '@/components/ui/card' |
| 7 | +import { inter } from '@/app/fonts/inter' |
| 8 | +import { soehne } from '@/app/fonts/soehne/soehne' |
| 9 | + |
| 10 | +interface ChangelogEntry { |
| 11 | + tag: string |
| 12 | + title: string |
| 13 | + content: string |
| 14 | + date: string |
| 15 | + url: string |
| 16 | + contributors?: string[] |
| 17 | +} |
| 18 | + |
| 19 | +function extractMentions(body: string): string[] { |
| 20 | + const matches = body.match(/@([A-Za-z0-9-]+)/g) ?? [] |
| 21 | + const uniq = Array.from(new Set(matches.map((m) => m.slice(1)))) |
| 22 | + return uniq |
| 23 | +} |
| 24 | + |
| 25 | +function enhanceContent(body: string): string { |
| 26 | + const lines = body.split('\n') |
| 27 | + const newLines = lines.map((line) => { |
| 28 | + if (line.trim().startsWith('- ')) { |
| 29 | + const mentionMatches = line.match(/@([A-Za-z0-9-]+)/g) ?? [] |
| 30 | + if (mentionMatches.length === 0) return line.replace(/ /g, '') |
| 31 | + const mentions = mentionMatches.map((match) => { |
| 32 | + const username = match.slice(1) |
| 33 | + const avatarUrl = `https://github.com/${username}.png` |
| 34 | + return `[](https://github.com/${username})` |
| 35 | + }) |
| 36 | + return `${line.replace(/ /g, '')} – ${mentions.join(' ')}` |
| 37 | + } |
| 38 | + return line |
| 39 | + }) |
| 40 | + return newLines.join('\n') |
| 41 | +} |
| 42 | + |
| 43 | +export default async function ChangelogContent() { |
| 44 | + let entries: ChangelogEntry[] = [] |
| 45 | + |
| 46 | + try { |
| 47 | + const res = await fetch('https://api.github.com/repos/simstudioai/sim/releases', { |
| 48 | + headers: { Accept: 'application/vnd.github+json' }, |
| 49 | + // Cache for 1 hour |
| 50 | + next: { revalidate: 3600 }, |
| 51 | + }) |
| 52 | + const releases: any[] = await res.json() |
| 53 | + entries = (releases || []) |
| 54 | + .filter((r) => !r.prerelease) |
| 55 | + .map((r) => ({ |
| 56 | + tag: r.tag_name, |
| 57 | + title: r.name || r.tag_name, |
| 58 | + content: enhanceContent(String(r.body || '')), |
| 59 | + date: r.published_at, |
| 60 | + url: r.html_url, |
| 61 | + contributors: extractMentions(String(r.body || '')), |
| 62 | + })) |
| 63 | + } catch (err) { |
| 64 | + // Fail silently; show empty state |
| 65 | + entries = [] |
| 66 | + } |
| 67 | + |
| 68 | + return ( |
| 69 | + <div className='min-h-screen bg-background'> |
| 70 | + <div className='relative grid md:grid-cols-2'> |
| 71 | + {/* Left intro panel */} |
| 72 | + <div className='relative top-0 overflow-hidden border-border border-b px-6 py-16 sm:px-10 md:sticky md:h-dvh md:border-r md:border-b-0 md:px-12 md:py-24'> |
| 73 | + <div className='absolute inset-0 bg-grid-pattern opacity-[0.03] dark:opacity-[0.06]' /> |
| 74 | + <div className='absolute inset-0 bg-gradient-to-tr from-background via-transparent to-background/60' /> |
| 75 | + |
| 76 | + <div className='relative mx-auto h-full max-w-xl md:flex md:flex-col md:justify-center'> |
| 77 | + <h1 |
| 78 | + className={`${soehne.className} mt-6 font-semibold text-4xl tracking-tight sm:text-5xl`} |
| 79 | + > |
| 80 | + Changelog |
| 81 | + </h1> |
| 82 | + <p className={`${inter.className} mt-4 text-muted-foreground text-sm`}> |
| 83 | + Stay up-to-date with the latest features, improvements, and bug fixes in Sim. All |
| 84 | + changes are documented here with detailed release notes. |
| 85 | + </p> |
| 86 | + <hr className='mt-6 border-border' /> |
| 87 | + |
| 88 | + <div className='mt-6 flex flex-wrap items-center gap-3 text-sm'> |
| 89 | + <Link |
| 90 | + href='https://github.com/simstudioai/sim/releases' |
| 91 | + target='_blank' |
| 92 | + rel='noopener noreferrer' |
| 93 | + className='group inline-flex items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[14px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all sm:text-[16px]' |
| 94 | + > |
| 95 | + <Github className='h-4 w-4' /> |
| 96 | + View on GitHub |
| 97 | + </Link> |
| 98 | + <Link |
| 99 | + href='/docs' |
| 100 | + className='inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 hover:bg-muted' |
| 101 | + > |
| 102 | + <BookOpen className='h-4 w-4' /> |
| 103 | + Documentation |
| 104 | + </Link> |
| 105 | + <Link |
| 106 | + href='/changelog.xml' |
| 107 | + className='inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 hover:bg-muted' |
| 108 | + > |
| 109 | + <Rss className='h-4 w-4' /> |
| 110 | + RSS Feed |
| 111 | + </Link> |
| 112 | + </div> |
| 113 | + </div> |
| 114 | + </div> |
| 115 | + |
| 116 | + {/* Right timeline */} |
| 117 | + <div className='relative px-4 py-10 sm:px-6 md:px-8 md:py-12'> |
| 118 | + <div className='-translate-x-full absolute top-0 left-0 hidden h-full w-px bg-gradient-to-b from-foreground/10 via-transparent to-transparent md:block' /> |
| 119 | + <div className='max-w-2xl'> |
| 120 | + <div className='space-y-12'> |
| 121 | + {entries.map((entry) => ( |
| 122 | + <Card key={entry.tag} className='border border-border bg-card shadow-sm'> |
| 123 | + <CardHeader className='pb-6'> |
| 124 | + <div className='mb-4 flex items-center justify-between'> |
| 125 | + <Badge |
| 126 | + variant='secondary' |
| 127 | + className='border-brand-primary/20 bg-brand-primary/10 px-2.5 py-1 font-mono text-brand-primary text-xs' |
| 128 | + > |
| 129 | + {entry.tag} |
| 130 | + </Badge> |
| 131 | + <div |
| 132 | + className={`${inter.className} flex items-center gap-2 text-muted-foreground text-sm`} |
| 133 | + > |
| 134 | + {new Date(entry.date).toLocaleDateString('en-US', { |
| 135 | + year: 'numeric', |
| 136 | + month: 'long', |
| 137 | + day: 'numeric', |
| 138 | + })} |
| 139 | + <a |
| 140 | + href={entry.url} |
| 141 | + target='_blank' |
| 142 | + rel='noopener noreferrer' |
| 143 | + className='inline-flex items-center gap-1 text-muted-foreground hover:text-brand-primary' |
| 144 | + > |
| 145 | + <ExternalLink className='h-4 w-4' /> |
| 146 | + </a> |
| 147 | + </div> |
| 148 | + </div> |
| 149 | + <h2 className={`${soehne.className} font-semibold text-2xl tracking-tight`}> |
| 150 | + {entry.title} |
| 151 | + </h2> |
| 152 | + |
| 153 | + {entry.contributors && entry.contributors.length > 0 && ( |
| 154 | + <div className='mt-4 flex items-center gap-3'> |
| 155 | + <span className={`${inter.className} text-muted-foreground text-sm`}> |
| 156 | + Contributors: |
| 157 | + </span> |
| 158 | + <div className='-space-x-2 flex items-center'> |
| 159 | + {entry.contributors.slice(0, 5).map((contributor) => ( |
| 160 | + <Avatar |
| 161 | + key={contributor} |
| 162 | + className='h-6 w-6 border-2 border-background' |
| 163 | + > |
| 164 | + <AvatarImage |
| 165 | + src={`https://github.com/${contributor}.png`} |
| 166 | + alt={`@${contributor}`} |
| 167 | + /> |
| 168 | + <AvatarFallback className='bg-muted text-xs'> |
| 169 | + {contributor.charAt(0).toUpperCase()} |
| 170 | + </AvatarFallback> |
| 171 | + </Avatar> |
| 172 | + ))} |
| 173 | + {entry.contributors.length > 5 && ( |
| 174 | + <div className='flex h-6 w-6 items-center justify-center rounded-full border-2 border-background bg-muted'> |
| 175 | + <span className='font-medium text-muted-foreground text-xs'> |
| 176 | + +{entry.contributors.length - 5} |
| 177 | + </span> |
| 178 | + </div> |
| 179 | + )} |
| 180 | + </div> |
| 181 | + </div> |
| 182 | + )} |
| 183 | + </CardHeader> |
| 184 | + <CardContent className='pt-0'> |
| 185 | + <div |
| 186 | + className={`${inter.className} prose prose-sm dark:prose-invert max-w-none prose-code:rounded prose-pre:border prose-pre:border-border prose-code:bg-muted prose-pre:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-headings:font-semibold prose-a:text-brand-primary prose-code:text-foreground prose-headings:text-foreground prose-p:text-muted-foreground prose-a:no-underline hover:prose-a:underline`} |
| 187 | + > |
| 188 | + <ReactMarkdown |
| 189 | + components={{ |
| 190 | + h2: ({ children, ...props }) => ( |
| 191 | + <h3 |
| 192 | + className='mt-6 mb-3 font-semibold text-lg tracking-tight first:mt-0' |
| 193 | + {...props} |
| 194 | + > |
| 195 | + {children} |
| 196 | + </h3> |
| 197 | + ), |
| 198 | + h3: ({ children, ...props }) => ( |
| 199 | + <h4 |
| 200 | + className='mt-5 mb-2 font-medium text-base tracking-tight' |
| 201 | + {...props} |
| 202 | + > |
| 203 | + {children} |
| 204 | + </h4> |
| 205 | + ), |
| 206 | + ul: ({ children, ...props }) => ( |
| 207 | + <ul className='mt-2 mb-4 space-y-1' {...props}> |
| 208 | + {children} |
| 209 | + </ul> |
| 210 | + ), |
| 211 | + li: ({ children, ...props }) => ( |
| 212 | + <li className='text-muted-foreground leading-relaxed' {...props}> |
| 213 | + {children} |
| 214 | + </li> |
| 215 | + ), |
| 216 | + p: ({ children, ...props }) => ( |
| 217 | + <p className='mb-4 text-muted-foreground leading-relaxed' {...props}> |
| 218 | + {children} |
| 219 | + </p> |
| 220 | + ), |
| 221 | + strong: ({ children, ...props }) => ( |
| 222 | + <strong className='font-medium text-foreground' {...props}> |
| 223 | + {children} |
| 224 | + </strong> |
| 225 | + ), |
| 226 | + code: ({ children, ...props }) => ( |
| 227 | + <code |
| 228 | + className='rounded bg-muted px-1.5 py-0.5 font-mono text-foreground text-sm' |
| 229 | + {...props} |
| 230 | + > |
| 231 | + {children} |
| 232 | + </code> |
| 233 | + ), |
| 234 | + img: ({ ...props }) => ( |
| 235 | + <img |
| 236 | + className='inline-block h-6 w-6 rounded-full border opacity-70' |
| 237 | + {...(props as any)} |
| 238 | + /> |
| 239 | + ), |
| 240 | + a: ({ className, ...props }: any) => ( |
| 241 | + <a |
| 242 | + {...props} |
| 243 | + className={`underline ${className ?? ''}`} |
| 244 | + target='_blank' |
| 245 | + rel='noreferrer' |
| 246 | + /> |
| 247 | + ), |
| 248 | + }} |
| 249 | + > |
| 250 | + {entry.content} |
| 251 | + </ReactMarkdown> |
| 252 | + </div> |
| 253 | + </CardContent> |
| 254 | + </Card> |
| 255 | + ))} |
| 256 | + |
| 257 | + {entries.length === 0 && ( |
| 258 | + <div className='text-muted-foreground text-sm'> |
| 259 | + No releases found yet. Check back soon. |
| 260 | + </div> |
| 261 | + )} |
| 262 | + </div> |
| 263 | + </div> |
| 264 | + </div> |
| 265 | + </div> |
| 266 | + </div> |
| 267 | + ) |
| 268 | +} |
0 commit comments