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
68 changes: 68 additions & 0 deletions nuxt/components/industry-page/Applications.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
// The zigzag "Applications for X Manufacturing" band from automotive.vue.
type Item = {
title: string
description: string
linkText: string
linkHref: string
image: string
imageAlt: string
variant: 'indigo' | 'red' | 'mixed'
}

defineProps<{ heading: string, description: string, items: Item[] }>()

const VARIANTS: Record<string, { gradient: string, border: string, mobileGradient: string, mobileBorder: string }> = {
indigo: { gradient: 'from-indigo-400 via-indigo-300 to-indigo-100', border: 'border-indigo-300', mobileGradient: 'from-indigo-100 to-indigo-50', mobileBorder: 'border-indigo-200' },
red: { gradient: 'from-red-200 via-red-100 to-red-50', border: 'border-red-100', mobileGradient: 'from-red-50 to-white', mobileBorder: 'border-red-100' },
mixed: { gradient: 'from-red-200 to-indigo-100', border: 'border-indigo-100', mobileGradient: 'from-red-50 to-indigo-50', mobileBorder: 'border-indigo-100' },
}
</script>

<template>
<section class="w-full relative pb-20 pt-14 px-6 overflow-hidden">
<div class="absolute inset-x-0 top-0 h-[400px] md:h-[561px] solution-section-bg-flipped" aria-hidden="true" />
<div class="relative z-10">
<div class="max-w-screen-lg mx-auto max-md:text-center mb-28 md:mt-10">
<h2 class="text-indigo-600 text-4xl md:text-5xl">{{ heading }}</h2>
<p class="text-gray-600 text-lgmd:text-2xl">{{ description }}</p>
</div>

<div class="md:max-w-screen-lg mx-auto">
<div
v-for="(item, index) in items"
:key="item.title"
class="max-md:text-center md:flex md:flex-row gap-8 items-center m-auto mb-14 md:mb-20"
:class="{ 'md:flex-row-reverse': index % 2 === 1 }"
>
<!-- Image (desktop): full-size screenshot offset over a colored panel -->
<div class="max-md:hidden md:w-[45%]">
<div class="relative aspect-[430/289]">
<div
class="absolute -top-5 w-full h-full rounded-lg bg-gradient-to-tl"
:class="[VARIANTS[item.variant].gradient, index % 2 === 0 ? '-left-5' : '-right-5']"
/>
<div class="absolute inset-0 rounded-lg border-2 overflow-hidden ff-image-cover" :class="VARIANTS[item.variant].border">
<img :src="item.image" :alt="item.imageAlt" loading="lazy">
</div>
</div>
</div>
<!-- Text -->
<div class="md:w-[55%] flex flex-col gap-3">
<h3 class="text-gray-700 text-3xl font-medium m-0">{{ item.title }}</h3>
<div class="md:hidden rounded-xl w-full bg-gradient-to-tl max-w-[500px] mx-auto my-4 p-4" :class="VARIANTS[item.variant].mobileGradient">
<div class="ff-image-rounded w-full border overflow-hidden" :class="VARIANTS[item.variant].mobileBorder">
<img :src="item.image" :alt="item.imageAlt" loading="lazy">
</div>
</div>
<p class="text-gray-500 m-0">{{ item.description }}</p>
<a :href="item.linkHref" class="flex items-center gap-1.5 text-blue-600 hover:underline max-md:justify-center">
{{ item.linkText }}
<UIcon name="i-heroicons-arrow-long-right" class="w-6 h-6 shrink-0" />
</a>
</div>
</div>
</div>
</div>
</section>
</template>
38 changes: 38 additions & 0 deletions nuxt/components/industry-page/Compliance.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
// The compliance grid from automotive.vue. "The Data Your Audit Asks For, In One Place"
// is fixed copy shared by every page built on this template, not page-specific data.
interface ComplianceItem { title: string, text: string, linkText: string, linkHref: string, icon?: string }

