diff --git a/packages/site-client/src/pages/element/wco-element-page.ts b/packages/site-client/src/pages/element/wco-element-page.ts index 4ea26fc9..2738abaf 100644 --- a/packages/site-client/src/pages/element/wco-element-page.ts +++ b/packages/site-client/src/pages/element/wco-element-page.ts @@ -31,13 +31,6 @@ export class WCOElementPage extends WCOPage { static styles = [ WCOPage.styles, css` - .full-screen-error { - display: flex; - flex: 1; - align-items: center; - justify-items: center; - } - main { display: grid; max-width: var(--content-width); @@ -90,7 +83,7 @@ export class WCOElementPage extends WCOPage { renderContent() { if (this.elementData === undefined) { - return html`
No element to display
`; + return this.fullScreenError('No element to display'); } const { packageName, @@ -131,7 +124,8 @@ export class WCOElementPage extends WCOPage {
${packageName} } +).__ssrData; + +// We need to hydrate the whole page to remove any defer-hydration attributes. +// We could also remove the attribute manually, or not use deferhydration, but +// instead manually assign the data into the element, and +// time imports so that automatic element hydration happend after. +hydrate(renderPackagePage(...data), document.body); diff --git a/packages/site-client/src/pages/package/shell.ts b/packages/site-client/src/pages/package/shell.ts new file mode 100644 index 00000000..f7c50604 --- /dev/null +++ b/packages/site-client/src/pages/package/shell.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import './wco-package-page.js'; +import type {PackageData} from './wco-package-page.js'; +import {html} from 'lit'; + +export const renderPackagePage = (packageData: PackageData) => + html``; diff --git a/packages/site-client/src/pages/package/wco-package-page.ts b/packages/site-client/src/pages/package/wco-package-page.ts new file mode 100644 index 00000000..ab17cce9 --- /dev/null +++ b/packages/site-client/src/pages/package/wco-package-page.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CustomElement} from '@webcomponents/catalog-api/lib/_schema.js'; +import {html, css} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {unsafeHTML} from 'lit/directives/unsafe-html.js'; + +import {WCOPage} from '../../shared/wco-page.js'; +import '../catalog/wco-element-card.js'; + +export interface PackageData { + name: string; + description: string; + version: string; + elements: CustomElement[]; +} + +@customElement('wco-package-page') +export class WCOPackagePage extends WCOPage { + static styles = [ + WCOPage.styles, + css` + h1 { + display: inline-block; + } + .elements { + display: grid; + grid-template-columns: repeat(4, 200px); + grid-template-rows: auto; + grid-auto-columns: 200px; + grid-auto-rows: 200px; + gap: 8px; + } + `, + ]; + + @property({attribute: false}) + packageData?: PackageData; + + renderContent() { + if (this.packageData === undefined) { + return this.fullScreenError('No package to display'); + } + + return html` +
+

${this.packageData.name}

+ v${this.packageData.version} +
+
${unsafeHTML(this.packageData.description)}
+
+ ${this.packageData.elements.map((e) => { + return html``; + })} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'wco-package-page': WCOPackagePage; + } +} diff --git a/packages/site-client/src/shared/wco-page.ts b/packages/site-client/src/shared/wco-page.ts index 6a6c5d18..2f2b4738 100644 --- a/packages/site-client/src/shared/wco-page.ts +++ b/packages/site-client/src/shared/wco-page.ts @@ -31,6 +31,13 @@ export class WCOPage extends LitElement { wco-footer { width: 100%; } + + .full-screen-error { + display: flex; + flex: 1; + align-items: center; + justify-items: center; + } `; render() { @@ -48,6 +55,10 @@ export class WCOPage extends LitElement { protected renderContent() { return html``; } + + protected fullScreenError(message: unknown) { + return html`
${message}
`; + } } declare global { diff --git a/packages/site-server/src/lib/catalog/router.ts b/packages/site-server/src/lib/catalog/router.ts index c2d76cc1..2cec199c 100644 --- a/packages/site-server/src/lib/catalog/router.ts +++ b/packages/site-server/src/lib/catalog/router.ts @@ -8,6 +8,7 @@ import Router from '@koa/router'; import {handleCatalogRoute} from './routes/catalog/catalog-route.js'; import {handleCatalogSearchRoute} from './routes/catalog/search-route.js'; import {handleElementRoute} from './routes/element/element-route.js'; +import {handlePackageRoute} from './routes/package/package-route.js'; // import cors from '@koa/cors'; export const catalogRouter = new Router(); @@ -19,3 +20,5 @@ catalogRouter.get('/', handleCatalogRoute); catalogRouter.get('/search', handleCatalogSearchRoute); catalogRouter.get('/element/:path+', handleElementRoute); + +catalogRouter.get('/package/:name+', handlePackageRoute); diff --git a/packages/site-server/src/lib/catalog/routes/package/package-route.ts b/packages/site-server/src/lib/catalog/routes/package/package-route.ts new file mode 100644 index 00000000..22b23cdc --- /dev/null +++ b/packages/site-server/src/lib/catalog/routes/package/package-route.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// This must be imported before lit +import {renderPage} from '@webcomponents/internal-site-templates/lib/base.js'; + +import {DefaultContext, DefaultState, ParameterizedContext} from 'koa'; +import {Readable} from 'stream'; +import {gql} from '@apollo/client/core/index.js'; +import Router from '@koa/router'; + +import {renderPackagePage} from '@webcomponents/internal-site-client/lib/pages/package/shell.js'; +import {client} from '../../graphql.js'; +import {PackageData} from '@webcomponents/internal-site-client/lib/pages/package/wco-package-page.js'; +import {marked} from 'marked'; + +export const handlePackageRoute = async ( + context: ParameterizedContext< + DefaultState, + DefaultContext & Router.RouterParamContext, + unknown + > +) => { + const {params} = context; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const packageName = params['name']!; + + // TODO (justinfagnani): To make this type-safe, we need to write + // a query .graphql document and generate a TypedDocumentNode from it. + const result = await client.query({ + query: gql`{ + package(packageName: "${packageName}") { + ... on ReadablePackageInfo { + name + description + version { + ... on ReadablePackageVersion { + version + description + customElements { + tagName + package + declaration + customElementExport + declaration + } + } + } + } + } + }`, + }); + + if (result.errors !== undefined && result.errors.length > 0) { + throw new Error(result.errors.map((e) => e.message).join('\n')); + } + const {data} = result; + const packageVersion = data.package?.version; + if (packageVersion === undefined) { + // TODO: 404 + throw new Error(`No such package version: ${packageName}`); + } + + // Set location because wco-nav-bar reads pathname from it. URL isn't + // exactly a Location, but it's close enough for read-only uses + globalThis.location = new URL(context.URL.href) as unknown as Location; + + const responseData: PackageData = { + name: packageName, + description: marked(data.package.description ?? ''), + version: packageVersion.version, + elements: packageVersion.customElements, + }; + + context.type = 'html'; + context.status = 200; + context.body = Readable.from( + renderPage( + { + title: `${packageName}`, + initScript: '/js/package/boot.js', + content: renderPackagePage(responseData), + initialData: [responseData], + }, + { + // We need to defer elements from hydrating so that we can + // manually provide data to the element in element-hydrate.js + deferHydration: true, + } + ) + ); +};