Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addLayoutAlias('default', 'layouts/base.njk');
eleventyConfig.addLayoutAlias('page', 'layouts/page.njk');
eleventyConfig.addLayoutAlias('nohero', 'layouts/nohero.njk');
eleventyConfig.addLayoutAlias('solution', 'layouts/solution.njk');
eleventyConfig.addLayoutAlias('use-case', 'layouts/use-case.njk');
eleventyConfig.addLayoutAlias('catalog', 'layouts/catalog.njk');
eleventyConfig.addLayoutAlias('redirect', 'layouts/redirect.njk');

Expand Down
9 changes: 6 additions & 3 deletions nuxt/components/NavIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
// carries the sizing class, leaving the file's own <svg> nested inside. This
// reproduces that exactly, so a given icon key renders identically whichever
// renderer served the page.
const props = withDefaults(defineProps<{ name?: string, solid?: boolean }>(), {
const props = withDefaults(defineProps<{ name?: string, solid?: boolean, size?: 'sm' | 'lg' }>(), {
name: '',
solid: false,
// `navoption` (nav and footer) wraps icons at ff-icon-sm; `ffIconLg`, which page
// content used, is the same wrapper at ff-icon-lg. One prop covers both shortcodes.
size: 'sm',
})

const markup = computed(() => {
Expand All @@ -23,8 +26,8 @@ const markup = computed(() => {
<template>
<svg
v-if="markup"
class="ff-icon ff-icon-sm"
:class="{ 'ff-icon--solid': solid }"
class="ff-icon"
:class="[`ff-icon-${size}`, { 'ff-icon--solid': solid }]"
fill="none"
viewBox="0 0 24 24"
v-html="markup"
Expand Down
48 changes: 48 additions & 0 deletions nuxt/components/use-case/AiBuildLayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
// "06 Build it with AI" from layouts/use-case.njk. The .njk wrote its CTA as a raw
// <a class="ff-btn"> with an inline capture() call; this keeps the same event name and
// props but routes it through useCapture like every other Nuxt CTA.
const props = defineProps<{
block: {
heading?: string
intro?: string
steps?: Array<{ title: string, detail: string }>
note?: string
}
slug: string
}>()

const capture = useCapture()

function ordinal(index: number) {
return String(index + 1).padStart(2, '0')
}
</script>

<template>
<div id="ai-build-layer" class="w-full py-16 sm:py-24 px-6 bg-gray-50 border-y border-gray-100 scroll-mt-16">
<div class="max-w-screen-lg mx-auto">
<p class="uppercase text-xs font-semibold text-indigo-400 mb-2">06 · Build it with AI</p>
<h2 class="max-md:text-center">{{ block.heading || 'From described to deployed, with the FlowFuse Expert' }}</h2>
<p v-if="block.intro" class="mt-4 max-w-3xl text-gray-600">{{ block.intro }}</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12">
<div v-for="(step, i) in block.steps" :key="step.title" class="rounded-xl bg-white border border-gray-200 p-6 flex flex-col gap-2">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-sparkles" class="w-6 h-6 text-indigo-500" />
<span class="text-indigo-500 text-xs font-semibold uppercase tracking-widest">Step {{ ordinal(i) }}</span>
</div>
<h4 class="m-0 text-gray-800">{{ step.title }}</h4>
<p class="m-0 text-gray-600 text-sm">{{ step.detail }}</p>
</div>
</div>
<div class="mt-10 flex">
<NuxtLink
class="ff-btn ff-btn--primary-outlined uppercase"
to="/ai/"
@click="capture('cta-ai', { position: 'use-case-ai-build-layer', page: props.slug })"
>Get started with the Expert</NuxtLink>
</div>
<p v-if="block.note" class="mt-8 text-sm text-gray-400">{{ block.note }}</p>
</div>
</div>
</template>
21 changes: 21 additions & 0 deletions nuxt/components/use-case/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
// The cross-link card shared by the /use-cases/ listing and components/use-case-links.njk,
// which rendered identical markup in both places.
withDefaults(defineProps<{
to: string
title: string
problem?: string
label?: string
}>(), { label: 'View use case' })
</script>

<template>
<NuxtLink :to="to" class="group hover:no-underline flex flex-col gap-3 rounded-xl border border-gray-200 p-6 bg-white hover:border-indigo-300 hover:shadow-sm transition-all">
<h3 class="m-0 text-gray-800 group-hover:text-indigo-600 transition-colors">{{ title }}</h3>
<p class="m-0 text-gray-600 text-sm flex-grow">{{ problem }}</p>
<span class="mt-2 text-indigo-600 text-sm font-semibold flex items-center gap-2">
{{ label }}
<UIcon name="i-heroicons-arrow-right" class="w-4 h-4" />
</span>
</NuxtLink>
</template>
29 changes: 29 additions & 0 deletions nuxt/components/use-case/ClosingCta.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
// The closing CTA band that layouts/use-case.njk and src/use-cases.njk both ended on.
// The markup and the sign-up button are identical; they differ only in the lead CTA
// (the layout asks for a demo, the listing asks for contact), so that stays a prop
// rather than being flattened to one of the two.
withDefaults(defineProps<{
heading?: string
description?: string
lead?: 'book-demo' | 'contact-us'
}>(), {
heading: 'See it on your operations',
description: 'Talk to an expert, or get started with FlowFuse today.',
lead: 'book-demo',
})
</script>

<template>
<div class="w-full px-6 py-20">
<div class="ff-blue-card max-md:max-w-xl md:max-w-screen-lg mx-auto pt-12 pb-10 text-center">
<h3 class="mb-4 w-full text-center">{{ heading }}</h3>
<p class="text-gray-600 mb-8 max-w-xl mx-auto text-center">{{ description }}</p>
<div class="flex flex-wrap gap-4 justify-center">
<CtaBookDemo v-if="lead === 'book-demo'" variant="highlight" position="footer" class="min-h-[40px]" />
<CtaContactUs v-else variant="highlight" position="footer" class="min-h-[40px]" />
<CtaSignUp variant="primary-outlined" position="footer" class="min-h-[40px]" />
</div>
</div>
</div>
</template>
43 changes: 43 additions & 0 deletions nuxt/components/use-case/Comparison.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
// "05 With / without FlowFuse" from layouts/use-case.njk.
defineProps<{
block: {
without?: Array<{ title: string, detail: string }>
with?: Array<{ title: string, detail: string }>
}
}>()
</script>

<template>
<div id="with-without" class="w-full py-16 sm:py-24 px-6 comparison-section-bg scroll-mt-16">
<div class="max-w-screen-lg mx-auto">
<p class="uppercase text-xs font-semibold text-indigo-400 mb-2">05 · With / without FlowFuse</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mt-6">
<div>
<h3 class="text-gray-500 m-0">Without FlowFuse</h3>
<div class="mt-6 flex flex-col gap-4">
<div v-for="row in block.without" :key="row.title" class="bg-white border border-gray-200 rounded-lg p-5 flex items-start gap-4">
<UIcon name="i-heroicons-x-mark" class="flex-shrink-0 w-6 h-6 text-red-400 mt-0.5" />
<div>
<h5 class="m-0 text-gray-800">{{ row.title }}</h5>
<p class="m-0 mt-1 text-gray-600 text-sm">{{ row.detail }}</p>
</div>
</div>
</div>
</div>
<div>
<h3 class="text-indigo-600 m-0">With FlowFuse</h3>
<div class="mt-6 flex flex-col gap-4">
<div v-for="row in block.with" :key="row.title" class="bg-white border border-indigo-100 rounded-lg p-5 flex items-start gap-4">
<UIcon name="i-heroicons-check-circle" class="flex-shrink-0 w-6 h-6 text-indigo-500 mt-0.5" />
<div>
<h5 class="m-0 text-gray-800">{{ row.title }}</h5>
<p class="m-0 mt-1 text-gray-600 text-sm">{{ row.detail }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
30 changes: 30 additions & 0 deletions nuxt/components/use-case/Competition.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
// "04 Why off-the-shelf doesn't work" from layouts/use-case.njk.
defineProps<{
block: {
heading: string
intro?: string
traps?: Array<{ label: string, title: string, detail: string }>
}
}>()
</script>

<template>
<div id="competition" class="w-full py-16 sm:py-24 px-6 bg-white scroll-mt-16">
<div class="max-w-screen-lg mx-auto">
<p class="uppercase text-xs font-semibold text-indigo-400 mb-2">04 · Why off-the-shelf doesn't work</p>
<h2 class="max-md:text-center">{{ block.heading }}</h2>
<p v-if="block.intro" class="mt-4 max-w-3xl text-gray-600">{{ block.intro }}</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-12">
<div v-for="trap in block.traps" :key="trap.title" class="rounded-xl bg-gray-50 border border-gray-200 p-6 flex flex-col gap-2">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-x-circle" class="w-6 h-6 text-red-400" />
<span class="text-red-500 text-xs font-semibold uppercase tracking-widest">{{ trap.label }}</span>
</div>
<h4 class="m-0 text-gray-800">{{ trap.title }}</h4>
<p class="m-0 text-gray-600 text-sm">{{ trap.detail }}</p>
</div>
</div>
</div>
</div>
</template>
17 changes: 17 additions & 0 deletions nuxt/components/use-case/Hero.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
// The hero band from layouts/use-case.njk. Ported verbatim, classes included.
defineProps<{ title: string, problem?: string }>()
</script>

<template>
<div class="w-full px-6 bg-[radial-gradient(ellipse_120%_140%_at_50%_-20%,theme(colors.indigo.50)_0%,theme(colors.white)_60%)]">
<div class="max-w-screen-lg mx-auto py-16 sm:py-24 text-center">
<p class="uppercase text-sm font-semibold text-indigo-500 mb-4">Use case</p>
<h1 class="m-auto max-w-3xl font-medium"><span class="text-indigo-600">{{ title }}</span></h1>
<p v-if="problem" class="mt-6 max-w-2xl mx-auto text-lg text-gray-600">{{ problem }}</p>
<div class="mt-10 flex flex-wrap gap-4 justify-center">
<CtaBookDemo variant="highlight" position="hero" class="min-h-[40px]" />
</div>
</div>
</div>
</template>
30 changes: 30 additions & 0 deletions nuxt/components/use-case/IndustryChips.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
// components/use-case-chips.njk. The slug -> label map mirrors the Use Cases Asana
// project fields, which are its source of truth.
defineProps<{ industries?: string[] }>()

const INDUSTRY_LABELS: Record<string, string> = {
'automotive': 'Automotive',
'food-beverage': 'Food & Beverage',
'life-sciences': 'Life Sciences',
'aviation-aerospace': 'Aviation & Aerospace',
'aerospace-components': 'Aerospace Components',
'renewables': 'Renewables',
'semiconductors': 'Semiconductors',
'electronics-appliances': 'Electronics & Appliances',
}
</script>

<template>
<div v-if="industries?.length" class="w-full px-6 py-10 bg-gray-50 border-y border-gray-100">
<div class="max-w-screen-lg mx-auto grid grid-cols-1 sm:grid-cols-[12rem_1fr] gap-x-8 gap-y-4 items-baseline">
<span class="uppercase text-xs font-semibold text-gray-400 tracking-widest">Common in these industries</span>
<p class="m-0 text-gray-700">
<template v-for="(slug, i) in industries" :key="slug">
<NuxtLink :to="`/industries/${slug}/`" class="text-indigo-600 hover:text-indigo-800 font-medium">{{ INDUSTRY_LABELS[slug] || slug }}</NuxtLink><template v-if="i < industries.length - 1">, </template>
</template>
<span class="text-gray-400"> · </span> <NuxtLink to="/industries/" class="text-gray-500 hover:text-indigo-600 text-sm">all industries</NuxtLink>
</p>
</div>
</div>
</template>
27 changes: 27 additions & 0 deletions nuxt/components/use-case/Links.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
// components/use-case-links.njk: cross-link cards whose copy is read from each use-case's
// own definition, so a title or problem line is written once. The .njk looped the whole
// collection per slug to preserve the caller's order; a lookup does the same thing here.
const props = defineProps<{ slugs: string[] }>()

const { data: all } = await useAsyncData('use-case-links', () =>
queryCollection('useCases').select('slug', 'title', 'problem').all()
)

const cards = computed(() => {
const bySlug = new Map((all.value || []).map(uc => [uc.slug, uc]))
return props.slugs.map(slug => bySlug.get(slug)).filter(Boolean)
})
</script>

<template>
<div v-if="cards.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-10">
<UseCaseCard
v-for="card in cards"
:key="card.slug"
:to="`/use-cases/${card.slug}/`"
:title="card.title"
:problem="card.problem"
/>
</div>
</template>
27 changes: 27 additions & 0 deletions nuxt/components/use-case/Outcomes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
// "02 Outcome first" from layouts/use-case.njk.
defineProps<{
block: {
heading: string
intro?: string
dimensions?: Array<{ label: string, title: string, detail: string }>
}
}>()
</script>

<template>
<div id="outcome-first" class="w-full py-16 sm:py-24 px-6 bg-gray-50 border-y border-gray-100 scroll-mt-16">
<div class="max-w-screen-lg mx-auto">
<p class="uppercase text-xs font-semibold text-indigo-400 mb-2">02 · Outcome first</p>
<h2 class="max-md:text-center">{{ block.heading }}</h2>
<p v-if="block.intro" class="mt-4 max-w-3xl text-gray-600">{{ block.intro }}</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-12">
<div v-for="dim in block.dimensions" :key="dim.title" class="rounded-xl bg-white border border-gray-200 p-6 flex flex-col gap-2">
<span class="text-indigo-500 text-xs font-semibold uppercase tracking-widest">{{ dim.label }}</span>
<h4 class="m-0 text-gray-800">{{ dim.title }}</h4>
<p class="m-0 text-gray-600 text-sm">{{ dim.detail }}</p>
</div>
</div>
</div>
</div>
</template>
33 changes: 33 additions & 0 deletions nuxt/components/use-case/Pain.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
// "01 Customer pain" from layouts/use-case.njk.
// The .njk pulled card icons with {% include "components/icons/<name>.svg" %}; content
// now names the Iconify glyph directly, so <UIcon> resolves it with nothing in between.
defineProps<{
block: {
heading: string
intro?: string[]
cards?: Array<{ icon?: string, title: string, detail: string }>
}
}>()
</script>

<template>
<div id="customer-pain" class="w-full py-16 sm:py-24 px-6 bg-white scroll-mt-16">
<div class="max-w-screen-lg mx-auto">
<p class="uppercase text-xs font-semibold text-indigo-400 mb-2">01 · Customer pain</p>
<h2 class="max-md:text-center"><span class="text-red-600">{{ block.heading }}</span></h2>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
<p v-for="(paragraph, i) in block.intro" :key="i" class="text-gray-600 m-0">{{ paragraph }}</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 mt-12">
<div v-for="card in block.cards" :key="card.title" class="rounded-xl bg-gray-50 border border-gray-200 p-6 flex flex-col gap-3">
<div class="w-9 h-9 bg-white rounded-lg border border-gray-200 flex items-center justify-center text-red-500">
<UIcon v-if="card.icon" :name="card.icon" class="w-5 h-5" />
</div>
<h4 class="m-0 text-gray-800">{{ card.title }}</h4>
<p class="m-0 text-gray-600 text-sm">{{ card.detail }}</p>
</div>
</div>
</div>
</div>
</template>
24 changes: 24 additions & 0 deletions nuxt/components/use-case/SectionNav.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
// The sticky in-page nav from layouts/use-case.njk. The .njk built its item list by
// testing each block for existence in turn; the renderer passes the resolved list here.
defineProps<{ items: Array<{ id: string, label: string }> }>()

function ordinal(index: number) {
return String(index + 1).padStart(2, '0')
}
</script>

<template>
<nav v-if="items.length" class="sticky top-0 z-30 w-full bg-white/90 backdrop-blur border-y border-gray-100">
<div class="max-w-screen-lg mx-auto px-6">
<ul class="flex gap-1 sm:gap-2 overflow-x-auto list-none m-0 p-0 py-2 text-sm">
<li v-for="(item, i) in items" :key="item.id" class="flex-shrink-0">
<a :href="`#${item.id}`" class="flex items-center gap-2 px-3 py-1.5 rounded-full text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 transition-colors">
<span class="font-semibold text-indigo-400">{{ ordinal(i) }}</span>
<span class="whitespace-nowrap">{{ item.label }}</span>
</a>
</li>
</ul>
</div>
</nav>
</template>
Loading
Loading