11import { MDXContent } from "@content-collections/mdx/react" ;
22import { Icon } from "@iconify-icon/react" ;
33import { createFileRoute , Link , notFound } from "@tanstack/react-router" ;
4- import { ChevronLeft } from "lucide-react" ;
4+ import { Download } from "lucide-react" ;
5+ import { useState } from "react" ;
56import semver from "semver" ;
67
7- import { getChangelogBySlug , getChangelogList } from "@/changelog" ;
8+ import { cn } from "@hypr/utils" ;
9+
10+ import {
11+ type ChangelogWithMeta ,
12+ getChangelogBySlug ,
13+ getChangelogList ,
14+ } from "@/changelog" ;
815import { defaultMDXComponents } from "@/components/mdx" ;
916import { NotFoundContent } from "@/components/not-found" ;
17+ import { getDownloadLinks , groupDownloadLinks } from "@/utils/download" ;
1018
1119export const Route = createFileRoute ( "/_view/changelog/$slug" ) ( {
1220 component : Component ,
@@ -67,71 +75,56 @@ export const Route = createFileRoute("/_view/changelog/$slug")({
6775} ) ;
6876
6977function Component ( ) {
70- const { changelog, diffUrl } = Route . useLoaderData ( ) ;
78+ const { changelog, allChangelogs , diffUrl } = Route . useLoaderData ( ) ;
7179
7280 const currentVersion = semver . parse ( changelog . version ) ;
7381 const isPrerelease = ! ! (
7482 currentVersion && currentVersion . prerelease . length > 0
7583 ) ;
76- const isLatest = changelog . newerSlug === null ;
77-
78- let prereleaseType = "" ;
79- let buildNumber = "" ;
80- if ( isPrerelease && currentVersion && currentVersion . prerelease . length > 0 ) {
81- prereleaseType = currentVersion . prerelease [ 0 ] ?. toString ( ) || "" ;
82- buildNumber = currentVersion . prerelease [ 1 ] ?. toString ( ) || "" ;
83- }
8484
8585 return (
8686 < main
8787 className = "flex-1 bg-linear-to-b from-white via-stone-50/20 to-white min-h-screen"
8888 style = { { backgroundImage : "url(/patterns/dots.svg)" } }
8989 >
9090 < div className = "max-w-6xl mx-auto border-x border-neutral-100 bg-white" >
91- < div className = "max-w-3xl mx-auto px-6 py-16 lg:py-24" >
92- < div className = "text-center" >
93- < Link
94- to = "/changelog"
95- className = "inline-flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700 mb-8 transition-colors"
96- >
97- < ChevronLeft className = "w-4 h-4" />
98- All versions
99- </ Link >
91+ < div className = "max-w-3xl mx-auto px-6 pt-16 lg:pt-24 pb-8" >
92+ < div className = "hidden md:flex md:flex-col md:items-center gap-12" >
93+ < div className = "flex flex-col items-center gap-6" >
94+ < img
95+ src = {
96+ isPrerelease
97+ ? "/api/images/icons/nightly-icon.png"
98+ : "/api/images/icons/stable-icon.png"
99+ }
100+ alt = "Hyprnote"
101+ className = "size-32 rounded-2xl"
102+ />
103+ < h1 className = "text-3xl sm:text-4xl font-mono font-medium text-stone-600" >
104+ { changelog . version }
105+ </ h1 >
106+ </ div >
100107
101- < div className = "flex flex-wrap items-center justify-center gap-3 mb-8" >
102- < h1 className = "text-3xl sm:text-4xl font-serif tracking-tight text-stone-600" >
108+ < DownloadLinksHero version = { changelog . version } />
109+ </ div >
110+
111+ < div className = "md:hidden text-center" >
112+ < div className = "flex flex-col items-center gap-3 mb-8" >
113+ < img
114+ src = {
115+ isPrerelease
116+ ? "/api/images/icons/nightly-icon.png"
117+ : "/api/images/icons/stable-icon.png"
118+ }
119+ alt = "Hyprnote"
120+ className = "size-16 rounded-2xl"
121+ />
122+ < h1 className = "text-3xl font-mono font-medium text-stone-600" >
103123 { changelog . version }
104124 </ h1 >
105- { isLatest && (
106- < span className = "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-linear-to-t from-amber-200 to-amber-100 text-amber-900 rounded-full" >
107- < Icon icon = "ri:rocket-fill" className = "text-xs" />
108- Latest
109- </ span >
110- ) }
111- { prereleaseType && (
112- < span className = "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-linear-to-b from-[#03BCF1] to-[#127FE5] text-white rounded-full" >
113- < Icon icon = "ri:moon-fill" className = "text-xs" />
114- { prereleaseType }
115- </ span >
116- ) }
117- { buildNumber && (
118- < span className = "inline-flex items-center px-2 py-0.5 text-xs font-medium bg-linear-to-t from-neutral-200 to-neutral-100 text-neutral-900 rounded-full" >
119- #{ buildNumber }
120- </ span >
121- ) }
122125 </ div >
123126
124- { diffUrl && (
125- < a
126- href = { diffUrl }
127- target = "_blank"
128- rel = "noopener noreferrer"
129- className = "inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium bg-linear-to-t from-stone-600 to-stone-500 text-white rounded-full shadow-md hover:shadow-lg hover:scale-[102%] active:scale-[98%] transition-all"
130- >
131- < Icon icon = "mdi:github" className = "text-base" />
132- View Diff
133- </ a >
134- ) }
127+ < DownloadLinksHeroMobile version = { changelog . version } />
135128 </ div >
136129
137130 < article className = "mt-12 prose prose-stone prose-headings:font-serif prose-headings:font-semibold prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h3:text-xl prose-h3:mt-6 prose-h3:mb-3 prose-h4:text-lg prose-h4:mt-4 prose-h4:mb-2 prose-a:text-stone-600 prose-a:underline prose-a:decoration-dotted hover:prose-a:text-stone-800 prose-code:bg-stone-50 prose-code:border prose-code:border-neutral-200 prose-code:rounded prose-code:px-1.5 prose-code:py-0.5 prose-code:text-sm prose-code:font-mono prose-code:text-stone-700 prose-pre:bg-stone-50 prose-pre:border prose-pre:border-neutral-200 prose-pre:rounded-sm prose-pre:prose-code:bg-transparent prose-pre:prose-code:border-0 prose-pre:prose-code:p-0 prose-img:rounded-lg prose-img:border prose-img:border-neutral-200 prose-img:my-6 max-w-none" >
@@ -141,7 +134,268 @@ function Component() {
141134 />
142135 </ article >
143136 </ div >
137+
138+ { diffUrl && (
139+ < >
140+ < div className = "border-t border-neutral-100" />
141+ < div className = "max-w-3xl mx-auto px-6 py-16 flex flex-col items-center text-center" >
142+ < h2 className = "text-3xl font-serif text-stone-600 mb-2" >
143+ View the Code
144+ </ h2 >
145+ < p className = "text-neutral-600 mb-6" >
146+ Curious about what changed? See the full diff on GitHub.
147+ </ p >
148+ < a
149+ href = { diffUrl }
150+ target = "_blank"
151+ rel = "noopener noreferrer"
152+ className = "inline-flex items-center gap-2 px-6 h-12 text-base font-medium bg-linear-to-t from-neutral-800 to-neutral-700 text-white rounded-full shadow-md hover:shadow-lg hover:scale-[102%] active:scale-[98%] transition-all"
153+ >
154+ < Icon icon = "mdi:github" className = "text-xl" />
155+ View Diff on GitHub
156+ </ a >
157+ </ div >
158+ </ >
159+ ) }
160+
161+ < div className = "border-t border-neutral-100" />
162+
163+ < div className = "max-w-3xl mx-auto px-6 py-16" >
164+ < RelatedReleases
165+ currentSlug = { changelog . slug }
166+ allChangelogs = { allChangelogs }
167+ />
168+ </ div >
144169 </ div >
145170 </ main >
146171 ) ;
147172}
173+
174+ function DownloadLinksHero ( { version } : { version : string } ) {
175+ const links = getDownloadLinks ( version ) ;
176+ const grouped = groupDownloadLinks ( links ) ;
177+
178+ return (
179+ < div className = "flex items-start gap-8" >
180+ < div className = "flex flex-col items-center gap-2" >
181+ < h3 className = "flex items-center gap-1.5 text-xs font-medium text-stone-500 uppercase tracking-wider" >
182+ < Icon icon = "simple-icons:apple" className = "text-sm" />
183+ macOS
184+ </ h3 >
185+ < div className = "flex flex-col gap-1.5" >
186+ { grouped . macos . map ( ( link ) => (
187+ < a
188+ key = { link . url }
189+ href = { link . url }
190+ className = { cn ( [
191+ "flex items-center justify-center gap-2 px-4 h-8 text-sm rounded-full transition-all" ,
192+ "bg-linear-to-b from-white to-stone-50 text-neutral-700" ,
193+ "border border-neutral-300" ,
194+ "hover:shadow-sm hover:scale-[102%] active:scale-[98%]" ,
195+ ] ) }
196+ >
197+ < Download className = "size-3.5 shrink-0" />
198+ < span > { link . label } </ span >
199+ </ a >
200+ ) ) }
201+ </ div >
202+ </ div >
203+
204+ < div className = "flex flex-col items-center gap-2" >
205+ < h3 className = "flex items-center gap-1.5 text-xs font-medium text-stone-500 uppercase tracking-wider" >
206+ < Icon icon = "simple-icons:linux" className = "text-sm" />
207+ Linux
208+ </ h3 >
209+ < div className = "flex flex-col gap-1.5" >
210+ { grouped . linux . map ( ( link ) => (
211+ < a
212+ key = { link . url }
213+ href = { link . url }
214+ className = { cn ( [
215+ "flex items-center justify-center gap-2 px-4 h-8 text-sm rounded-full transition-all" ,
216+ "bg-linear-to-b from-white to-stone-50 text-neutral-700" ,
217+ "border border-neutral-300" ,
218+ "hover:shadow-sm hover:scale-[102%] active:scale-[98%]" ,
219+ ] ) }
220+ >
221+ < Download className = "size-3.5 shrink-0" />
222+ < span > { link . label } </ span >
223+ </ a >
224+ ) ) }
225+ </ div >
226+ </ div >
227+ </ div >
228+ ) ;
229+ }
230+
231+ function DownloadLinksHeroMobile ( { version } : { version : string } ) {
232+ const links = getDownloadLinks ( version ) ;
233+ const grouped = groupDownloadLinks ( links ) ;
234+ const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
235+
236+ const allLinks = [ ...grouped . macos , ...grouped . linux ] ;
237+
238+ return (
239+ < div className = "w-full max-w-sm" >
240+ < div className = "relative" >
241+ < div className = "overflow-hidden" >
242+ < div
243+ className = "flex transition-transform duration-300 ease-in-out"
244+ style = { { transform : `translateX(-${ activeIndex * 100 } %)` } }
245+ >
246+ { allLinks . map ( ( link ) => (
247+ < div key = { link . url } className = "w-full flex-shrink-0 px-2" >
248+ < a
249+ href = { link . url }
250+ className = { cn ( [
251+ "flex flex-col items-center gap-2 px-6 py-4 rounded-2xl transition-all" ,
252+ "bg-linear-to-b from-white to-stone-50 text-neutral-700" ,
253+ "border border-neutral-300" ,
254+ "hover:shadow-sm active:scale-[98%]" ,
255+ ] ) }
256+ >
257+ < Download className = "size-5 shrink-0" />
258+ < div className = "text-center" >
259+ < div className = "text-xs font-medium text-stone-500 uppercase tracking-wider mb-1" >
260+ { link . platform }
261+ </ div >
262+ < div className = "text-sm font-medium" > { link . label } </ div >
263+ </ div >
264+ </ a >
265+ </ div >
266+ ) ) }
267+ </ div >
268+ </ div >
269+
270+ < div className = "flex justify-center gap-2 mt-3" >
271+ { allLinks . map ( ( _ , index ) => (
272+ < button
273+ key = { index }
274+ onClick = { ( ) => setActiveIndex ( index ) }
275+ className = { cn ( [
276+ "h-1.5 rounded-full transition-all" ,
277+ activeIndex === index
278+ ? "w-6 bg-stone-600"
279+ : "w-1.5 bg-stone-300 hover:bg-stone-400" ,
280+ ] ) }
281+ />
282+ ) ) }
283+ </ div >
284+ </ div >
285+ </ div >
286+ ) ;
287+ }
288+
289+ function RelatedReleases ( {
290+ currentSlug,
291+ allChangelogs,
292+ } : {
293+ currentSlug : string ;
294+ allChangelogs : ChangelogWithMeta [ ] ;
295+ } ) {
296+ const currentIndex = allChangelogs . findIndex ( ( c ) => c . slug === currentSlug ) ;
297+ if ( currentIndex === - 1 ) return null ;
298+
299+ const total = allChangelogs . length ;
300+ let startIndex : number ;
301+ let endIndex : number ;
302+
303+ if ( total <= 5 ) {
304+ startIndex = 0 ;
305+ endIndex = total ;
306+ } else if ( currentIndex <= 2 ) {
307+ startIndex = 0 ;
308+ endIndex = 5 ;
309+ } else if ( currentIndex >= total - 2 ) {
310+ startIndex = total - 5 ;
311+ endIndex = total ;
312+ } else {
313+ startIndex = currentIndex - 2 ;
314+ endIndex = currentIndex + 3 ;
315+ }
316+
317+ const relatedChangelogs = allChangelogs . slice ( startIndex , endIndex ) ;
318+
319+ return (
320+ < section >
321+ < div className = "text-center mb-8" >
322+ < h2 className = "text-3xl font-serif text-stone-600 mb-2" >
323+ Other Releases
324+ </ h2 >
325+ < p className = "text-neutral-600" > Explore more versions of Hyprnote</ p >
326+ </ div >
327+
328+ < div className = "grid gap-4 grid-cols-5" >
329+ { relatedChangelogs . map ( ( release ) => {
330+ const version = semver . parse ( release . version ) ;
331+ const isPrerelease = version && version . prerelease . length > 0 ;
332+ const nightlyNumber =
333+ isPrerelease && version ?. prerelease [ 0 ] === "nightly"
334+ ? version . prerelease [ 1 ]
335+ : null ;
336+ const isCurrent = release . slug === currentSlug ;
337+
338+ return (
339+ < Link
340+ key = { release . slug }
341+ to = "/changelog/$slug"
342+ params = { { slug : release . slug } }
343+ className = { cn ( [
344+ "group block" ,
345+ isCurrent && "pointer-events-none" ,
346+ ] ) }
347+ >
348+ < article
349+ className = { cn ( [
350+ "flex flex-col items-center gap-2 p-4 rounded-lg transition-all duration-300" ,
351+ isCurrent ? "bg-stone-100" : "hover:bg-stone-50" ,
352+ ] ) }
353+ >
354+ < img
355+ src = {
356+ isPrerelease
357+ ? "/api/images/icons/nightly-icon.png"
358+ : "/api/images/icons/stable-icon.png"
359+ }
360+ alt = "Hyprnote"
361+ className = { cn ( [
362+ "size-12 rounded-xl transition-all duration-300" ,
363+ ! isCurrent && "group-hover:scale-110" ,
364+ ] ) }
365+ />
366+
367+ < div className = "flex items-center gap-1.5" >
368+ < h3
369+ className = { cn ( [
370+ "text-sm font-mono font-medium text-stone-600 transition-colors" ,
371+ ! isCurrent && "group-hover:text-stone-800" ,
372+ ] ) }
373+ >
374+ { version
375+ ? `${ version . major } .${ version . minor } .${ version . patch } `
376+ : release . version }
377+ </ h3 >
378+ { nightlyNumber !== null && (
379+ < span className = "inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-stone-200 text-stone-600 rounded-full" >
380+ #{ nightlyNumber }
381+ </ span >
382+ ) }
383+ </ div >
384+ </ article >
385+ </ Link >
386+ ) ;
387+ } ) }
388+ </ div >
389+
390+ < div className = "text-center mt-8" >
391+ < Link
392+ to = "/changelog"
393+ className = "inline-flex items-center gap-2 px-6 h-12 text-base font-medium bg-linear-to-b from-white to-stone-50 text-neutral-700 border border-neutral-300 rounded-full shadow-sm hover:shadow-md hover:scale-[102%] active:scale-[98%] transition-all"
394+ >
395+ View all releases
396+ < Icon icon = "mdi:arrow-right" className = "text-base" />
397+ </ Link >
398+ </ div >
399+ </ section >
400+ ) ;
401+ }
0 commit comments