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
17 changes: 17 additions & 0 deletions nuxt/components/industry/Cta.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
// The closing CTA band from layouts/industry.njk. Distinct from the use-case one: a
// single Book-a-demo button, and the copy is per page rather than defaulted.
defineProps<{ cta: { title: string, description?: string } }>()
</script>

<template>
<div class="w-full px-6 pt-12 sm:pt-16 pb-24">
<div class="ff-blue-card max-md:max-w-xl md:max-w-screen-lg mx-auto pt-12 pb-10">
<h3 class="mb-4 w-full text-center">{{ cta.title }}</h3>
<p class="text-center text-gray-600 mb-8 max-w-2xl mx-auto">{{ cta.description }}</p>
<div class="flex justify-center">
<CtaBookDemo variant="highlight" position="cta" class="min-h-[40px]" />
</div>
</div>
</div>
</template>
58 changes: 58 additions & 0 deletions nuxt/components/industry/Hero.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script setup lang="ts">
// The hero band from layouts/industry.njk. The eyebrow glyph and the accented heading
// half both came out of frontmatter; the glyph resolves through <NavIcon> (the same SVG
// file the .njk included), and the heading is inline markup the .njk printed with `| safe`.

defineProps<{
hero: {
eyebrow?: string
eyebrowIcon?: string
heading: string
description?: string
image: string
imageAlt?: string
subCta?: string
}
}>()
</script>

<template>
<div class="w-full">
<div class="w-full px-6 py-10 sm:py-12 md:pt-6">
<div class="md:flex md:my-16 md:flex-row md:justify-between container mx-auto max-md:text-left md:max-w-screen-lg gap-8 items-stretch">
<div class="m-auto md:w-1/2">
<div v-if="hero.eyebrow" class="mb-4 max-md:flex max-md:justify-start">
<span class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 industry-eyebrow-icon text-gray-800">
<span v-if="hero.eyebrowIcon" class="w-5 h-5 inline-flex items-center shrink-0">
<NavIcon :name="hero.eyebrowIcon" />
</span>
<span class="font-medium">{{ hero.eyebrow }}</span>
</span>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<h1 class="w-full mt-0 m-auto font-medium text-3xl sm:text-4xl" v-html="hero.heading" />
<p class="mb-10 text-gray-600">{{ hero.description }}</p>
<div class="md:mt-3 gap-2 hidden md:flex md:flex-col md:items-start md:justify-start">
<p v-if="hero.subCta" class="text-sm text-gray-600 m-0 mb-1">{{ hero.subCta }}</p>
<div class="flex gap-4 items-center flex-row">
<CtaBookDemo variant="highlight" position="hero" class="min-h-[40px]" />
<CtaContactUs variant="ghost" position="hero" icon="i-lucide-arrow-right" />
</div>
</div>
</div>
<div class="md:w-1/2 flex-grow relative">
<div class="ff-image-cover ff-image-rounded w-full h-full">
<img :src="hero.image" :alt="hero.imageAlt" width="496" class="w-full h-auto">
</div>
</div>
<div class="flex flex-col md:hidden gap-3 mt-8 items-start">
<p v-if="hero.subCta" class="text-sm text-gray-600 m-0 mb-1">{{ hero.subCta }}</p>
<div class="flex flex-row gap-4 items-center justify-start flex-wrap">
<CtaBookDemo variant="highlight" position="hero-mobile" class="min-h-[40px]" />
<CtaContactUs variant="ghost" position="hero-mobile" icon="i-lucide-arrow-right" />
</div>
</div>
</div>
</div>
</div>
</template>
33 changes: 33 additions & 0 deletions nuxt/components/industry/MatchingUseCases.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
// components/industry-use-cases.njk: every use-case whose `industries[]` names this
// industry. The mapping's source of truth is the Use Cases Asana project (Industry field).
const props = defineProps<{ slug: string, displayName: string }>()

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

const matching = computed(() =>
(all.value || [])
.filter(uc => uc.industries?.includes(props.slug))
.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase(), 'en'))
)
</script>

<template>
<div class="w-full py-16 sm:py-24 px-6 bg-gray-50 border-y border-gray-100">
<div class="max-w-screen-lg mx-auto">
<h2 class="max-md:text-center">Use cases in <span class="text-indigo-600">{{ displayName }}</span></h2>
<p class="mt-4 max-w-3xl text-gray-600">Operational workflow patterns teams in this industry build and own with FlowFuse.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-10">
<UseCaseCard
v-for="uc in matching"
:key="uc.slug"
:to="`/use-cases/${uc.slug}/`"
:title="uc.title"
:problem="uc.problem"
/>
</div>
</div>
</div>
</template>
32 changes: 32 additions & 0 deletions nuxt/components/industry/Outcomes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
// "Key Outcomes" from layouts/industry.njk.
defineProps<{
outcomes: {
title: string
subtitle?: string
items?: Array<{ svgPath?: string, title: string, description: string }>
}
}>()
</script>

