Skip to content

Commit 6e9daff

Browse files
Add download page for nightly versions (#11)
* Vibe coded nightly page * Some improvements * Clean the release body * Clean up some vibe code crap
1 parent b458b22 commit 6e9daff

File tree

6 files changed

+300
-7
lines changed

6 files changed

+300
-7
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"dev": "vite",
88
"build": "tsc && vite build && sscli -b https://chainner.app -r ./dist/client",
99
"preview": "vite preview",
10-
"lint": "eslint . --ext .ts",
10+
"lint": "eslint .",
11+
"lint:fix": "eslint . --fix",
1112
"add-domain": "echo \"chainner.app\" > dist/CNAME"
1213
},
1314
"dependencies": {

src/api/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
12
import fetch from 'cross-fetch';
23
import { IGithubRelease } from '../types/githubTypes';
34

@@ -35,3 +36,8 @@ export const getAllVersions = async (): Promise<IGithubRelease[] | undefined> =>
3536
export const getRepoInfo = async (): Promise<any | undefined> => {
3637
return await fetchCached('https://api.github.com/repos/chaiNNer-org/chaiNNer');
3738
};
39+
40+
// Nightly (separate repository)
41+
export const getAllNightlyVersions = async (): Promise<IGithubRelease[] | undefined> => {
42+
return await fetchCached('https://api.github.com/repos/chaiNNer-org/chaiNNer-nightly/releases');
43+
};

src/pages/download.page.server.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { PageContextServer } from '../types';
44
import { IGithubRelease } from '../types/githubTypes';
55

66
async function onBeforeRender(pageContext: PageContextServer) {
7-
// `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here.
7+
const allVersions = (await getAllVersions()) ?? [];
8+
const ordered = allVersions
9+
.filter((r) => !r.draft)
10+
.sort((a, b) => new Date(b.published_at || b.created_at).getTime() - new Date(a.published_at || a.created_at).getTime());
11+
812
const data = await getLatestVersion();
913

1014
const dmgBuild = data?.assets.find((asset) => asset.name.endsWith('.dmg'));
@@ -17,13 +21,12 @@ async function onBeforeRender(pageContext: PageContextServer) {
1721
const macZip = zipBuilds?.find((asset) => asset.name.includes('mac'));
1822
const linuxZip = zipBuilds?.find((asset) => asset.name.includes('linux'));
1923

20-
const allVersions = (await getAllVersions()) ?? [];
2124
const computedChangelog = allVersions.reduce((acc: string, curr: IGithubRelease) => {
2225
return `${acc}\n# ${curr.name}\n${curr.body}`;
2326
}, '');
2427

2528
// available as `pageContext.pageProps`
26-
const pageProps = { dmgBuild, exeBuild, debBuild, rpmBuild, winZip, macZip, linuxZip, computedChangelog, latestVersion: data };
29+
const pageProps = { dmgBuild, exeBuild, debBuild, rpmBuild, winZip, macZip, linuxZip, latestVersion: data, computedChangelog };
2730
return {
2831
pageContext: {
2932
pageProps,

src/pages/download.page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ShellWrapper } from '../components/PageShell/PageShell';
1515
import ChakraUIRenderer from '../utils/chakra-ui-markdown-renderer';
1616

1717
function Page(pageProps: {
18-
latestVersion: IGithubRelease;
18+
latestVersion: IGithubRelease | undefined;
1919
dmgBuild: IReleaseAsset | undefined;
2020
exeBuild: IReleaseAsset | undefined;
2121
debBuild: IReleaseAsset | undefined;
@@ -98,7 +98,7 @@ function Page(pageProps: {
9898
height="auto"
9999
px={8}
100100
py={7}
101-
disabled={!isSupportedOS}
101+
disabled={!isSupportedOS || currentBuild == null}
102102
>
103103
<VStack>
104104
<HStack>
@@ -114,7 +114,7 @@ function Page(pageProps: {
114114
}}
115115
fontWeight="bold"
116116
>
117-
{latestVersion != null ? `Download ${latestVersion?.name}` : 'Loading...'}
117+
{latestVersion != null ? `Download ${latestVersion?.name}` : 'No stable release found'}
118118
</Text>
119119
</HStack>
120120
<HStack>
@@ -134,6 +134,15 @@ function Page(pageProps: {
134134
</Link>
135135
</Text>
136136
)}
137+
<Text color="white">
138+
Want cutting-edge features?{' '}
139+
<Link
140+
color="blue.300"
141+
href="/nightly"
142+
>
143+
Try Nightly builds
144+
</Link>
145+
</Text>
137146
</VStack>
138147
<Box
139148
color="white"

src/pages/nightly.page.server.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
2+
import { getAllNightlyVersions } from '../api/api';
3+
import { PageContextServer } from '../types';
4+
import { IGithubRelease } from '../types/githubTypes';
5+
6+
async function onBeforeRender(pageContext: PageContextServer) {
7+
const allVersions = (await getAllNightlyVersions()) ?? [];
8+
const ordered = allVersions
9+
.filter((r) => !r.draft)
10+
.sort((a, b) => new Date(b.published_at || b.created_at).getTime() - new Date(a.published_at || a.created_at).getTime());
11+
12+
const latest = ordered[0];
13+
14+
const dmgBuild = latest?.assets.find((asset) => asset.name.endsWith('.dmg'));
15+
const exeBuild = latest?.assets.find((asset) => asset.name.endsWith('.exe'));
16+
const debBuild = latest?.assets.find((asset) => asset.name.endsWith('.deb'));
17+
const rpmBuild = latest?.assets.find((asset) => asset.name.endsWith('.rpm'));
18+
19+
const zipBuilds = latest?.assets.filter((asset) => asset.name.endsWith('.zip'));
20+
const winZip = zipBuilds?.find((asset) => asset.name.includes('win'));
21+
const macZip = zipBuilds?.find((asset) => asset.name.includes('mac'));
22+
const linuxZip = zipBuilds?.find((asset) => asset.name.includes('linux'));
23+
24+
const cleanReleaseBody = (release: IGithubRelease) => {
25+
const body = release.body;
26+
const dateLabel = formatReleaseDate(release);
27+
if (body == null) return '';
28+
29+
const normalized = body
30+
.replace(/\r\n/g, '\n')
31+
.replace(/'\s+'/g, '\n')
32+
.replace(/'\s*\n/g, '\n')
33+
.replace(/\n\s*'/g, '\n');
34+
35+
const lines = normalized
36+
.split('\n')
37+
.map((line) => line.trim())
38+
.filter((line) => line.length > 0)
39+
.filter((line) => !/^Built on\s+\d{4}-\d{2}-\d{2}$/i.test(line))
40+
.filter((line) => (dateLabel == null ? true : line !== dateLabel));
41+
42+
const cleaned = lines
43+
.map((line) => {
44+
const withoutDate = line.replace(/Built on\s+\d{4}-\d{2}-\d{2}/gi, '').trim();
45+
return withoutDate.replace(/\s{2,}/g, ' ').trim();
46+
})
47+
.filter((line) => line.length > 0);
48+
49+
if (cleaned.length === 0) return '';
50+
return cleaned.map((line) => `- ${line}`).join('\n');
51+
};
52+
53+
const formatReleaseDate = (release?: IGithubRelease) => {
54+
const dateString = release?.published_at ?? release?.created_at;
55+
if (dateString == null) return undefined;
56+
const date = new Date(dateString);
57+
if (Number.isNaN(date.getTime())) return undefined;
58+
return date.toISOString().split('T')[0];
59+
};
60+
61+
const releaseToMarkdown = (release: IGithubRelease) => {
62+
const dateHeading = formatReleaseDate(release) ?? release.tag_name ?? 'Nightly build';
63+
const body = cleanReleaseBody(release);
64+
return body.length > 0 ? `# ${dateHeading}\n\n${body}` : `# ${dateHeading}`;
65+
};
66+
67+
const computedChangelog = ordered.map(releaseToMarkdown).join('\n');
68+
69+
const pageProps = {
70+
dmgBuild,
71+
exeBuild,
72+
debBuild,
73+
rpmBuild,
74+
winZip,
75+
macZip,
76+
linuxZip,
77+
latestVersion: latest,
78+
computedChangelog,
79+
releaseDate: formatReleaseDate(latest),
80+
};
81+
return {
82+
pageContext: {
83+
pageProps,
84+
},
85+
};
86+
}
87+
88+
export { onBeforeRender };

src/pages/nightly.page.tsx

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
2+
import banner from '../assets/banner.png';
3+
import '../index.scss';
4+
import { Box, Button, HStack, Icon, Image, Link, Spacer, Text, VStack, Alert, AlertIcon } from '@chakra-ui/react';
5+
import { IGithubRelease, IReleaseAsset } from '../types/githubTypes';
6+
import { OS, isSupportedOS } from '../utils';
7+
import { BsWindows, BsApple, BsFillQuestionDiamondFill } from 'react-icons/bs/index.js';
8+
import { FaLinux } from 'react-icons/fa/index.js';
9+
import { MdDownload } from 'react-icons/md/index.js';
10+
import ReactMarkdown from 'react-markdown';
11+
import { GitHubButton } from '../components/buttons/GitHubButton';
12+
import { DiscordButton } from '../components/buttons/DiscordButton';
13+
import { KofiButton } from '../components/buttons/KofiButton';
14+
import { ShellWrapper } from '../components/PageShell/PageShell';
15+
import ChakraUIRenderer from '../utils/chakra-ui-markdown-renderer';
16+
17+
function Page(pageProps: {
18+
latestVersion: IGithubRelease | undefined;
19+
dmgBuild: IReleaseAsset | undefined;
20+
exeBuild: IReleaseAsset | undefined;
21+
debBuild: IReleaseAsset | undefined;
22+
rpmBuild: IReleaseAsset | undefined;
23+
winZip: IReleaseAsset | undefined;
24+
macZip: IReleaseAsset | undefined;
25+
linuxZip: IReleaseAsset | undefined;
26+
releaseDate: string | undefined;
27+
computedChangelog: string;
28+
}) {
29+
const { latestVersion, dmgBuild, exeBuild, debBuild, rpmBuild, winZip, macZip, linuxZip, releaseDate, computedChangelog } = pageProps;
30+
31+
let currentBuild: IReleaseAsset | undefined;
32+
let zipBuild: IReleaseAsset | undefined;
33+
let icon;
34+
switch (OS.name) {
35+
case 'Windows':
36+
currentBuild = exeBuild;
37+
zipBuild = winZip;
38+
icon = BsWindows;
39+
break;
40+
case 'Mac OS':
41+
currentBuild = dmgBuild;
42+
zipBuild = macZip;
43+
icon = BsApple;
44+
break;
45+
case 'Debian':
46+
currentBuild = debBuild;
47+
zipBuild = linuxZip;
48+
icon = FaLinux;
49+
break;
50+
case 'RedHat':
51+
currentBuild = rpmBuild;
52+
zipBuild = linuxZip;
53+
icon = FaLinux;
54+
break;
55+
case 'Linux':
56+
currentBuild = debBuild;
57+
zipBuild = linuxZip;
58+
icon = FaLinux;
59+
break;
60+
default:
61+
icon = BsFillQuestionDiamondFill;
62+
break;
63+
}
64+
65+
return (
66+
<ShellWrapper>
67+
<VStack
68+
spacing={8}
69+
mb="auto"
70+
>
71+
<HStack w="full">
72+
<Spacer display={{ base: 'block', sm: 'none' }} />
73+
<Image
74+
src={banner}
75+
w={{
76+
base: '240px',
77+
md: '360px',
78+
}}
79+
/>
80+
<Spacer />
81+
<HStack
82+
spacing={{ base: 2, lg: 6 }}
83+
display={{ base: 'none', sm: 'flex' }}
84+
>
85+
<GitHubButton />
86+
<DiscordButton />
87+
<KofiButton />
88+
</HStack>
89+
</HStack>
90+
91+
<Alert
92+
status="warning"
93+
borderRadius="md"
94+
w="full"
95+
bgColor="yellow.700"
96+
color="white"
97+
>
98+
<AlertIcon />
99+
Nightly builds are experimental and may be unstable.
100+
</Alert>
101+
102+
<VStack>
103+
<Button
104+
colorScheme="green"
105+
borderRadius="2xl"
106+
onClick={() => {
107+
if (currentBuild != null && isSupportedOS) {
108+
window.location.href = currentBuild?.browser_download_url;
109+
}
110+
}}
111+
height="auto"
112+
px={8}
113+
py={7}
114+
disabled={!isSupportedOS || currentBuild == null}
115+
>
116+
<VStack>
117+
<HStack>
118+
<Icon
119+
boxSize={8}
120+
as={MdDownload}
121+
></Icon>
122+
<Text
123+
fontSize={{
124+
base: 20,
125+
sm: 26,
126+
md: 36,
127+
}}
128+
fontWeight="bold"
129+
>
130+
{latestVersion != null ? `Download Nightly${releaseDate != null ? ` (${releaseDate})` : ''}` : 'No nightly release found'}
131+
</Text>
132+
</HStack>
133+
<HStack>
134+
<Icon as={icon}></Icon>
135+
<Text fontSize={18}>{isSupportedOS ? OS.name : 'Unsupported OS'}</Text>
136+
</HStack>
137+
</VStack>
138+
</Button>
139+
{zipBuild != null && isSupportedOS && (
140+
<Text color="white">
141+
Or download the{' '}
142+
<Link
143+
color="blue.300"
144+
href={zipBuild?.browser_download_url}
145+
>
146+
portable nightly (zip)
147+
</Link>
148+
</Text>
149+
)}
150+
<Text color="white">
151+
Prefer stability?{' '}
152+
<Link
153+
color="blue.300"
154+
href="/download"
155+
>
156+
Go to stable downloads
157+
</Link>
158+
</Text>
159+
</VStack>
160+
<Box
161+
color="white"
162+
w="full"
163+
bgColor="gray.700"
164+
borderRadius="lg"
165+
>
166+
<Box
167+
mx={2}
168+
p={4}
169+
h="49rem"
170+
w="full"
171+
overflowY="scroll"
172+
>
173+
<ReactMarkdown
174+
components={ChakraUIRenderer()}
175+
skipHtml
176+
>
177+
{computedChangelog}
178+
</ReactMarkdown>
179+
</Box>
180+
</Box>
181+
</VStack>
182+
</ShellWrapper>
183+
);
184+
}
185+
186+
export { Page };

0 commit comments

Comments
 (0)