-
Notifications
You must be signed in to change notification settings - Fork 19
blueprints: serve /blueprints/ from Nuxt #5794
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
bbfaabf
blueprints: serve /blueprints/ from Nuxt
dimitrieh d8066c3
Merge branch 'main' into nuxt/blueprints
dimitrieh b3ab87f
blueprints: name the contact band CtaContactUsLine, alongside the oth…
dimitrieh b0a557c
blueprints: take the contact band's href from the CTA destination reg…
dimitrieh 37cd7eb
blueprints: serve /blueprints/ from Nuxt in dev too
dimitrieh c18b8b9
blueprints: use Heroicons directly, drop SiteArt
dimitrieh 00a4b19
Merge remote-tracking branch 'origin/main' into nuxt/blueprints
dimitrieh 1cbd94a
Merge remote-tracking branch 'origin/main' into nuxt/blueprints
dimitrieh cf97789
Merge remote-tracking branch 'origin/main' into nuxt/blueprints
dimitrieh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <script setup lang="ts"> | ||
| // src/_includes/blueprints/blueprint-card.njk - one tile in the Blueprint Library grid. | ||
| // | ||
| // What the port changes on purpose: | ||
| // - The description is still printed unescaped, as `| safe` did. It comes from a README | ||
| // in FlowFuse/blueprint-library - a private repo whose submissions the team reviews - | ||
| // so it is repo content like any other page here, not visitor input. | ||
| // - `tileImage` ran each screenshot through eleventy-img at 380px. The image is now a | ||
| // plain path into nuxt/public/blueprints/, resized by @nuxt/image's Netlify provider. | ||
| import { blueprintAuthor, blueprintTagLabel, blueprintTags, deployUrl } from '../lib/blueprint-display.mjs' | ||
| import type { BlueprintListEntry } from '../composables/useBlueprintList' | ||
|
|
||
| const props = defineProps<{ entry: BlueprintListEntry }>() | ||
|
|
||
| const FALLBACK_IMAGE = '/images/og-blog.jpg' | ||
| const FALLBACK_IMAGE_ALT = 'Image with logo and the slogan: Elevate Node-RED with Flowfuse' | ||
|
|
||
| const tags = computed(() => blueprintTags(props.entry.tags)) | ||
| const author = computed(() => blueprintAuthor(props.entry.author)) | ||
| const image = computed(() => props.entry.image || FALLBACK_IMAGE) | ||
| const imageAlt = computed(() => props.entry.image ? `Image representing ${props.entry.title}` : FALLBACK_IMAGE_ALT) | ||
| </script> | ||
|
|
||
| <template> | ||
| <li class="flex flex-col h-full"> | ||
| <div class="mb-2"> | ||
| <label | ||
| v-for="tag in tags" | ||
| :key="tag" | ||
| class="text-gray-700 text-xs font-light rounded-sm bg-indigo-50 py-1.5 px-2 inline-block w-auto" | ||
| >{{ blueprintTagLabel(tag) }}</label> | ||
| </div> | ||
| <div class="grid bg-white ff-image-cover blueprint rounded-lg border drop-shadow-md hover:drop-shadow-lg grow"> | ||
| <NuxtLink :to="`${entry.path}/`" class="w-full flex flex-col group hover:no-underline"> | ||
| <div class="transition-transform group-hover:scale-105 ff-image-cover aspect-video border-b"> | ||
| <img :src="image" :alt="imageAlt" width="380" loading="lazy" class="w-full h-auto"> | ||
| </div> | ||
| <h5 class="mt-4 mb-0 group-hover:underline px-4 font-medium text-lg leading-6">{{ entry.title }}</h5> | ||
| <!-- eslint-disable-next-line vue/no-v-html --> | ||
| <p class="text-sm leading-normal font-light pt-1 mb-4 mt-3 px-4 text-gray-500" v-html="entry.description" /> | ||
| </NuxtLink> | ||
| <div class="justify-self-end flex flex-row justify-between items-center w-full px-4 py-2 bg-indigo-50/50 mt-auto border-t"> | ||
| <div class="flex flex-col"> | ||
| <label class="text-xs">Author:</label> | ||
| <BlueprintCompanyTile :company="author" /> | ||
| </div> | ||
| <a | ||
| v-if="entry.blueprintId" | ||
| :href="deployUrl(entry.blueprintId)" | ||
| class="ff-btn ff-btn--primary-outlined flex gap-2" | ||
| target="_blank" | ||
| rel="noopener" | ||
| >DEPLOY <UIcon name="i-heroicons-rocket-launch" class="size-6 shrink-0" /></a> | ||
| </div> | ||
| </div> | ||
| </li> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <script setup lang="ts"> | ||
| // src/_includes/blueprints/ - the `renderCompanyTile` shortcode from .eleventy.js, whose | ||
| // only two callers were the blueprint card and the blueprint layout. The .company-tile | ||
| // classes it emitted are still styled by src/css/style.css. | ||
| // | ||
| // The shortcode interpolated the company's fields straight into an HTML string. These come | ||
| // from a data file rather than page content, but they are still values from another | ||
| // repository, so they are bound as attributes and text here instead. | ||
| // | ||
| // The logo is decorative: the company name sits next to it as text, so an alt would just | ||
| // repeat it. The shortcode emitted no alt at all. | ||
| defineProps<{ | ||
| company: { name: string, img: string, url: string } | ||
| }>() | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="company-tile"> | ||
| <img class="company-tile-logo" :src="company.img" alt=""> | ||
| <a :href="company.url" class="no-underline text-gray-700">{{ company.name }}</a> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <script setup lang="ts"> | ||
| // src/blueprints.njk plus src/_includes/blueprints/template.njk and the | ||
| // layouts/catalog.njk hero it extended: the paginated Blueprint Library grid. | ||
| // | ||
| // Shared by pages/blueprints/index.vue and pages/blueprints/[page].vue, the way | ||
| // <BlogListing> is shared by the blog's listing routes. | ||
| import { BLUEPRINTS_DESCRIPTION, BLUEPRINTS_TITLE } from '../composables/useBlueprintList' | ||
|
|
||
| const props = defineProps<{ page: number }>() | ||
|
|
||
| const { entries, totalPages } = useBlueprintList(() => props.page) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="w-full px-6 page hero catalog-hero"> | ||
| <div class="container m-auto text-center flex py-8 max-w-lg md:max-w-6xl"> | ||
| <div class="text-left w-full"> | ||
| <h1>{{ BLUEPRINTS_TITLE }}</h1> | ||
| <p class="md:w-9/12">{{ BLUEPRINTS_DESCRIPTION }}</p> | ||
| <div class="flex gap-2 mt-8"> | ||
| <NuxtLink to="/blueprints/submit/" class="ff-btn ff-btn--primary flex-col uppercase">Submit Your Own</NuxtLink> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <div class="container m-auto text-left max-w-lg md:max-w-6xl pt-8 pb-12 w-full ff-full-bg gap-4"> | ||
| <ul class="flex flex-col sm:grid md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-12 pb-10 border-b"> | ||
| <BlueprintCard v-for="entry in entries" :key="entry.path" :entry="entry" /> | ||
| </ul> | ||
| <Pagination base-path="/blueprints" :page="page" :total-pages="totalPages" /> | ||
| <CtaContactUsLine /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <script setup lang="ts"> | ||
| // src/_includes/contact-us-cta-line.njk - the one-line band under the Blueprint Library | ||
| // grid and in the blueprint detail sidebar. A text band with an inline link, not a | ||
| // button, so it does not go through cta/CtaButton.vue like the other Cta* components. | ||
| // It shares their destination registry so the href stays in one place, but like the .njk | ||
| // it fires no event: adding tracking here would be a behaviour change, not a port. | ||
| import { CTA_DESTINATIONS } from '../lib/cta-destinations' | ||
|
|
||
| const HREF = CTA_DESTINATIONS.contactUs.href | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="bg-indigo-50 py-1 px-4 rounded-md w-full mx-auto text-center mt-12"> | ||
| <p>Looking for help with your project? <NuxtLink :to="HREF" class="underline">Contact us</NuxtLink>; our experts will be happy to provide a solution for your needs.</p> | ||
| </div> | ||
| </template> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // The Blueprint Library listing, as src/blueprints.njk paginated it: 12 per page. | ||
| export const BLUEPRINTS_PAGE_SIZE = 12 | ||
|
|
||
| // The listing copy, from src/blueprints.njk's frontmatter. `title` was the <h1> and | ||
| // `meta.title` the <title>; the wording differs between them in the source and is kept. | ||
| export const BLUEPRINTS_TITLE = 'Blueprint Library' | ||
| export const BLUEPRINTS_META_TITLE = 'Blueprints Library' | ||
| export const BLUEPRINTS_DESCRIPTION = 'Explore FlowFuse Blueprints, choose templates for quick setups, perfect for learning and fast solution-building. Customizable for unique needs. Simplify your Node-RED projects with FlowFuse Blueprints!' | ||
|
|
||
| export interface BlueprintListEntry { | ||
| path: string | ||
| title: string | ||
| description?: string | ||
| image?: string | ||
| tags?: string[] | ||
| author?: string | ||
| blueprintId?: string | ||
| } | ||
|
|
||
| // Ordered by path, descending. 11ty ordered the collection by date and reversed it, but no | ||
| // blueprint README carries a date, so every entry fell back to its file mtime - the CI | ||
| // checkout time, identical for all of them - and the tie broke on input path. Reverse path | ||
| // order is that same order, made explicit. The one blueprint that does set a `date` | ||
| // (manufacturing/oee-dashboard) sorted to the very end there and now sits with its | ||
| // category, which is the only visible difference. | ||
| const ORDER_FIELD = 'path' | ||
|
|
||
| // `page` is read through toValue so the slice follows a client-side move between listing | ||
| // pages, which reuses this component rather than remounting it. | ||
| export function useBlueprintList (page: MaybeRefOrGetter<number>) { | ||
| const { data: allEntries } = useAsyncData('blueprints-all', () => | ||
| queryCollection('blueprints') | ||
| .select('path', 'title', 'description', 'image', 'tags', 'author', 'blueprintId') | ||
| .order(ORDER_FIELD, 'DESC') | ||
| .all() as Promise<BlueprintListEntry[]> | ||
| ) | ||
|
|
||
| const totalPages = computed(() => Math.max(1, Math.ceil((allEntries.value || []).length / BLUEPRINTS_PAGE_SIZE))) | ||
|
|
||
| const entries = computed(() => { | ||
| const start = (Math.max(1, toValue(page)) - 1) * BLUEPRINTS_PAGE_SIZE | ||
| return (allEntries.value || []).slice(start, start + BLUEPRINTS_PAGE_SIZE) | ||
| }) | ||
|
|
||
| return { entries, totalPages } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Display helpers for the Blueprint Library, ported from src/_includes/blueprints/. | ||
| // Kept free of Nuxt and Vue imports so it can be unit tested with `node --test`; the | ||
| // blueprint pages and cards are the only callers. | ||
| // | ||
| // Everything here reads data that lives in FlowFuse/blueprint-library, so each helper is | ||
| // deliberately tolerant of a value it has never seen. | ||
|
|
||
| // Was src/_data/companies/*.json, read only by the two blueprint templates. A blueprint | ||
| // names its author in frontmatter (`author: signl`); everything unattributed is FlowFuse's, | ||
| // which is the fallback both templates spelled out as `companies["flowfuse"]`. | ||
| const COMPANIES = { | ||
| flowfuse: { name: 'FlowFuse', img: '/images/flowfuse-icon.png', url: 'https://flowfuse.com' }, | ||
| signl: { name: 'SIGNL4', img: '/images/signl4_logo.png', url: 'https://www.signl4.com/' }, | ||
| } | ||
|
|
||
| export function blueprintAuthor (author) { | ||
| return COMPANIES[author] || COMPANIES.flowfuse | ||
| } | ||
|
|
||
| // Every blueprint carries the `blueprints` tag, which is what put it in the collection; | ||
| // only the rest describe it. | ||
| export function blueprintTags (tags) { | ||
| return (tags || []).filter(tag => tag !== 'blueprints') | ||
| } | ||
|
|
||
| /** | ||
| * The label for one tag, reproducing what the Nunjucks card built with | ||
| * `replace('-', ' ') | replace('20', '2.0') | title`. | ||
| * | ||
| * An already-uppercase tag is printed as authored, which is how MES and HMI keep their | ||
| * capitals. That check is also why `ai` renders as "Ai": the fix for that is to capitalise | ||
| * the tag in the library's frontmatter, not to keep a list of acronyms here. | ||
| */ | ||
| export function blueprintTagLabel (tag) { | ||
| if (tag === tag.toUpperCase()) return tag | ||
| return tag | ||
| .replace(/-/g, ' ') | ||
| .replace(/20/g, '2.0') | ||
| .split(' ') | ||
| .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) | ||
| .join(' ') | ||
| } | ||
|
|
||
| /** The "Deploy" button target. The id comes from the library, so it is encoded here. */ | ||
| export function deployUrl (blueprintId) { | ||
| return `https://app.flowfuse.com/deploy/blueprint?blueprintId=${encodeURIComponent(blueprintId ?? '')}` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { test } from 'node:test' | ||
| import assert from 'node:assert/strict' | ||
|
|
||
| import { blueprintAuthor, blueprintTagLabel, blueprintTags, deployUrl } from './blueprint-display.mjs' | ||
|
|
||
| test('blueprintAuthor resolves a named author', () => { | ||
| assert.deepEqual(blueprintAuthor('signl'), { name: 'SIGNL4', img: '/images/signl4_logo.png', url: 'https://www.signl4.com/' }) | ||
| }) | ||
|
|
||
| test('blueprintAuthor falls back to FlowFuse for an unattributed or unknown author', () => { | ||
| assert.equal(blueprintAuthor(undefined).name, 'FlowFuse') | ||
| assert.equal(blueprintAuthor('a-partner-with-no-data-file').name, 'FlowFuse') | ||
| }) | ||
|
|
||
| test('blueprintTags drops the tag that defines the collection', () => { | ||
| assert.deepEqual(blueprintTags(['blueprints', 'manufacturing', 'MES']), ['manufacturing', 'MES']) | ||
| assert.deepEqual(blueprintTags(undefined), []) | ||
| }) | ||
|
|
||
| test('blueprintTagLabel reproduces every label the library currently produces', () => { | ||
| assert.equal(blueprintTagLabel('manufacturing'), 'Manufacturing') | ||
| assert.equal(blueprintTagLabel('getting-started'), 'Getting Started') | ||
| assert.equal(blueprintTagLabel('dashboard-20'), 'Dashboard 2.0') | ||
| assert.equal(blueprintTagLabel('dashboard-2.0'), 'Dashboard 2.0') | ||
| assert.equal(blueprintTagLabel('other'), 'Other') | ||
| // Acronyms are authored in capitals and printed as authored. | ||
| assert.equal(blueprintTagLabel('MES'), 'MES') | ||
| assert.equal(blueprintTagLabel('HMI'), 'HMI') | ||
| // The one label the source tag gets wrong; see the helper's comment. | ||
| assert.equal(blueprintTagLabel('ai'), 'Ai') | ||
| }) | ||
|
|
||
| test('deployUrl encodes the blueprint id', () => { | ||
| assert.equal(deployUrl('PaRL4JNeBM'), 'https://app.flowfuse.com/deploy/blueprint?blueprintId=PaRL4JNeBM') | ||
| assert.equal(deployUrl('a&b=c'), 'https://app.flowfuse.com/deploy/blueprint?blueprintId=a%26b%3Dc') | ||
| assert.equal(deployUrl(undefined), 'https://app.flowfuse.com/deploy/blueprint?blueprintId=') | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we already had a CTA component?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do, but they are all buttons through
cta/CtaButton.vue. This one is the one-line text band with an inline link, ported fromcontact-us-cta-line.njk, so there is no button to share. Renamed it toCtaContactUsLine.vueso it sits with the rest of the family.The wider point stands though: the teardown branch ends up with thirteen CTA-ish components. Consolidating them touches pages across five of the open migration PRs, so better as its own PR once 5777 is done.