Skip to content

Appstore on the website #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
729 changes: 689 additions & 40 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"astro-icon": "^1.1.5",
"limax": "4.1.0",
"lodash.merge": "^4.6.2",
"make-fetch-happen": "^14.0.3",
"marked": "^15.0.11",
"p-map": "^7.0.3",
"sanitize-html": "^2.16.0",
"unpic": "^4.1.2"
},
"devDependencies": {
Expand All @@ -43,7 +47,9 @@
"@tailwindcss/typography": "^0.5.16",
"@types/js-yaml": "^4.0.9",
"@types/lodash.merge": "^4.6.9",
"@types/make-fetch-happen": "^10.0.4",
"@types/mdx": "^2.0.13",
"@types/sanitize-html": "^2.16.0",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"astro-compress": "2.3.8",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/styles/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

@layer components {
.btn {
@apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer;
@apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer;
}

.btn-primary {
Expand Down
27 changes: 27 additions & 0 deletions src/components/appstore/AppIcon.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
import { Image } from 'astro:assets';
import { Icon } from 'astro-icon/components';

export interface Props {
name: string;
path?: string;
version?: string;
}

const { path, name, version = 'latest' } = Astro.props;
---

{
path ? (
<Image
src={`https://unpkg.com/${name}@${version}/public/${path}`}
alt={name}
width={144}
height={144}
loading="lazy"
class={`size-[72px] rounded object-cover`}
/>
) : (
<Icon name="tabler:apps" class="size-[72px] p-5 bg-slate-100 text-slate-300 dark:bg-slate-900 rounded-lg" />
)
}
14 changes: 14 additions & 0 deletions src/components/appstore/AppItem.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import AppIcon from './AppIcon.astro';
import type { Module } from '../../content/appstore';

export type Props = Module;
const module = Astro.props;
---

<a href={`/appstore/${module.name}`}>
<AppIcon name={module.name} path={module.signalk?.appIcon} version={module.version} />
<h2 class="text-xl font-semibold">{module.name}</h2>
<p class="">{module.description}</p>
{/* <pre>{JSON.stringify(module, null, 2)}</pre> */}
</a>
1 change: 1 addition & 0 deletions src/components/appstore/Label.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h3 class="text-xs uppercase font-bold tracking-wider text-muted mb-2"><slot /></h3>
35 changes: 35 additions & 0 deletions src/components/appstore/ListItem.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
import { Icon } from 'astro-icon/components';

export interface Props {
text?: string;
icon?: string;
image?: string;
href?: string;
target?: string;
rel?: string;
}

const {
text = await Astro.slots.render('default'),
image = await Astro.slots.render('image'),
icon,
href,
target,
rel,
} = Astro.props;

const Element = href ? 'a' : 'div';
---

<Element class="flex gap-2 items-center" {...href ? { href, target, rel } : []}>
{icon && <Icon name={icon} class="size-8 p-2 bg-slate-100 text-slate-500 dark:bg-slate-900 rounded-full" />}
{
image && (
<span class="size-8">
<Fragment set:html={image} />
</span>
)
}
<span set:html={text} />
</Element>
137 changes: 137 additions & 0 deletions src/content/appstore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { z, defineCollection } from 'astro:content';
import doFetch from 'make-fetch-happen';
import { join } from 'node:path';
import type { LoaderContext } from 'astro/loaders';
import pMap from 'p-map';

const cachePath = join(import.meta.dirname, '../../node_modules/.cache/fetch');

export const fetch = doFetch.defaults({
cachePath,
});

const schema = z.object({
name: z.string(),
description: z.string().optional(),
version: z.string(),
keywords: z.array(z.string()),
publisher: z.object({
username: z.string(),
email: z.string(),
}),
maintainers: z.array(
z.object({
username: z.string(),
email: z.string(),
})
),
license: z.string().optional(),
date: z.string().datetime(),
links: z.record(z.string(), z.string()),
downloads: z.object({
monthly: z.number(),
weekly: z.number(),
}),
updated: z.string(),
score: z.object({
final: z.number(),
detail: z.object({
popularity: z.number(),
quality: z.number(),
maintenance: z.number(),
}),
}),
flags: z.object({
insecure: z.number(),
}),
readme: z.string(),
signalk: z.object({
appIcon: z.string().optional(),
displayName: z.string().optional(),
}).optional(),
});

export type Module = z.infer<typeof schema>;

export default defineCollection({
schema,
loader: npmLoader({
keywords: ['signalk-node-server-plugin', 'signalk-embeddable-webapp', 'signalk-webapp'],
}),
});

export function npmLoader({ keywords = [], concurrency = 10 }: { keywords?: string[]; concurrency?: number } = {}) {
return {
name: 'npm',
load: async ({ store, logger, meta, parseData }: LoaderContext): Promise<void> => {
logger.info(`Loading npm modules with keywords: ${keywords.join(', ')}`);

await pMap(
keywords,
async (keyword) => {
const modules = await fetchModulesByKeyword(keyword);
await pMap(
modules,
async ({ package: pkg, ...module }) => {
const id = pkg.name;

logger.debug(`Fetching ${id}`);
const res = await fetch(`https://registry.npmjs.org/${id}`);
if (!res.ok) throw new Error(`Failed to fetch package data: ${res.statusText}`);
const { readme, versions } = await res.json();

const data = await parseData({
id,
data: {
...module,
...versions[pkg.version],
...pkg,
readme,
}
})

meta.set('lastModified', data.date);

store.set({
id,
data: {
...module,
...data,
},
});
},
{ concurrency }
);
},
{ concurrency }
);
},
};
}

async function fetchModulesByKeyword(keyword: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const items: any[] = [];
const size = 250;

while (true) {
const url = new URL('https://registry.npmjs.org/-/v1/search');
url.search = new URLSearchParams({
size: size.toString(),
from: items.length.toString(),
text: `keywords:${keyword}`,
}).toString();

const response = await fetch(url.toString());
if (!response.ok) throw new Error(`Failed to fetch modules: ${response.statusText}`);
const data = await response.json();

items.push(...data.objects);

if (data.objects.length < 250) {
break;
}
}

return items;
}
2 changes: 2 additions & 0 deletions src/content/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z, defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import appstore from './appstore';

const metadataDefinition = () =>
z
Expand Down Expand Up @@ -67,4 +68,5 @@ const postCollection = defineCollection({

export const collections = {
post: postCollection,
appstore,
};
4 changes: 4 additions & 0 deletions src/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const headerData = {
target: '_blank',
href: 'https://demo.signalk.org/documentation/',
},
{
text: 'AppStore',
href: getPermalink('appstore'),
},
{
text: 'Blog',
href: getBlogPermalink(),
Expand Down
Loading
Loading