Skip to content

Commit a3d86dd

Browse files
committed
feat(changelog): added changelog
1 parent 8eaa83f commit a3d86dd

File tree

6 files changed

+371
-2
lines changed

6 files changed

+371
-2
lines changed

apps/sim/app/(landing)/components/footer/footer.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ export default function Footer({ fullWidth = false }: FooterProps) {
214214
>
215215
Enterprise
216216
</Link>
217+
<Link
218+
href='/changelog'
219+
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
220+
>
221+
Changelog
222+
</Link>
217223
<Link
218224
href='/privacy'
219225
target='_blank'
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export const dynamic = 'force-static'
4+
export const revalidate = 3600
5+
6+
interface Release {
7+
id: number
8+
tag_name: string
9+
name: string
10+
body: string
11+
html_url: string
12+
published_at: string
13+
prerelease: boolean
14+
}
15+
16+
function escapeXml(str: string) {
17+
return str
18+
.replace(/&/g, '&amp;')
19+
.replace(/</g, '&lt;')
20+
.replace(/>/g, '&gt;')
21+
.replace(/"/g, '&quot;')
22+
.replace(/'/g, '&apos;')
23+
}
24+
25+
export async function GET() {
26+
try {
27+
const res = await fetch('https://api.github.com/repos/simstudioai/sim/releases', {
28+
headers: { Accept: 'application/vnd.github+json' },
29+
next: { revalidate },
30+
})
31+
const releases: Release[] = await res.json()
32+
const items = (releases || [])
33+
.filter((r) => !r.prerelease)
34+
.map(
35+
(r) => `
36+
<item>
37+
<title>${escapeXml(r.name || r.tag_name)}</title>
38+
<link>${r.html_url}</link>
39+
<guid isPermaLink="true">${r.html_url}</guid>
40+
<pubDate>${new Date(r.published_at).toUTCString()}</pubDate>
41+
<description><![CDATA[${r.body || ''}]]></description>
42+
</item>
43+
`
44+
)
45+
.join('')
46+
47+
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
48+
<rss version="2.0">
49+
<channel>
50+
<title>Sim Changelog</title>
51+
<link>https://sim.dev/changelog</link>
52+
<description>Latest changes, fixes and updates in Sim.</description>
53+
<language>en-us</language>
54+
${items}
55+
</channel>
56+
</rss>`
57+
58+
return new NextResponse(xml, {
59+
status: 200,
60+
headers: {
61+
'Content-Type': 'application/rss+xml; charset=utf-8',
62+
'Cache-Control': `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}`,
63+
},
64+
})
65+
} catch {
66+
return new NextResponse('Service Unavailable', { status: 503 })
67+
}
68+
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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(/&nbsp/g, '')
31+
const mentions = mentionMatches.map((match) => {
32+
const username = match.slice(1)
33+
const avatarUrl = `https://github.com/${username}.png`
34+
return `[![${match}](${avatarUrl})](https://github.com/${username})`
35+
})
36+
return `${line.replace(/&nbsp/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+
}

apps/sim/app/changelog/layout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Nav from '@/app/(landing)/components/nav/nav'
2+
3+
export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
4+
return (
5+
<div className='min-h-screen bg-background font-geist-sans text-foreground'>
6+
<Nav />
7+
{children}
8+
</div>
9+
)
10+
}

apps/sim/app/changelog/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Metadata } from 'next'
2+
import ChangelogContent from './components/changelog-content'
3+
4+
export const metadata: Metadata = {
5+
title: 'Changelog | Sim - AI Agent Workflow Builder',
6+
description: 'Stay up-to-date with the latest features, improvements, and bug fixes in Sim.',
7+
openGraph: {
8+
title: 'Changelog | Sim - AI Agent Workflow Builder',
9+
description: 'Stay up-to-date with the latest features, improvements, and bug fixes in Sim.',
10+
type: 'website',
11+
},
12+
}
13+
14+
export default function ChangelogPage() {
15+
return <ChangelogContent />
16+
}

apps/sim/app/conditional-theme-provider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
77
export function ConditionalThemeProvider({ children, ...props }: ThemeProviderProps) {
88
const pathname = usePathname()
99

10-
// Force light mode for landing page (root path and /homepage), auth verify, invite, and legal pages
10+
// Force light mode for certain pages
1111
const forcedTheme =
1212
pathname === '/' ||
1313
pathname === '/homepage' ||
@@ -16,7 +16,8 @@ export function ConditionalThemeProvider({ children, ...props }: ThemeProviderPr
1616
pathname.startsWith('/terms') ||
1717
pathname.startsWith('/privacy') ||
1818
pathname.startsWith('/invite') ||
19-
pathname.startsWith('/verify')
19+
pathname.startsWith('/verify') ||
20+
pathname.startsWith('/changelog')
2021
? 'light'
2122
: undefined
2223

0 commit comments

Comments
 (0)