defineProps<{ industryName: string, description: string, items: ComplianceItem[] }>()
</script>

<template>
<section class="w-full py-16 md:py-24 px-6 bg-radial-[ellipse_60%_70%_at_center_bottom] from-blue-200/30 to-blue-200/0">
<div class="max-w-screen-lg mx-auto">
<div class="mb-12">
<h2 class="text-gray-700 mb-2">{{ industryName }} <span class="text-indigo-600">Compliance &amp; Standards</span></h2>
<p class="text-xl font-medium mt-0 mb-4">The Data Your Audit Asks For, In One Place</p>
<p class="text-gray-600">{{ description }}</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<a
v-for="item in items"
:key="item.title"
:href="item.linkHref"
class="group hover:no-underline flex flex-col gap-3 rounded-xl border border-gray-200 p-5 bg-white hover:border-indigo-300 hover:shadow-sm transition-all"
>
<div class="w-6 h-6 text-indigo-600">
<UIcon v-if="item.icon" :name="item.icon" class="size-6" />
<IconsCertificateIcon v-else />
</div>
<h3 class="m-0 text-lg font-medium text-gray-800 group-hover:text-indigo-600 transition-colors">{{ item.title }}</h3>
<p class="m-0 text-gray-600 text-sm flex-grow">{{ item.text }}</p>
<span class="mt-2 text-blue-600 text-sm flex items-center gap-1.5 group-hover:underline">
{{ item.linkText }}
<UIcon name="i-heroicons-arrow-long-right" class="w-6 h-6 shrink-0" />
</span>
</a>
</div>
</div>
</section>
</template>
18 changes: 18 additions & 0 deletions nuxt/components/industry-page/Cta.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
// The closing CTA band from automotive.vue. Distinct from industry-page/Roi's fixed link
// and from the industriesLegacy Cta band: this one is the "get started" gradient card,
// always ending in a single Book-a-demo button.
defineProps<{ heading: string, description: string }>()
</script>

<template>
<div class="w-full px-6 pb-20">
<div class="max-w-screen-lg mx-auto">
<div class="rounded-xl px-9 py-12 flex flex-col items-center gap-8 text-center ff-get-started-bg">
<p class="text-white text-5xl font-medium m-0">{{ heading }}</p>
<p class="text-indigo-50 font-light text-xl max-w-2xl m-0">{{ description }}</p>
<CtaBookDemo variant="highlight" position="final-cta" />
</div>
</div>
</div>
</template>
67 changes: 67 additions & 0 deletions nuxt/components/industry-page/Hero.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script setup lang="ts">
// The hero band from pages/industries/automotive.vue: eyebrow badge, accent heading,
// buttons, a customer-quote card, and an optional SocialProof line. The metrics grid and
// customer-story bridge live in industry-page/Metrics.vue, rendered right after this.
defineProps<{
hero: {
eyebrow: string
eyebrowIcon: string
heading: string
description: string
quote: {
text: string
author: string
role: string
company: string
image: string
imageAlt: string
}
}
socialProof?: string
}>()
</script>

