Skip to content

Commit 5bf9c8c

Browse files
authored
Add a basic package page. (#1448)
* Add a basic package page. Fixes #1444 * Address review comments.
1 parent 391eeec commit 5bf9c8c

File tree

7 files changed

+212
-9
lines changed

7 files changed

+212
-9
lines changed

packages/site-client/src/pages/element/wco-element-page.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ export class WCOElementPage extends WCOPage {
3131
static styles = [
3232
WCOPage.styles,
3333
css`
34-
.full-screen-error {
35-
display: flex;
36-
flex: 1;
37-
align-items: center;
38-
justify-items: center;
39-
}
40-
4134
main {
4235
display: grid;
4336
max-width: var(--content-width);
@@ -90,7 +83,7 @@ export class WCOElementPage extends WCOPage {
9083

9184
renderContent() {
9285
if (this.elementData === undefined) {
93-
return html`<div class="full-screen-error">No element to display</div>`;
86+
return this.fullScreenError('No element to display');
9487
}
9588
const {
9689
packageName,
@@ -131,7 +124,8 @@ export class WCOElementPage extends WCOPage {
131124
<div id="logo-container"><div id="logo"></div></div>
132125
<div id="meta-container">
133126
<span id="package-meta"
134-
>${packageName}<select>
127+
><a href="/catalog/package/${packageName}">${packageName}</a
128+
><select>
135129
<!-- TODO (justinfagnani): get actual version and dist tag data -->
136130
<option>x.x.x</option>
137131
</select></span
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import 'lit/experimental-hydrate-support.js';
8+
import {hydrate} from 'lit/experimental-hydrate.js';
9+
import {renderPackagePage} from './shell.js';
10+
11+
const data = (
12+
globalThis as unknown as {__ssrData: Parameters<typeof renderPackagePage>}
13+
).__ssrData;
14+
15+
// We need to hydrate the whole page to remove any defer-hydration attributes.
16+
// We could also remove the attribute manually, or not use deferhydration, but
17+
// instead manually assign the data into the <wco-package-page> element, and
18+
// time imports so that automatic element hydration happend after.
19+
hydrate(renderPackagePage(...data), document.body);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import './wco-package-page.js';
8+
import type {PackageData} from './wco-package-page.js';
9+
import {html} from 'lit';
10+
11+
export const renderPackagePage = (packageData: PackageData) =>
12+
html`<wco-package-page .packageData=${packageData}></wco-package-page>`;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {CustomElement} from '@webcomponents/catalog-api/lib/_schema.js';
8+
import {html, css} from 'lit';
9+
import {customElement, property} from 'lit/decorators.js';
10+
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
11+
12+
import {WCOPage} from '../../shared/wco-page.js';
13+
import '../catalog/wco-element-card.js';
14+
15+
export interface PackageData {
16+
name: string;
17+
description: string;
18+
version: string;
19+
elements: CustomElement[];
20+
}
21+
22+
@customElement('wco-package-page')
23+
export class WCOPackagePage extends WCOPage {
24+
static styles = [
25+
WCOPage.styles,
26+
css`
27+
h1 {
28+
display: inline-block;
29+
}
30+
.elements {
31+
display: grid;
32+
grid-template-columns: repeat(4, 200px);
33+
grid-template-rows: auto;
34+
grid-auto-columns: 200px;
35+
grid-auto-rows: 200px;
36+
gap: 8px;
37+
}
38+
`,
39+
];
40+
41+
@property({attribute: false})
42+
packageData?: PackageData;
43+
44+
renderContent() {
45+
if (this.packageData === undefined) {
46+
return this.fullScreenError('No package to display');
47+
}
48+
49+
return html`
50+
<div>
51+
<h1>${this.packageData.name}</h1>
52+
v${this.packageData.version}
53+
</div>
54+
<div>${unsafeHTML(this.packageData.description)}</div>
55+
<div class="elements">
56+
${this.packageData.elements.map((e) => {
57+
return html`<wco-element-card .element=${e}></wco-element-card>`;
58+
})}
59+
</div>
60+
`;
61+
}
62+
}
63+
64+
declare global {
65+
interface HTMLElementTagNameMap {
66+
'wco-package-page': WCOPackagePage;
67+
}
68+
}

packages/site-client/src/shared/wco-page.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export class WCOPage extends LitElement {
3131
wco-footer {
3232
width: 100%;
3333
}
34+
35+
.full-screen-error {
36+
display: flex;
37+
flex: 1;
38+
align-items: center;
39+
justify-items: center;
40+
}
3441
`;
3542

3643
render() {
@@ -48,6 +55,10 @@ export class WCOPage extends LitElement {
4855
protected renderContent() {
4956
return html`<slot></slot>`;
5057
}
58+
59+
protected fullScreenError(message: unknown) {
60+
return html`<div class="full-screen-error">${message}</div>`;
61+
}
5162
}
5263

5364
declare global {

packages/site-server/src/lib/catalog/router.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Router from '@koa/router';
88
import {handleCatalogRoute} from './routes/catalog/catalog-route.js';
99
import {handleCatalogSearchRoute} from './routes/catalog/search-route.js';
1010
import {handleElementRoute} from './routes/element/element-route.js';
11+
import {handlePackageRoute} from './routes/package/package-route.js';
1112
// import cors from '@koa/cors';
1213

1314
export const catalogRouter = new Router();
@@ -19,3 +20,5 @@ catalogRouter.get('/', handleCatalogRoute);
1920
catalogRouter.get('/search', handleCatalogSearchRoute);
2021

2122
catalogRouter.get('/element/:path+', handleElementRoute);
23+
24+
catalogRouter.get('/package/:name+', handlePackageRoute);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// This must be imported before lit
8+
import {renderPage} from '@webcomponents/internal-site-templates/lib/base.js';
9+
10+
import {DefaultContext, DefaultState, ParameterizedContext} from 'koa';
11+
import {Readable} from 'stream';
12+
import {gql} from '@apollo/client/core/index.js';
13+
import Router from '@koa/router';
14+
15+
import {renderPackagePage} from '@webcomponents/internal-site-client/lib/pages/package/shell.js';
16+
import {client} from '../../graphql.js';
17+
import {PackageData} from '@webcomponents/internal-site-client/lib/pages/package/wco-package-page.js';
18+
import {marked} from 'marked';
19+
20+
export const handlePackageRoute = async (
21+
context: ParameterizedContext<
22+
DefaultState,
23+
DefaultContext & Router.RouterParamContext<DefaultState, DefaultContext>,
24+
unknown
25+
>
26+
) => {
27+
const {params} = context;
28+
29+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30+
const packageName = params['name']!;
31+
32+
// TODO (justinfagnani): To make this type-safe, we need to write
33+
// a query .graphql document and generate a TypedDocumentNode from it.
34+
const result = await client.query({
35+
query: gql`{
36+
package(packageName: "${packageName}") {
37+
... on ReadablePackageInfo {
38+
name
39+
description
40+
version {
41+
... on ReadablePackageVersion {
42+
version
43+
description
44+
customElements {
45+
tagName
46+
package
47+
declaration
48+
customElementExport
49+
declaration
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}`,
56+
});
57+
58+
if (result.errors !== undefined && result.errors.length > 0) {
59+
throw new Error(result.errors.map((e) => e.message).join('\n'));
60+
}
61+
const {data} = result;
62+
const packageVersion = data.package?.version;
63+
if (packageVersion === undefined) {
64+
// TODO: 404
65+
throw new Error(`No such package version: ${packageName}`);
66+
}
67+
68+
// Set location because wco-nav-bar reads pathname from it. URL isn't
69+
// exactly a Location, but it's close enough for read-only uses
70+
globalThis.location = new URL(context.URL.href) as unknown as Location;
71+
72+
const responseData: PackageData = {
73+
name: packageName,
74+
description: marked(data.package.description ?? ''),
75+
version: packageVersion.version,
76+
elements: packageVersion.customElements,
77+
};
78+
79+
context.type = 'html';
80+
context.status = 200;
81+
context.body = Readable.from(
82+
renderPage(
83+
{
84+
title: `${packageName}`,
85+
initScript: '/js/package/boot.js',
86+
content: renderPackagePage(responseData),
87+
initialData: [responseData],
88+
},
89+
{
90+
// We need to defer elements from hydrating so that we can
91+
// manually provide data to the element in element-hydrate.js
92+
deferHydration: true,
93+
}
94+
)
95+
);
96+
};

0 commit comments

Comments
 (0)