Skip to content

Commit 0e417b8

Browse files
authored
Add version numbers next to key integrations that are often not up-to-date with the GitHub version (#3553)
* feat: add current version numbers for firefox, js, rust, and vscode * chore: tweak formatting * fix: numbers match order * feat: up-to-date/behind version tooltips
1 parent 449184c commit 0e417b8

4 files changed

Lines changed: 209 additions & 10 deletions

File tree

packages/web/src/lib/marketing/data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type Integration = {
1717
categoryLabel: string;
1818
tag?: string;
1919
platform?: string;
20+
version?: string;
2021
cta: 'install' | 'docs';
2122
color?: string;
2223
fg?: string;
@@ -106,7 +107,7 @@ export const integrationCategories: IntegrationCategory[] = [
106107
items: [
107108
{
108109
id: 'vscode',
109-
name: 'Visual Studio Code',
110+
name: 'VS Code',
110111
desc: 'Marketplace extension.',
111112
href: marketingLinks.vscode,
112113
platform: 'VS Code, Cursor',
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { writable } from 'svelte/store';
2+
3+
export const liveVersions = writable<Record<string, string>>({
4+
harper: '',
5+
firefox: '',
6+
js: '',
7+
rust: '',
8+
vscode: '',
9+
});
10+
11+
function isValidVersion(version: string): boolean {
12+
return /^\d+\.\d+\.\d+$/.test(version);
13+
}
14+
15+
export function isUpToDate(harperVersion: string, integrationVersion: string): boolean | null {
16+
// 1. Structural/syntax invalidity
17+
if (!isValidVersion(harperVersion) || !isValidVersion(integrationVersion)) {
18+
return null;
19+
}
20+
21+
const [hMaj, hMin, hPatch] = harperVersion.split('.').map(Number);
22+
const [iMaj, iMin, iPatch] = integrationVersion.split('.').map(Number);
23+
24+
// 2. Logic invalidity: Integration is ahead of Harper ("Too New")
25+
if (iMaj > hMaj) return null;
26+
if (iMaj === hMaj && iMin > hMin) return null;
27+
if (iMaj === hMaj && iMin === hMin && iPatch > hPatch) return null;
28+
29+
// 3. Exact match: Up-to-date
30+
if (iMaj === hMaj && iMin === hMin && iPatch === hPatch) return true;
31+
32+
// 4. Fallthrough: Integration must be older
33+
return false;
34+
}
35+
36+
export async function loadLiveVersions() {
37+
// 0. Harper
38+
(async () => {
39+
try {
40+
const ver = (await (await fetch('https://writewithharper.com/latestversion')).text()).replace(
41+
/^v/,
42+
'',
43+
);
44+
45+
if (isValidVersion(ver)) {
46+
liveVersions.update((v) => ({ ...v, harper: ver }));
47+
}
48+
} catch {}
49+
})();
50+
51+
// 1. Firefox Addon
52+
(async () => {
53+
try {
54+
const ver = (
55+
await (
56+
await fetch(
57+
'https://addons.mozilla.org/api/v5/addons/addon/private-grammar-checker-harper/',
58+
)
59+
).json()
60+
).current_version.version;
61+
62+
if (isValidVersion(ver)) {
63+
liveVersions.update((v) => ({ ...v, firefox: ver }));
64+
}
65+
} catch {}
66+
})();
67+
68+
// 2. JS Package
69+
(async () => {
70+
try {
71+
const ver = (await (await fetch('https://registry.npmjs.org/harper.js')).json())['dist-tags']
72+
.latest;
73+
74+
if (isValidVersion(ver)) {
75+
liveVersions.update((v) => ({ ...v, js: ver }));
76+
}
77+
} catch {}
78+
})();
79+
80+
// 3. Rust Crate
81+
(async () => {
82+
try {
83+
const lines = (await (await fetch('https://index.crates.io/ha/rp/harper-core')).text()).split(
84+
'\n',
85+
);
86+
const ver = JSON.parse(lines[lines.length - 2]).vers;
87+
88+
if (isValidVersion(ver)) {
89+
liveVersions.update((v) => ({ ...v, rust: ver }));
90+
}
91+
} catch {}
92+
})();
93+
94+
// 4. VS Code Extension
95+
(async () => {
96+
try {
97+
const res = await fetch(
98+
'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery',
99+
{
100+
method: 'POST',
101+
headers: {
102+
'Content-Type': 'application/json',
103+
Accept: 'application/json;api-version=3.0-preview.1',
104+
},
105+
body: JSON.stringify({
106+
filters: [
107+
{
108+
criteria: [
109+
{ filterType: 8, value: 'Microsoft.VisualStudio.Code' },
110+
{ filterType: 7, value: 'elijah-potter.harper' },
111+
],
112+
pageNumber: 1,
113+
pageSize: 1,
114+
sortBy: 0,
115+
sortOrder: 0,
116+
},
117+
],
118+
assetTypes: [],
119+
flags: 194,
120+
}),
121+
},
122+
);
123+
const ver = (await res.json()).results[0].extensions[0].versions[0].version;
124+
125+
if (isValidVersion(ver)) {
126+
liveVersions.update((v) => ({ ...v, vscode: ver }));
127+
}
128+
} catch {}
129+
})();
130+
}

packages/web/src/routes/+page.svelte

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ import PrivacySpeedCards from '$lib/marketing/PrivacySpeedCards.svelte';
1919
import { featuredIntegrationIds, integrations, marketingLinks } from '$lib/marketing/data';
2020
import { LazyEditor } from 'harper-editor';
2121
import type { Linter } from 'harper.js';
22+
import { isUpToDate, loadLiveVersions, liveVersions } from '$lib/marketing/versions';
2223
import { onMount } from 'svelte';
2324
import demoText from '../../../../demo.md?raw';
2425
2526
const editorContent = demoText.trim();
2627
let linter: Linter | null = null;
2728
29+
const VERSION_UPTODATE = 'Up-to-date';
30+
const VERSION_BEHIND = 'This version is slightly behind the core engine due to a delay';
31+
2832
const testimonials = [
2933
{
3034
authorName: 'Rich Edmonds',
@@ -142,10 +146,17 @@ const faqs = [
142146
},
143147
];
144148
149+
let liveFxVer = '';
150+
let liveJsVer = '';
151+
let liveRustVer = '';
152+
let liveVscodeVer = '';
153+
145154
onMount(() => {
146155
void (async () => {
147156
linter = await createEditorLinter();
148157
})();
158+
159+
void loadLiveVersions();
149160
});
150161
151162
</script>
@@ -241,14 +252,30 @@ onMount(() => {
241252
{#each featuredIntegrationIds as id}
242253
{@const integration = integrations.find((item) => item.id === id)}
243254
{#if integration}
255+
{@const upToDate = isUpToDate($liveVersions.harper, $liveVersions[integration.id])}
256+
{@const titleText = upToDate ? VERSION_UPTODATE : upToDate === false ? VERSION_BEHIND : null}
244257
<a
245258
class="flex items-center gap-3 rounded-[0.65rem] px-3 py-[0.65rem] !text-[#1c1a16] no-underline hover:bg-black/[0.04] hover:no-underline dark:!text-white dark:hover:bg-white/10"
246259
href={integration.href}
247260
>
248261
<IntegrationTile {integration} size={32} />
249262
<span class="flex min-w-0 flex-col">
250-
<strong class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.84rem]">{integration.name}</strong>
251-
<small class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.72rem] text-[#807a6e] dark:text-white/55">{integration.desc}</small>
263+
<div class="flex items-center gap-1.5 overflow-hidden">
264+
<strong class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.84rem]">{integration.name}</strong>
265+
{#if $liveVersions[integration.id]}
266+
{#if upToDate != null}
267+
<span
268+
class="inline-flex items-center rounded-full bg-[#f4f1ea] dark:bg-white/10 px-1.5 py-0.5 text-[0.65rem] font-mono font-medium text-[#6b6455] dark:text-white/80 border border-[#e4dfd3] dark:border-white/10 select-none"
269+
title={titleText}
270+
>
271+
{$liveVersions[integration.id]}
272+
</span>
273+
{/if}
274+
{/if}
275+
</div>
276+
<small class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.72rem] text-[#807a6e] dark:text-white/55">
277+
{integration.desc}
278+
</small>
252279
</span>
253280
</a>
254281
{/if}

packages/web/src/routes/get/+page.svelte

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { onMount } from 'svelte';
23
import {
34
type Integration,
45
integrationCategories,
@@ -8,10 +9,18 @@ import {
89
import IntegrationTile from '$lib/marketing/IntegrationTile.svelte';
910
import MarketingFooter from '$lib/marketing/MarketingFooter.svelte';
1011
import MarketingHeader from '$lib/marketing/MarketingHeader.svelte';
12+
import { isUpToDate, liveVersions, loadLiveVersions } from '$lib/marketing/versions';
1113
1214
let activeCategory = 'all';
1315
let query = '';
1416
17+
const VERSION_UPTODATE = 'Up-to-date';
18+
const VERSION_BEHIND = 'This version is slightly behind the core engine due to a delay';
19+
20+
onMount(() => {
21+
void loadLiveVersions();
22+
});
23+
1524
$: filtered = integrations.filter((integration) => {
1625
if (activeCategory === 'community' && !integration.community) {
1726
return false;
@@ -71,14 +80,30 @@ function clearFilters() {
7180
{#each ['desktop', 'chrome'] as id}
7281
{@const integration = integrations.find((item) => item.id === id)}
7382
{#if integration}
83+
{@const upToDate = isUpToDate($liveVersions.harper, $liveVersions[integration.id])}
84+
{@const titleText = upToDate ? VERSION_UPTODATE : upToDate === false ? VERSION_BEHIND : null}
7485
<a
7586
class="grid grid-cols-[2.5rem_1fr_auto] items-center gap-[0.9rem] rounded-xl border-[0.5px] border-[rgba(28,26,22,0.1)] bg-white px-[1.1rem] py-[0.9rem] !text-[#1c1a16] no-underline transition-[transform,box-shadow,border-color] duration-150 hover:-translate-y-px hover:border-[#b06a1b] hover:shadow-[0_10px_24px_-16px_rgba(28,26,22,0.16)] hover:no-underline dark:border-white/10 dark:bg-white/5 dark:!text-white dark:hover:border-primary-300 max-[640px]:grid-cols-[2.5rem_1fr] [&_em]:max-[640px]:col-start-2"
7687
href={integration.href}
7788
>
7889
<IntegrationTile {integration} size={40} />
7990
<span class="flex min-w-0 flex-col">
80-
<strong class="text-[0.94rem] leading-[1.25]">{integration.name}</strong>
81-
<small class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.8rem] leading-[1.4] text-[#807a6e] dark:text-white/55">{integration.desc}</small>
91+
<div class="flex items-center gap-1.5">
92+
<strong class="text-[0.94rem] leading-[1.25]">{integration.name}</strong>
93+
{#if $liveVersions[integration.id]}
94+
{#if upToDate != null}
95+
<span
96+
class="inline-flex items-center rounded-full bg-[#f4f1ea] dark:bg-white/10 px-1.5 py-0.5 text-[0.68rem] font-mono font-medium text-[#6b6455] dark:text-white/80 border border-[#e4dfd3] dark:border-white/10 select-none"
97+
title={titleText}
98+
>
99+
{$liveVersions[integration.id]}
100+
</span>
101+
{/if}
102+
{/if}
103+
</div>
104+
<small class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.8rem] leading-[1.4] text-[#807a6e] dark:text-white/55">
105+
{integration.desc}
106+
</small>
82107
</span>
83108
<em class="whitespace-nowrap text-[0.78rem] font-extrabold text-[#b06a1b] not-italic dark:text-primary-300">{ctaLabel(integration)} →</em>
84109
</a>
@@ -164,16 +189,32 @@ function clearFilters() {
164189
</div>
165190
{:else}
166191
{#each filtered as integration}
192+
{@const upToDate = isUpToDate($liveVersions.harper, $liveVersions[integration.id])}
193+
{@const titleText = upToDate ? VERSION_UPTODATE : upToDate === false ? VERSION_BEHIND : null}
167194
<a
168195
class="grid grid-cols-[2.5rem_1fr_auto] items-center gap-[0.9rem] rounded-xl border-[0.5px] border-[rgba(28,26,22,0.1)] bg-white px-[1.1rem] py-[0.9rem] !text-[#1c1a16] no-underline transition-[transform,box-shadow,border-color] duration-150 hover:-translate-y-px hover:border-[#b06a1b] hover:shadow-[0_10px_24px_-16px_rgba(28,26,22,0.16)] hover:no-underline dark:border-white/10 dark:bg-white/5 dark:!text-white dark:hover:border-primary-300 max-[640px]:grid-cols-[2.5rem_1fr] [&_em]:max-[640px]:col-start-2"
169196
href={integration.href}
170197
>
171-
<IntegrationTile {integration} size={40} />
172-
<span class="flex min-w-0 flex-col">
198+
<IntegrationTile {integration} size={40} />
199+
<span class="flex min-w-0 flex-col">
200+
<div class="flex items-center gap-1.5">
173201
<strong class="text-[0.94rem] leading-[1.25]">{integration.name}</strong>
174-
<small class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.8rem] leading-[1.4] text-[#807a6e] dark:text-white/55">{integration.platform}</small>
175-
</span>
176-
<em class="whitespace-nowrap text-[0.78rem] font-extrabold text-[#b06a1b] not-italic dark:text-primary-300">{ctaLabel(integration)} →</em>
202+
{#if $liveVersions[integration.id]}
203+
{#if upToDate != null}
204+
<span
205+
class="inline-flex items-center rounded-full bg-[#f4f1ea] dark:bg-white/10 px-1.5 py-0.5 text-[0.68rem] font-mono font-medium text-[#6b6455] dark:text-white/80 border border-[#e4dfd3] dark:border-white/10 select-none"
206+
title={titleText}
207+
>
208+
{$liveVersions[integration.id]}
209+
</span>
210+
{/if}
211+
{/if}
212+
</div>
213+
<small class="overflow-hidden text-ellipsis whitespace-nowrap text-[0.8rem] leading-[1.4] text-[#807a6e] dark:text-white/55">
214+
{integration.platform}
215+
</small>
216+
</span>
217+
<em class="whitespace-nowrap text-[0.78rem] font-extrabold text-[#b06a1b] not-italic dark:text-primary-300">{ctaLabel(integration)} →</em>
177218
</a>
178219
{/each}
179220
{/if}

0 commit comments

Comments
 (0)