<template>
<div class="w-full py-16 sm:py-24 px-6">
<div class="max-w-screen-lg mx-auto">
<div class="flex flex-col md:flex-row gap-12">
<h2 class="md:w-1/4 max-md:text-center">
<span class="text-indigo-600">{{ outcomes.title }}</span> {{ outcomes.subtitle }}
</h2>
<div class="md:w-3/4 flex flex-col gap-4">
<div v-for="item in outcomes.items" :key="item.title" class="bg-indigo-50 border border-indigo-200 rounded-lg p-5 flex items-center gap-6">
<div class="flex-shrink-0 w-8 h-8 text-indigo-500 mt-0.5">
<NavIcon :name="item.svgPath" />
</div>
<p class="m-0 text-xl font-light">
<strong>{{ item.title }}</strong> {{ item.description }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
36 changes: 36 additions & 0 deletions nuxt/components/industry/Problem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
// "The Problem Today" from layouts/industry.njk. `problemTitle` overrides the default
// heading on the one page that sets it.

defineProps<{
problems: string[]
image: string
title?: string
industry: string
}>()
</script>

<template>
<div class="w-full py-16 sm:py-24 px-6 bg-white">
<div class="max-w-screen-lg mx-auto">
<div class="flex flex-col md:flex-row gap-12 items-center">
<div class="md:w-1/2">
<!-- eslint-disable-next-line vue/no-v-html -->
<h2 v-if="title" class="max-md:text-center" v-html="title" />
<h2 v-else class="max-md:text-center"><span class="text-red-600">The Problem</span> Today</h2>
<ul class="mt-6 flex flex-col gap-6 list-none p-0 m-0">
<li v-for="problem in problems" :key="problem" class="flex items-start gap-3">
<span class="flex-shrink-0 w-12 h-12 bg-red-300 rounded-full flex items-center justify-center text-white">
<UIcon name="i-heroicons-x-mark" class="w-6 h-6" />
</span>
<p class="text-gray-700 m-0 pt-2">{{ problem }}</p>
</li>
</ul>
</div>
<div class="md:w-1/2 flex justify-center md:order-first">
<img :src="image" :alt="`Diagram of the problems ${industry} teams face today`" class="max-w-full h-auto">
</div>
</div>
</div>
</div>
</template>
36 changes: 36 additions & 0 deletions nuxt/components/industry/Solution.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
// "What You Get with FlowFuse" from layouts/industry.njk.

defineProps<{
solution: {
title: string
benefits?: Array<{ svgPath?: string, text: string }>
}
image: string
industry: string
}>()
</script>

<template>
<div class="w-full bg-[radial-gradient(ellipse_120%_120%_at_50%_120%,theme(colors.indigo.600)_0%,theme(colors.indigo.900)_100%)] py-16 sm:py-24 px-6">
<div class="max-w-screen-lg mx-auto">
<div class="flex flex-col md:flex-row gap-12 items-center">
<div class="md:w-1/2">
<h2 class="text-white max-md:text-center">{{ solution.title }}</h2>
<ul class="mt-8 flex flex-col gap-6 list-none p-0 m-0">
<li v-for="benefit in solution.benefits" :key="benefit.text" class="flex items-start gap-6">
<span class="flex-shrink-0 w-12 h-12 bg-indigo-50 rounded-full flex items-center justify-center text-indigo-600">
<span class="w-6 h-6"><NavIcon :name="benefit.svgPath" /></span>
</span>
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="text-indigo-100 m-0 font-light" v-html="benefit.text" />
</li>
</ul>
</div>
<div class="md:w-1/2 flex justify-center">
<img :src="image" :alt="`Diagram of how FlowFuse solves those problems for ${industry}`" class="max-w-full h-auto">
</div>
</div>
</div>
</div>
</template>
42 changes: 42 additions & 0 deletions nuxt/components/industry/UseCaseTiles.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
// The hand-authored "Use Cases" tile grid from layouts/industry.njk, which renders either
// a flat list of tiles or the same tiles under group labels.

type Tile = { image: string, imageAlt?: string, description: string }

const props = defineProps<{
useCases: {
title: string
items?: Tile[]
groups?: Array<{ label: string, items: Tile[] }>
}
}>()

// The .njk branched on `groups`; normalising to one shape keeps the tile markup written
// once instead of duplicated across both branches.
const groups = computed(() =>
props.useCases.groups?.length
? props.useCases.groups
: [{ label: '', items: props.useCases.items || [] }]
)
</script>

<template>
<div class="w-full py-16 px-6 comparison-section-bg">
<div class="max-w-screen-lg mx-auto">
<h2 class="text-center text-indigo-600 mb-10">{{ useCases.title }}</h2>
<div v-for="group in groups" :key="group.label" :class="group.label ? 'mb-10' : ''">
<h3 v-if="group.label" class="text-sm font-semibold uppercase text-indigo-400 mb-4">{{ group.label }}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
<div v-for="item in group.items" :key="item.image" class="flex flex-col gap-3 rounded-xl border border-white p-3 bg-[linear-gradient(135deg,_theme(colors.white)_0%,_theme(colors.white/10%)_100%)]">
<div class="w-full h-48 ff-image-cover rounded overflow-hidden">
<img :src="item.image" :alt="item.imageAlt" width="400" loading="lazy" class="w-full h-auto">
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="m-0 py-1" v-html="item.description" />
</div>
</div>
</div>
</div>
</div>
</template>
9 changes: 5 additions & 4 deletions nuxt/components/use-case/Card.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<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.
// The cross-link card shared by the /use-cases/ listing, components/use-case-links.njk,
// and components/industry-use-cases.njk (now IndustryMatchingUseCases.vue) — one look
// across all three call sites.
withDefaults(defineProps<{
to: string
title: string
Expand All @@ -13,9 +14,9 @@ withDefaults(defineProps<{
<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">
<span class="mt-2 text-blue-600 text-sm flex items-center gap-1.5 group-hover:underline">
{{ label }}
<UIcon name="i-heroicons-arrow-right" class="w-4 h-4" />
<UIcon name="i-heroicons-arrow-long-right" class="shrink-0 w-4 h-4" />
</span>
</NuxtLink>
</template>
76 changes: 76 additions & 0 deletions nuxt/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,82 @@ export default defineContentConfig({
}).optional(),
})
}),
// The seven industry pages were pure 11ty frontmatter read by layouts/industry.njk,
// the same shape as the operational use-cases. /industries/automotive/ is not here:
// it had a bespoke markup body and stays a hand-written Vue page, which is why the
// listing merges this collection with that one entry.
//
// Named industriesLegacy, not industries: these seven pages are transitional and
// due to be deleted once the automotive-page template covers them (or their
// replacements). `industries` is reserved for that template's own collection.
industriesLegacy: defineCollection({
type: 'data',
source: 'industries-legacy/*.yml',
schema: z.object({
slug: z.string(),
// Named seoMeta rather than meta because `meta` is reserved by
// @nuxt/content and a declared `meta` field is silently replaced.
seoMeta: z.object({
title: z.string(),
description: z.string(),
}),
metaTitle: z.string().optional(),
hero: z.object({
eyebrow: z.string().optional(),
// A NavIcon registry key. The .njk named an SVG path under
// src/_includes/; the lift rewrote those to keys.
eyebrowIcon: z.string().optional(),
heading: z.string(),
description: z.string().optional(),
image: z.string(),
imageAlt: z.string().optional(),
subCta: z.string().optional(),
}),
// Overrides the default "The Problem Today" heading.
problemTitle: z.string().optional(),
problems: z.array(z.string()),
problemImage: z.string(),
solution: z.object({
title: z.string(),
benefits: z.array(z.object({
svgPath: z.string().optional(),
text: z.string(),
})).optional(),
}),
solutionImage: z.string(),
outcomes: z.object({
title: z.string(),
subtitle: z.string().optional(),
items: z.array(z.object({
svgPath: z.string().optional(),
title: z.string(),
description: z.string(),
})).optional(),
}),
// Either a flat `items` list or the same tiles under `groups`.
useCases: z.object({
title: z.string(),
items: z.array(z.object({
image: z.string(),
imageAlt: z.string().optional(),
description: z.string(),
})).optional(),
groups: z.array(z.object({
label: z.string(),
items: z.array(z.object({
image: z.string(),
imageAlt: z.string().optional(),
description: z.string(),
})),
})).optional(),
}),
socialProofText: z.string().optional(),
cta: z.object({
title: z.string(),
description: z.string().optional(),
}),
})
}),
ebooks: defineCollection({
type: 'page',
source: 'ebooks/*.md',
Expand Down
Loading
Loading