<template>
<section class="w-full relative">
<div class="px-6 pt-16 md:pt-20">
<div class="max-w-screen-lg mx-auto grid md:grid-cols-2 gap-12 items-stretch">
<div class="max-md:text-center md:flex md:flex-col md:justify-center">
<div class="mb-4 max-md:flex max-md:justify-center">
<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 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="text-4xl md:text-5xl font-medium m-0" v-html="hero.heading" />
<p class="mt-6 text-gray-700">{{ hero.description }}</p>
<div class="mt-8 flex gap-4 items-center max-md:justify-center flex-wrap">
<CtaBookDemo variant="highlight" position="hero" />
<CtaPricing variant="ghost" position="hero" icon="i-lucide-arrow-right" />
</div>
</div>
<div class="w-full md:flex md:items-center">
<div class="relative w-full md:pt-6 max-md:mt-10">
<div class="relative rounded-lg border border-indigo-200 p-6 pt-28 bg-indigo-50/50">
<div class="absolute -top-14 left-1/2 -translate-x-1/2 md:left-8 md:translate-x-0 w-36 h-36 rounded-full bg-red-200 border-4 border-white shadow-lg overflow-hidden ff-image-cover">
<img :src="hero.quote.image" :alt="hero.quote.imageAlt" loading="eager">
</div>
<p class="italic text-gray-600 font-medium m-0">&ldquo;{{ hero.quote.text }}&rdquo;</p>
<p class="text-gray-500 text-right mt-4 mb-0">{{ hero.quote.author }}, {{ hero.quote.role }}, <span class="font-semibold text-gray-600">{{ hero.quote.company }}</span></p>
</div>
</div>
</div>
</div>
<div class="max-w-screen-lg mx-auto mt-16 text-center">
<div class="mx-auto text-center -mt-0.5 -mb-10">
<!-- No `socialProof`: falls back to SocialProof's own generic default line,
same as any other page that doesn't pass `eyebrow`. `?? undefined`
matters here — an absent optional @nuxt/content field comes through as
`null`, and SocialProof's own `withDefaults` only substitutes for
`undefined`, not `null`. -->
<SocialProof :eyebrow="socialProof ?? undefined" eyebrow-bg="indigo" />
</div>
</div>
</div>
</section>
</template>
39 changes: 39 additions & 0 deletions nuxt/components/industry-page/Metrics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
// The metrics grid from pages/industries/automotive.vue, plus an optional customer-story
// bridge (heading + description above the grid, link below it, right-aligned). Split out
// of industry-page/Hero.vue so the hero band and this one can change independently.
defineProps<{
metrics: Array<{ number: string, text: string }>
metricsBridge?: { heading: string, description: string, linkText: string, linkHref: string }
}>()
</script>

<template>
<div class="w-full px-6">
<!-- The radial fade only applies when there's a bridge above the metrics — the same
soft-red look .bg-radial-red gives automotive's own metrics band elsewhere, just
lighter (from-red-100/15, vs. that class's fixed 0.3 alpha) and built as a plain
Tailwind arbitrary gradient so the alpha's easy to tune per page if needed. -->
<div
class="max-w-md sm:max-w-screen-lg mx-auto mt-16 pb-10"
:class="{ 'bg-radial-[ellipse_50%_70%_at_center_bottom] from-red-50/50 to-red-100/0': metricsBridge }"
>
<div v-if="metricsBridge" class="max-md:text-center mb-8">
<h2 class="text-gray-700 mb-4 mt-20" v-html="metricsBridge.heading" />
<p class="text-gray-600">{{ metricsBridge.description }}</p>
</div>
<div class="grid sm:grid-cols-3 gap-12 my-4 max-sm:w-full m-auto">
<div v-for="metric in metrics" :key="metric.number" class="w-full h-full rounded-lg bg-red-50/70 pb-3 px-6 pt-6">
<h3 class="text-5xl font-semibold text-red-400">{{ metric.number }}</h3>
<p class="mt-0 font-normal leading-6">{{ metric.text }}</p>
</div>
</div>
<div v-if="metricsBridge" class="text-right mt-8">
<a :href="metricsBridge.linkHref" class="inline-flex items-center gap-1.5 text-blue-600 hover:underline">
{{ metricsBridge.linkText }}
<UIcon name="i-heroicons-arrow-long-right" class="w-4 h-4 shrink-0" />
</a>
</div>
</div>
</div>
</template>
21 changes: 21 additions & 0 deletions nuxt/components/industry-page/Roi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
// "The problem, and what it costs" band from automotive.vue. The calculator itself and
// the "open the full calculator" link are identical on every page, so they stay fixed
// here rather than becoming per-page data.
defineProps<{ heading: string, description: string }>()
</script>

<template>
<section class="w-full py-16 px-6">
<div class="max-w-screen-lg mx-auto">
<div class="max-md:mx-auto max-md:text-center">
<h2 class="text-gray-700 mb-4" v-html="heading" />
<p class="text-gray-600 m-0">{{ description }}</p>
</div>
<div class="mt-10">
<RoiCalculator compact />
</div>
<p class="text-center text-sm text-gray-500 mt-8">Want to tune every assumption and see the research behind it? <a href="/resources/roi-calculator/" class="text-indigo-600 font-semibold hover:underline">Open the full ROI calculator</a>.</p>
</div>
</section>
</template>
83 changes: 83 additions & 0 deletions nuxt/components/industry-page/Template.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script setup lang="ts">
// The shared page body for every industry page built on the automotive.vue template.
// A thin pages/industries/<slug>.vue queries its own `industries` collection entry and
// renders it through this one component, so the band markup lives in exactly one place.
const props = defineProps<{
page: {
slug: string
industryName: string
seoMeta: { title: string, description: string }
hero: {
eyebrow: string
eyebrowIcon: string
heading: string
description: string
quote: { text: string, author: string, role: string, company: string, image: string, imageAlt: string }
}
socialProof?: string
metrics: Array<{ number: string, text: string }>
metricsBridge?: { heading: string, description: string, linkText: string, linkHref: string }
roi: { heading: string, description: string }
applications: {
heading: string
description: string
items: Array<{ title: string, description: string, linkText: string, linkHref: string, image: string, imageAlt: string, variant: 'indigo' | 'red' | 'mixed' }>
}
compliance: {
description: string
items: Array<{ title: string, text: string, linkText: string, linkHref: string, icon?: string }>
}
faqs: Array<{ question: string, answer: string }>
closingCta: { heading: string, description: string }
}
}>()

useSeoMeta({
title: props.page.seoMeta.title,
description: props.page.seoMeta.description,
ogUrl: `https://flowfuse.com/industries/${props.page.slug}/`,
twitterSite: '@FlowFuseinc',
})

useSchemaOrg([
defineWebPage({ '@type': 'FAQPage' }),
...props.page.faqs.map(item => defineQuestion(item)),
])
</script>

<template>
<div class="w-full">
<IndustryPageHero :hero="page.hero" :social-proof="page.socialProof" />

<IndustryPageMetrics :metrics="page.metrics" :metrics-bridge="page.metricsBridge" />

<IndustryPageRoi :heading="page.roi.heading" :description="page.roi.description" />

<IndustryPageApplications
:heading="page.applications.heading"
:description="page.applications.description"
:items="page.applications.items"
/>

<EnterpriseSecurity />

<IndustryPageCompliance
:industry-name="page.industryName"
:description="page.compliance.description"
:items="page.compliance.items"
/>

<!-- Dynamic, not page data: every use-case whose own `industries[]` field names this
slug shows up here. Shared with the industriesLegacy [slug].vue's own band. -->
<IndustryPageUseCases :slug="page.slug" :display-name="page.industryName" />

<div class="w-full px-6 pt-20 pb-10">
<div class="max-w-screen-lg mx-auto">
<h2 class="mb-1 text-center md:text-left">Frequently Asked <span class="text-indigo-600">Questions</span></h2>
<BlogFaq :faq="page.faqs" />
</div>
</div>

<IndustryPageCta :heading="page.closingCta.heading" :description="page.closingCta.description" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<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).
//
// Lives here, not under components/industry/, because both templates use it: the
// industriesLegacy [slug].vue AND this one (industry-page/Template.vue). industry/ is
// due for deletion once the seven legacy pages go; this component has to survive that.
const props = defineProps<{ slug: string, displayName: string }>()

const { data: all } = await useAsyncData('industry-use-cases', () =>
Expand Down
2 changes: 1 addition & 1 deletion nuxt/components/use-case/Card.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
// 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
// and components/industry-use-cases.njk (now IndustryPageUseCases.vue) — one look
// across all three call sites.
withDefaults(defineProps<{
to: string
Expand Down
Loading
Loading