Compare commits
7 Commits
codex/cove
...
glary/laun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
202288db36 | ||
|
|
8ec1fa7da4 | ||
|
|
193b013c25 | ||
|
|
7f676425c2 | ||
|
|
2f394f018e | ||
|
|
46e4b5ecd1 | ||
|
|
b16b29e969 |
13
apps/website/public/drops/comfy-cli.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<text x="240" y="40" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">COMFY CLI</text>
|
||||
<rect x="60" y="70" width="360" height="160" rx="8" fill="#000000" stroke="#f2ff59" stroke-width="2" opacity="0.9"/>
|
||||
<g font-family="monospace" font-size="14" fill="#f2ff59">
|
||||
<text x="76" y="100">$ comfy launch</text>
|
||||
<text x="76" y="124" opacity="0.7">→ starting ComfyUI...</text>
|
||||
<text x="76" y="148" opacity="0.7">→ http://127.0.0.1:8188</text>
|
||||
<text x="76" y="172">$ comfy node install</text>
|
||||
<text x="76" y="196" opacity="0.7">→ installed 3 nodes ✓</text>
|
||||
<text x="76" y="220">$ _</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 821 B |
18
apps/website/public/drops/comfy-mcp.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<text x="240" y="40" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">COMFY MCP</text>
|
||||
<circle cx="240" cy="155" r="44" fill="#f2ff59"/>
|
||||
<text x="240" y="165" text-anchor="middle" font-family="sans-serif" font-size="20" font-weight="800" fill="#211927">MCP</text>
|
||||
<g stroke="#f2ff59" stroke-width="2" opacity="0.6">
|
||||
<line x1="240" y1="155" x2="120" y2="100"/>
|
||||
<line x1="240" y1="155" x2="120" y2="210"/>
|
||||
<line x1="240" y1="155" x2="360" y2="100"/>
|
||||
<line x1="240" y1="155" x2="360" y2="210"/>
|
||||
</g>
|
||||
<g fill="#f2ff59" opacity="0.4">
|
||||
<circle cx="120" cy="100" r="14"/>
|
||||
<circle cx="120" cy="210" r="14"/>
|
||||
<circle cx="360" cy="100" r="14"/>
|
||||
<circle cx="360" cy="210" r="14"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 917 B |
12
apps/website/public/drops/desktop-client.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<rect x="80" y="55" width="320" height="200" rx="12" fill="#f2ff59" opacity="0.08" stroke="#f2ff59" stroke-width="2"/>
|
||||
<rect x="80" y="55" width="320" height="28" rx="12" fill="#f2ff59" opacity="0.15"/>
|
||||
<circle cx="96" cy="69" r="4" fill="#f2ff59"/>
|
||||
<circle cx="110" cy="69" r="4" fill="#f2ff59" opacity="0.6"/>
|
||||
<circle cx="124" cy="69" r="4" fill="#f2ff59" opacity="0.4"/>
|
||||
<rect x="104" y="110" width="100" height="60" rx="8" fill="#f2ff59" opacity="0.25"/>
|
||||
<rect x="220" y="110" width="100" height="60" rx="8" fill="#f2ff59" opacity="0.4"/>
|
||||
<rect x="104" y="185" width="216" height="40" rx="8" fill="#f2ff59" opacity="0.15"/>
|
||||
<text x="240" y="42" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">DESKTOP CLIENT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 934 B |
7
apps/website/public/drops/fresh-drops.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<text x="240" y="40" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">COMFY CREATE 2026</text>
|
||||
<rect x="80" y="70" width="320" height="170" rx="12" fill="#f2ff59" opacity="0.08" stroke="#f2ff59" stroke-width="2"/>
|
||||
<polygon points="220,130 220,200 290,165" fill="#f2ff59"/>
|
||||
<circle cx="255" cy="165" r="60" fill="none" stroke="#f2ff59" stroke-width="2" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 572 B |
8
apps/website/public/drops/fresh-originals.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<text x="240" y="40" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">COMFY ORIGINALS</text>
|
||||
<rect x="120" y="80" width="240" height="160" rx="8" fill="#f2ff59" opacity="0.15" stroke="#f2ff59" stroke-width="2"/>
|
||||
<path d="M180 130 L240 90 L300 130 L300 200 L180 200 Z" fill="#f2ff59" opacity="0.3"/>
|
||||
<circle cx="240" cy="160" r="22" fill="#f2ff59"/>
|
||||
<text x="240" y="167" text-anchor="middle" font-family="sans-serif" font-size="16" font-weight="800" fill="#211927">C</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
15
apps/website/public/drops/oss-vram.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<text x="240" y="40" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">VRAM IMPROVEMENTS</text>
|
||||
<g transform="translate(80, 80)">
|
||||
<rect width="320" height="24" rx="4" fill="#f2ff59" opacity="0.1"/>
|
||||
<rect width="280" height="24" rx="4" fill="#f2ff59" opacity="0.4"/>
|
||||
<text x="160" y="17" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#211927" font-weight="700">BEFORE: 22GB / 24GB</text>
|
||||
</g>
|
||||
<g transform="translate(80, 130)">
|
||||
<rect width="320" height="24" rx="4" fill="#f2ff59" opacity="0.1"/>
|
||||
<rect width="180" height="24" rx="4" fill="#f2ff59"/>
|
||||
<text x="160" y="17" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#211927" font-weight="700">AFTER: 14GB / 24GB</text>
|
||||
</g>
|
||||
<text x="240" y="210" text-anchor="middle" font-family="monospace" font-size="32" font-weight="700" fill="#f2ff59">-36%</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
13
apps/website/public/drops/team-plans.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 270" fill="none">
|
||||
<rect width="480" height="270" fill="#211927"/>
|
||||
<text x="240" y="40" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="600" fill="#c2bfb9" letter-spacing="2">TEAM PLANS</text>
|
||||
<g transform="translate(80, 85)">
|
||||
<circle cx="40" cy="40" r="32" fill="#f2ff59" opacity="0.9"/>
|
||||
<circle cx="110" cy="40" r="32" fill="#f2ff59" opacity="0.7"/>
|
||||
<circle cx="180" cy="40" r="32" fill="#f2ff59" opacity="0.5"/>
|
||||
<circle cx="250" cy="40" r="32" fill="#f2ff59" opacity="0.3"/>
|
||||
<circle cx="320" cy="40" r="32" fill="#c2bfb9" opacity="0.2" stroke="#c2bfb9" stroke-width="2" stroke-dasharray="4 4"/>
|
||||
<text x="320" y="46" text-anchor="middle" font-family="sans-serif" font-size="20" font-weight="700" fill="#c2bfb9">+</text>
|
||||
</g>
|
||||
<text x="240" y="220" text-anchor="middle" font-family="sans-serif" font-size="18" font-weight="300" fill="#c2bfb9">Shared workspace • Pooled credits</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1009 B |
25
apps/website/src/components/drops/FooterCtaSection.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="bg-secondary-mauve/30 mt-12 border-t border-primary-comfy-canvas/10 px-6 py-24 text-center md:py-32"
|
||||
data-testid="drops-footer-cta"
|
||||
>
|
||||
<h2
|
||||
class="mx-auto max-w-5xl text-4xl/tight font-light tracking-tight uppercase md:text-6xl/tight lg:text-7xl/tight"
|
||||
>
|
||||
<span class="block text-primary-comfy-canvas">
|
||||
{{ t('drops-landing.footerCta.line1', locale) }}
|
||||
</span>
|
||||
<span class="text-primary-comfy-yellow block font-bold">
|
||||
{{ t('drops-landing.footerCta.line2', locale) }}
|
||||
</span>
|
||||
</h2>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
import { captureDropClick } from './useDropClickCapture'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
interface FreshCard {
|
||||
id: string
|
||||
titleKey: TranslationKey
|
||||
bodyKey: TranslationKey
|
||||
ctaKey: TranslationKey
|
||||
href: string
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
const cards: FreshCard[] = [
|
||||
{
|
||||
id: 'all-latest-drops',
|
||||
titleKey: 'drops-landing.fresh.card1.title',
|
||||
bodyKey: 'drops-landing.fresh.card1.body',
|
||||
ctaKey: 'drops-landing.fresh.card1.cta',
|
||||
href: externalLinks.liveDemoEvent,
|
||||
imageUrl: '/drops/fresh-drops.svg'
|
||||
},
|
||||
{
|
||||
id: 'comfy-originals',
|
||||
titleKey: 'drops-landing.fresh.card2.title',
|
||||
bodyKey: 'drops-landing.fresh.card2.body',
|
||||
ctaKey: 'drops-landing.fresh.card2.cta',
|
||||
href: externalLinks.blog,
|
||||
imageUrl: '/drops/fresh-originals.svg'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="border-t border-primary-comfy-canvas/10 px-6 py-20 md:px-20 md:py-28"
|
||||
data-testid="drops-fresh"
|
||||
>
|
||||
<div class="mx-auto max-w-6xl text-center">
|
||||
<p
|
||||
class="text-sm tracking-widest text-primary-comfy-canvas/60 uppercase md:text-base"
|
||||
>
|
||||
{{ t('drops-landing.fresh.eyebrow', locale) }}
|
||||
</p>
|
||||
<h2
|
||||
class="text-primary-comfy-yellow mt-2 text-4xl font-bold tracking-tight uppercase md:text-6xl"
|
||||
>
|
||||
{{ t('drops-landing.fresh.headingAccent', locale) }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ul class="mx-auto mt-16 grid max-w-6xl grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<li
|
||||
v-for="card in cards"
|
||||
:key="card.id"
|
||||
class="bg-transparency-white-t4 flex flex-col overflow-hidden rounded-4xl border border-primary-comfy-canvas/10"
|
||||
:data-testid="`drops-fresh-${card.id}`"
|
||||
>
|
||||
<div
|
||||
class="flex aspect-video items-center justify-center overflow-hidden bg-primary-comfy-ink/40 p-6"
|
||||
>
|
||||
<img
|
||||
:src="card.imageUrl"
|
||||
:alt="t(card.titleKey, locale)"
|
||||
class="max-h-full max-w-full object-contain"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-4 p-8">
|
||||
<h3 class="text-2xl font-light text-primary-comfy-canvas md:text-3xl">
|
||||
{{ t(card.titleKey, locale) }}
|
||||
</h3>
|
||||
<p class="text-base text-primary-comfy-canvas/70">
|
||||
{{ t(card.bodyKey, locale) }}
|
||||
</p>
|
||||
<div class="mt-auto pt-4">
|
||||
<BrandButton
|
||||
:href="card.href"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:data-testid="`drops-fresh-${card.id}-cta`"
|
||||
class="px-6 py-3 text-sm"
|
||||
@click="
|
||||
captureDropClick('fresh', {
|
||||
card_id: card.id,
|
||||
href: card.href
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ t(card.ctaKey, locale) }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
75
apps/website/src/components/drops/GetStartedSection.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
interface Step {
|
||||
number: string
|
||||
titleKey: TranslationKey
|
||||
bodyKey: TranslationKey
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
number: '1',
|
||||
titleKey: 'drops-landing.getStarted.step.1.title',
|
||||
bodyKey: 'drops-landing.getStarted.step.1.body'
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
titleKey: 'drops-landing.getStarted.step.2.title',
|
||||
bodyKey: 'drops-landing.getStarted.step.2.body'
|
||||
},
|
||||
{
|
||||
number: '3',
|
||||
titleKey: 'drops-landing.getStarted.step.3.title',
|
||||
bodyKey: 'drops-landing.getStarted.step.3.body'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="border-t border-primary-comfy-canvas/10 px-6 py-20 md:px-20 md:py-28"
|
||||
data-testid="drops-get-started"
|
||||
>
|
||||
<div class="mx-auto max-w-6xl text-center">
|
||||
<p
|
||||
class="text-sm tracking-widest text-primary-comfy-canvas/60 uppercase md:text-base"
|
||||
>
|
||||
{{ t('drops-landing.getStarted.eyebrow', locale) }}
|
||||
</p>
|
||||
<h2
|
||||
class="text-primary-comfy-yellow mt-2 text-4xl font-bold tracking-tight uppercase md:text-6xl"
|
||||
>
|
||||
{{ t('drops-landing.getStarted.headingAccent', locale) }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ol
|
||||
class="mx-auto mt-16 grid max-w-6xl grid-cols-1 gap-6 md:grid-cols-3 md:gap-8"
|
||||
>
|
||||
<li
|
||||
v-for="step in steps"
|
||||
:key="step.number"
|
||||
class="bg-transparency-white-t4 flex flex-col gap-4 rounded-4xl border border-primary-comfy-canvas/10 p-8"
|
||||
:data-testid="`drops-step-${step.number}`"
|
||||
>
|
||||
<span
|
||||
class="text-primary-comfy-yellow font-mono text-5xl font-light md:text-6xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ step.number }}
|
||||
</span>
|
||||
<h3 class="text-2xl font-light text-primary-comfy-canvas">
|
||||
{{ t(step.titleKey, locale) }}
|
||||
</h3>
|
||||
<p class="text-base text-primary-comfy-canvas/70">
|
||||
{{ t(step.bodyKey, locale) }}
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</template>
|
||||
82
apps/website/src/components/drops/HeroSection.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { externalLinks, getRoutes } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
import { captureDropClick } from './useDropClickCapture'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const routes = getRoutes(locale)
|
||||
const dropsUrl = `comfy.org${routes.drops}`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="max-w-9xl mx-auto px-6 pt-16 pb-12 text-center lg:px-20 lg:pt-28 lg:pb-20"
|
||||
data-testid="drops-hero"
|
||||
>
|
||||
<h1
|
||||
class="mx-auto max-w-5xl text-5xl/none font-light tracking-tight text-primary-comfy-canvas uppercase md:text-7xl/none lg:text-8xl/none"
|
||||
>
|
||||
<span class="block">{{ t('drops-landing.hero.eyebrow', locale) }}</span>
|
||||
<span class="text-primary-comfy-yellow block font-bold">
|
||||
{{ t('drops-landing.hero.headingAccent', locale) }}
|
||||
</span>
|
||||
</h1>
|
||||
<p
|
||||
class="mx-auto mt-10 max-w-2xl text-lg text-primary-comfy-canvas md:text-xl"
|
||||
>
|
||||
{{ t('drops-landing.hero.body1', locale) }}
|
||||
</p>
|
||||
<p
|
||||
class="mx-auto mt-4 max-w-2xl text-base text-primary-comfy-canvas/80 md:text-lg"
|
||||
>
|
||||
{{ t('drops-landing.hero.body2', locale) }}
|
||||
</p>
|
||||
<div
|
||||
class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
|
||||
>
|
||||
<BrandButton
|
||||
:href="externalLinks.liveDemoEvent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="lg"
|
||||
data-testid="drops-hero-primary-cta"
|
||||
class="px-8 py-4 text-base"
|
||||
@click="
|
||||
captureDropClick('hero', {
|
||||
cta: 'explore_whats_new',
|
||||
href: externalLinks.liveDemoEvent
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ t('drops-landing.hero.primaryCta', locale) }}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
:href="externalLinks.workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
data-testid="drops-hero-secondary-cta"
|
||||
class="px-8 py-4 text-base"
|
||||
@click="
|
||||
captureDropClick('hero', {
|
||||
cta: 'see_all_workflows',
|
||||
href: externalLinks.workflows
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ t('drops-landing.hero.secondaryCta', locale) }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
<a
|
||||
:href="routes.drops"
|
||||
class="mt-8 inline-block text-xs tracking-widest text-primary-comfy-canvas/40 uppercase"
|
||||
>
|
||||
{{ dropsUrl }}
|
||||
</a>
|
||||
</section>
|
||||
</template>
|
||||
102
apps/website/src/components/drops/MoreDropsSection.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import type { DropCta, DropItem } from './dropItems'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { getRoutes } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
import { dropItems, resolveCtaHref } from './dropItems'
|
||||
import { captureDropClick } from './useDropClickCapture'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const routes = getRoutes(locale)
|
||||
|
||||
function hrefFor(cta: DropCta): string {
|
||||
return resolveCtaHref(cta, routes)
|
||||
}
|
||||
|
||||
function handleCtaClick(item: DropItem, cta: DropCta) {
|
||||
captureDropClick('more_drops', {
|
||||
drop_id: item.id,
|
||||
cta: cta.labelKey,
|
||||
href: hrefFor(cta),
|
||||
external: cta.external
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
id="more-drops"
|
||||
class="border-t border-primary-comfy-canvas/10 px-6 py-20 md:px-20 md:py-28"
|
||||
data-testid="drops-more-drops"
|
||||
>
|
||||
<div class="mx-auto max-w-6xl text-center">
|
||||
<p
|
||||
class="text-sm tracking-widest text-primary-comfy-canvas/60 uppercase md:text-base"
|
||||
>
|
||||
{{ t('drops-landing.moreDrops.eyebrow', locale) }}
|
||||
</p>
|
||||
<h2
|
||||
class="text-primary-comfy-yellow mt-2 text-4xl font-bold tracking-tight uppercase md:text-6xl"
|
||||
>
|
||||
{{ t('drops-landing.moreDrops.headingAccent', locale) }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ul class="mx-auto mt-16 flex max-w-6xl flex-col gap-8 md:gap-12">
|
||||
<li
|
||||
v-for="(item, index) in dropItems"
|
||||
:key="item.id"
|
||||
class="bg-transparency-white-t4 grid grid-cols-1 gap-8 overflow-hidden rounded-4xl border border-primary-comfy-canvas/10 md:gap-12 lg:grid-cols-5"
|
||||
:data-testid="`drops-item-${item.id}`"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex aspect-video items-center justify-center overflow-hidden bg-primary-comfy-ink/40 p-8 lg:col-span-2 lg:aspect-auto',
|
||||
index % 2 === 1 && 'lg:order-2'
|
||||
)
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="item.imageUrl"
|
||||
:alt="t(item.titleKey, locale)"
|
||||
class="max-h-72 max-w-full object-contain"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col justify-center gap-5 p-8 md:p-10 lg:col-span-3"
|
||||
>
|
||||
<h3 class="text-3xl font-light text-primary-comfy-canvas md:text-4xl">
|
||||
{{ t(item.titleKey, locale) }}
|
||||
</h3>
|
||||
<p class="text-primary-comfy-yellow text-lg font-light md:text-xl">
|
||||
{{ t(item.taglineKey, locale) }}
|
||||
</p>
|
||||
<p class="text-base text-primary-comfy-canvas/70">
|
||||
{{ t(item.bodyKey, locale) }}
|
||||
</p>
|
||||
<div v-if="item.cta" class="mt-2">
|
||||
<BrandButton
|
||||
:href="hrefFor(item.cta)"
|
||||
:target="item.cta.external ? '_blank' : undefined"
|
||||
:rel="item.cta.external ? 'noopener noreferrer' : undefined"
|
||||
size="md"
|
||||
:data-testid="`drops-item-${item.id}-cta`"
|
||||
class="px-6 py-3 text-sm"
|
||||
@click="handleCtaClick(item, item.cta)"
|
||||
>
|
||||
{{ t(item.cta.labelKey, locale) }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
83
apps/website/src/components/drops/dropItems.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { getRoutes } from '../../config/routes'
|
||||
import type { TranslationKey } from '../../i18n/translations'
|
||||
|
||||
type Routes = ReturnType<typeof getRoutes>
|
||||
|
||||
interface InternalCta {
|
||||
labelKey: TranslationKey
|
||||
external: false
|
||||
routeKey: keyof Routes
|
||||
}
|
||||
|
||||
interface ExternalCta {
|
||||
labelKey: TranslationKey
|
||||
external: true
|
||||
url: string
|
||||
}
|
||||
|
||||
export type DropCta = InternalCta | ExternalCta
|
||||
|
||||
export interface DropItem {
|
||||
id: string
|
||||
titleKey: TranslationKey
|
||||
taglineKey: TranslationKey
|
||||
bodyKey: TranslationKey
|
||||
imageUrl: string
|
||||
cta?: DropCta
|
||||
}
|
||||
|
||||
const REGISTER_FOR_LIVE_DEMO_CTA: ExternalCta = {
|
||||
labelKey: 'drops-landing.item.register-for-live-demo.cta',
|
||||
external: true,
|
||||
url: 'https://luma.com/l7c5z4gp?tk=nm3dWZ'
|
||||
}
|
||||
|
||||
export const dropItems: DropItem[] = [
|
||||
{
|
||||
id: 'desktop-client',
|
||||
titleKey: 'drops-landing.item.desktop-client.title',
|
||||
taglineKey: 'drops-landing.item.desktop-client.tagline',
|
||||
bodyKey: 'drops-landing.item.desktop-client.body',
|
||||
imageUrl: '/drops/desktop-client.svg',
|
||||
cta: {
|
||||
labelKey: 'drops-landing.item.desktop-client.cta',
|
||||
external: false,
|
||||
routeKey: 'download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'oss-vram',
|
||||
titleKey: 'drops-landing.item.oss-vram.title',
|
||||
taglineKey: 'drops-landing.item.oss-vram.tagline',
|
||||
bodyKey: 'drops-landing.item.oss-vram.body',
|
||||
imageUrl: '/drops/oss-vram.svg'
|
||||
},
|
||||
{
|
||||
id: 'comfy-mcp',
|
||||
titleKey: 'drops-landing.item.comfy-mcp.title',
|
||||
taglineKey: 'drops-landing.item.comfy-mcp.tagline',
|
||||
bodyKey: 'drops-landing.item.comfy-mcp.body',
|
||||
imageUrl: '/drops/comfy-mcp.svg',
|
||||
cta: REGISTER_FOR_LIVE_DEMO_CTA
|
||||
},
|
||||
{
|
||||
id: 'comfy-cli',
|
||||
titleKey: 'drops-landing.item.comfy-cli.title',
|
||||
taglineKey: 'drops-landing.item.comfy-cli.tagline',
|
||||
bodyKey: 'drops-landing.item.comfy-cli.body',
|
||||
imageUrl: '/drops/comfy-cli.svg',
|
||||
cta: REGISTER_FOR_LIVE_DEMO_CTA
|
||||
},
|
||||
{
|
||||
id: 'team-plans',
|
||||
titleKey: 'drops-landing.item.team-plans.title',
|
||||
taglineKey: 'drops-landing.item.team-plans.tagline',
|
||||
bodyKey: 'drops-landing.item.team-plans.body',
|
||||
imageUrl: '/drops/team-plans.svg',
|
||||
cta: REGISTER_FOR_LIVE_DEMO_CTA
|
||||
}
|
||||
]
|
||||
|
||||
export function resolveCtaHref(cta: DropCta, routes: Routes): string {
|
||||
return cta.external ? cta.url : routes[cta.routeKey]
|
||||
}
|
||||
157
apps/website/src/components/drops/dropsLanding.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { TranslationKey } from '../../i18n/translations'
|
||||
|
||||
import { getRoutes } from '../../config/routes'
|
||||
import { hasKey, t, translationKeys } from '../../i18n/translations'
|
||||
import { dropItems, resolveCtaHref } from './dropItems'
|
||||
|
||||
const PREFIX = 'drops-landing'
|
||||
|
||||
const EXPECTED_SECTION_PREFIXES = [
|
||||
'page',
|
||||
'hero',
|
||||
'moreDrops',
|
||||
'item',
|
||||
'getStarted',
|
||||
'fresh',
|
||||
'footerCta'
|
||||
] as const
|
||||
|
||||
const STEP_COUNT = 3
|
||||
const FRESH_CARD_COUNT = 2
|
||||
|
||||
const INTERNAL_KEY_PATTERNS = [
|
||||
/open-questions/,
|
||||
/todo/i,
|
||||
/draft/i,
|
||||
/placeholder/i,
|
||||
/internal/i
|
||||
]
|
||||
|
||||
function dropsKeys(): string[] {
|
||||
return translationKeys.filter((k) => k.startsWith(`${PREFIX}.`))
|
||||
}
|
||||
|
||||
describe('drops landing i18n', () => {
|
||||
it('exposes the canonical top-level section prefixes', () => {
|
||||
const keys = dropsKeys()
|
||||
for (const section of EXPECTED_SECTION_PREFIXES) {
|
||||
const hit = keys.some((k) => k.startsWith(`${PREFIX}.${section}.`))
|
||||
expect(hit, `missing section: ${section}`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('exposes page, hero, and section headings', () => {
|
||||
expect(hasKey(`${PREFIX}.page.title`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.page.description`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.hero.eyebrow`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.hero.headingAccent`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.hero.body1`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.hero.body2`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.hero.primaryCta`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.hero.secondaryCta`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.moreDrops.eyebrow`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.moreDrops.headingAccent`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.getStarted.eyebrow`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.getStarted.headingAccent`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.fresh.eyebrow`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.fresh.headingAccent`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.footerCta.line1`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.footerCta.line2`)).toBe(true)
|
||||
})
|
||||
|
||||
it('matches every drop item to title, tagline, and body translation keys', () => {
|
||||
for (const item of dropItems) {
|
||||
expect(hasKey(item.titleKey)).toBe(true)
|
||||
expect(hasKey(item.taglineKey)).toBe(true)
|
||||
expect(hasKey(item.bodyKey)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('matches every CTA label to a translation key', () => {
|
||||
for (const item of dropItems) {
|
||||
if (!item.cta) continue
|
||||
expect(hasKey(item.cta.labelKey)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('exposes the get-started step copy', () => {
|
||||
for (let n = 1; n <= STEP_COUNT; n++) {
|
||||
expect(hasKey(`${PREFIX}.getStarted.step.${n}.title`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.getStarted.step.${n}.body`)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('exposes the fresh-from-comfy-create card copy', () => {
|
||||
for (let n = 1; n <= FRESH_CARD_COUNT; n++) {
|
||||
expect(hasKey(`${PREFIX}.fresh.card${n}.title`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.fresh.card${n}.body`)).toBe(true)
|
||||
expect(hasKey(`${PREFIX}.fresh.card${n}.cta`)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('returns non-empty english copy for every drops-landing key', () => {
|
||||
for (const key of dropsKeys()) {
|
||||
expect(t(key as TranslationKey, 'en').trim().length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('does not leak internal-only keys (drafts, todos, open questions)', () => {
|
||||
const leaks = dropsKeys().filter((k) =>
|
||||
INTERNAL_KEY_PATTERNS.some((re) => re.test(k))
|
||||
)
|
||||
expect(leaks).toEqual([])
|
||||
})
|
||||
|
||||
it('uses unique drop item ids', () => {
|
||||
const ids = dropItems.map((item) => item.id)
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
})
|
||||
|
||||
it('has a non-empty image url and resolvable href for every drop item with a CTA', () => {
|
||||
const routes = getRoutes('en')
|
||||
for (const item of dropItems) {
|
||||
expect(item.imageUrl.length).toBeGreaterThan(0)
|
||||
if (!item.cta) continue
|
||||
expect(resolveCtaHref(item.cta, routes).length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves internal CTA hrefs through the locale-aware route helper', () => {
|
||||
const internalCtas = dropItems
|
||||
.map((item) => item.cta)
|
||||
.filter(
|
||||
(cta): cta is NonNullable<typeof cta> & { external: false } =>
|
||||
cta !== undefined && !cta.external
|
||||
)
|
||||
expect(internalCtas.length).toBeGreaterThan(0)
|
||||
const enRoutes = getRoutes('en')
|
||||
const zhRoutes = getRoutes('zh-CN')
|
||||
for (const cta of internalCtas) {
|
||||
expect(resolveCtaHref(cta, enRoutes)).toBe(enRoutes[cta.routeKey])
|
||||
expect(resolveCtaHref(cta, zhRoutes)).toBe(zhRoutes[cta.routeKey])
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves external CTA hrefs regardless of locale', () => {
|
||||
const externalCtas = dropItems
|
||||
.map((item) => item.cta)
|
||||
.filter(
|
||||
(cta): cta is NonNullable<typeof cta> & { external: true } =>
|
||||
cta !== undefined && cta.external
|
||||
)
|
||||
expect(externalCtas.length).toBeGreaterThan(0)
|
||||
const enRoutes = getRoutes('en')
|
||||
const zhRoutes = getRoutes('zh-CN')
|
||||
for (const cta of externalCtas) {
|
||||
expect(resolveCtaHref(cta, enRoutes)).toBe(cta.url)
|
||||
expect(resolveCtaHref(cta, zhRoutes)).toBe(cta.url)
|
||||
}
|
||||
})
|
||||
|
||||
it('omits the CTA button for items without a CTA (e.g. oss-vram pending blog post)', () => {
|
||||
const itemsWithoutCta = dropItems.filter((item) => !item.cta)
|
||||
expect(itemsWithoutCta.map((item) => item.id)).toContain('oss-vram')
|
||||
})
|
||||
})
|
||||
12
apps/website/src/components/drops/useDropClickCapture.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
export function captureDropClick(
|
||||
location: string,
|
||||
properties: Record<string, unknown> = {}
|
||||
): void {
|
||||
try {
|
||||
posthog.capture('drops_cta_clicked', { ...properties, location })
|
||||
} catch (error) {
|
||||
console.error('PostHog capture failed', error)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const baseRoutes = {
|
||||
careers: '/careers',
|
||||
customers: '/customers',
|
||||
demos: '/demos',
|
||||
drops: '/drops',
|
||||
termsOfService: '/terms-of-service',
|
||||
privacyPolicy: '/privacy-policy',
|
||||
affiliates: '/affiliates',
|
||||
@@ -35,6 +36,7 @@ type Routes = typeof baseRoutes
|
||||
const LOCALE_INVARIANT_ROUTE_KEYS = new Set<keyof Routes>([
|
||||
'affiliates',
|
||||
'affiliateTerms',
|
||||
'drops',
|
||||
'termsOfService'
|
||||
])
|
||||
|
||||
@@ -61,6 +63,7 @@ export const externalLinks = {
|
||||
docsSubscription: 'https://docs.comfy.org/support/subscription/subscribing',
|
||||
github: 'https://github.com/Comfy-Org/ComfyUI',
|
||||
githubInstall: 'https://github.com/Comfy-Org/ComfyUI#installing',
|
||||
liveDemoEvent: 'https://luma.com/l7c5z4gp?tk=nm3dWZ',
|
||||
platform: 'https://platform.comfy.org',
|
||||
platformUsage: 'https://platform.comfy.org/profile/usage',
|
||||
support: 'https://support.comfy.org/hc/en-us',
|
||||
|
||||
@@ -4731,6 +4731,210 @@ const translations = {
|
||||
'affiliate-landing.footerCta.termsLink': {
|
||||
en: 'Read the affiliate program terms',
|
||||
'zh-CN': 'Read the affiliate program terms'
|
||||
},
|
||||
|
||||
// Drops landing page (/drops)
|
||||
// English-only copy; zh-CN values mirror en until a localized version lands.
|
||||
'drops-landing.page.title': {
|
||||
en: 'Discover Our Latest Drops — Comfy',
|
||||
'zh-CN': 'Discover Our Latest Drops — Comfy'
|
||||
},
|
||||
'drops-landing.page.description': {
|
||||
en: 'Every model, every feature, every update — visible, inspectable, and ready to run. Build with the latest. Ship faster than any one company could alone.',
|
||||
'zh-CN':
|
||||
'Every model, every feature, every update — visible, inspectable, and ready to run. Build with the latest. Ship faster than any one company could alone.'
|
||||
},
|
||||
|
||||
// Hero
|
||||
'drops-landing.hero.eyebrow': {
|
||||
en: "WHAT'S",
|
||||
'zh-CN': "WHAT'S"
|
||||
},
|
||||
'drops-landing.hero.headingAccent': {
|
||||
en: 'NEW',
|
||||
'zh-CN': 'NEW'
|
||||
},
|
||||
'drops-landing.hero.body1': {
|
||||
en: 'Every model, every feature, every update — visible, inspectable, and ready to run.',
|
||||
'zh-CN':
|
||||
'Every model, every feature, every update — visible, inspectable, and ready to run.'
|
||||
},
|
||||
'drops-landing.hero.body2': {
|
||||
en: 'Build with the latest. Ship faster than any one company could alone.',
|
||||
'zh-CN':
|
||||
'Build with the latest. Ship faster than any one company could alone.'
|
||||
},
|
||||
'drops-landing.hero.primaryCta': {
|
||||
en: "EXPLORE WHAT'S NEW",
|
||||
'zh-CN': "EXPLORE WHAT'S NEW"
|
||||
},
|
||||
'drops-landing.hero.secondaryCta': {
|
||||
en: 'SEE ALL WORKFLOWS',
|
||||
'zh-CN': 'SEE ALL WORKFLOWS'
|
||||
},
|
||||
|
||||
// More Drops section
|
||||
'drops-landing.moreDrops.eyebrow': {
|
||||
en: 'MORE DROPS FROM',
|
||||
'zh-CN': 'MORE DROPS FROM'
|
||||
},
|
||||
'drops-landing.moreDrops.headingAccent': {
|
||||
en: 'COMFY CREATE',
|
||||
'zh-CN': 'COMFY CREATE'
|
||||
},
|
||||
|
||||
// Drop items (5 of them)
|
||||
'drops-landing.item.desktop-client.title': {
|
||||
en: 'New Desktop Client',
|
||||
'zh-CN': 'New Desktop Client'
|
||||
},
|
||||
'drops-landing.item.desktop-client.tagline': {
|
||||
en: 'A native desktop experience built for the way you actually work.',
|
||||
'zh-CN': 'A native desktop experience built for the way you actually work.'
|
||||
},
|
||||
'drops-landing.item.desktop-client.body': {
|
||||
en: 'Every node, every model — faster, tighter, and always local.',
|
||||
'zh-CN': 'Every node, every model — faster, tighter, and always local.'
|
||||
},
|
||||
'drops-landing.item.desktop-client.cta': {
|
||||
en: 'TRY THE DESKTOP CLIENT',
|
||||
'zh-CN': 'TRY THE DESKTOP CLIENT'
|
||||
},
|
||||
'drops-landing.item.oss-vram.title': {
|
||||
en: 'OSS VRAM Improvements',
|
||||
'zh-CN': 'OSS VRAM Improvements'
|
||||
},
|
||||
'drops-landing.item.oss-vram.tagline': {
|
||||
en: 'Push more through your hardware.',
|
||||
'zh-CN': 'Push more through your hardware.'
|
||||
},
|
||||
'drops-landing.item.oss-vram.body': {
|
||||
en: 'Significant open-source VRAM optimizations so you can run larger models, deeper pipelines, and more complex workflows — without hitting the ceiling.',
|
||||
'zh-CN':
|
||||
'Significant open-source VRAM optimizations so you can run larger models, deeper pipelines, and more complex workflows — without hitting the ceiling.'
|
||||
},
|
||||
'drops-landing.item.comfy-mcp.title': {
|
||||
en: 'Comfy MCP',
|
||||
'zh-CN': 'Comfy MCP'
|
||||
},
|
||||
'drops-landing.item.comfy-mcp.tagline': {
|
||||
en: 'Connect Comfy to any tool that speaks MCP.',
|
||||
'zh-CN': 'Connect Comfy to any tool that speaks MCP.'
|
||||
},
|
||||
'drops-landing.item.comfy-mcp.body': {
|
||||
en: 'Build pipelines that talk directly to your stack. Every connection is a node. Every parameter is yours to control.',
|
||||
'zh-CN':
|
||||
'Build pipelines that talk directly to your stack. Every connection is a node. Every parameter is yours to control.'
|
||||
},
|
||||
'drops-landing.item.comfy-cli.title': {
|
||||
en: 'Comfy CLI',
|
||||
'zh-CN': 'Comfy CLI'
|
||||
},
|
||||
'drops-landing.item.comfy-cli.tagline': {
|
||||
en: 'Run, manage, and automate your workflows from the terminal.',
|
||||
'zh-CN': 'Run, manage, and automate your workflows from the terminal.'
|
||||
},
|
||||
'drops-landing.item.comfy-cli.body': {
|
||||
en: 'Full pipeline control without the GUI. Scriptable, composable, and built for teams who move fast.',
|
||||
'zh-CN':
|
||||
'Full pipeline control without the GUI. Scriptable, composable, and built for teams who move fast.'
|
||||
},
|
||||
'drops-landing.item.team-plans.title': {
|
||||
en: 'Comfy Team Plans',
|
||||
'zh-CN': 'Comfy Team Plans'
|
||||
},
|
||||
'drops-landing.item.team-plans.tagline': {
|
||||
en: 'Professional-grade infrastructure for teams who build together.',
|
||||
'zh-CN': 'Professional-grade infrastructure for teams who build together.'
|
||||
},
|
||||
'drops-landing.item.team-plans.body': {
|
||||
en: 'Shared workflows, collaborative node graphs, and enterprise-ready controls — all in one place.',
|
||||
'zh-CN':
|
||||
'Shared workflows, collaborative node graphs, and enterprise-ready controls — all in one place.'
|
||||
},
|
||||
'drops-landing.item.register-for-live-demo.cta': {
|
||||
en: 'REGISTER FOR THE LIVE DEMO',
|
||||
'zh-CN': 'REGISTER FOR THE LIVE DEMO'
|
||||
},
|
||||
|
||||
// Get Started in 3 Steps section
|
||||
'drops-landing.getStarted.eyebrow': {
|
||||
en: 'GET STARTED IN',
|
||||
'zh-CN': 'GET STARTED IN'
|
||||
},
|
||||
'drops-landing.getStarted.headingAccent': {
|
||||
en: '3 STEPS',
|
||||
'zh-CN': '3 STEPS'
|
||||
},
|
||||
'drops-landing.getStarted.step.1.title': {
|
||||
en: 'Download or Sign up',
|
||||
'zh-CN': 'Download or Sign up'
|
||||
},
|
||||
'drops-landing.getStarted.step.1.body': {
|
||||
en: 'Download Local or launch Cloud.',
|
||||
'zh-CN': 'Download Local or launch Cloud.'
|
||||
},
|
||||
'drops-landing.getStarted.step.2.title': {
|
||||
en: 'Load a workflow',
|
||||
'zh-CN': 'Load a workflow'
|
||||
},
|
||||
'drops-landing.getStarted.step.2.body': {
|
||||
en: 'Start from a community template or build your own from scratch.',
|
||||
'zh-CN': 'Start from a community template or build your own from scratch.'
|
||||
},
|
||||
'drops-landing.getStarted.step.3.title': {
|
||||
en: 'Generate & Iterate',
|
||||
'zh-CN': 'Generate & Iterate'
|
||||
},
|
||||
'drops-landing.getStarted.step.3.body': {
|
||||
en: "Run, adjust, and scale when you're ready.",
|
||||
'zh-CN': "Run, adjust, and scale when you're ready."
|
||||
},
|
||||
|
||||
// Fresh From Comfy Create section
|
||||
'drops-landing.fresh.eyebrow': {
|
||||
en: 'FRESH FROM',
|
||||
'zh-CN': 'FRESH FROM'
|
||||
},
|
||||
'drops-landing.fresh.headingAccent': {
|
||||
en: 'COMFY CREATE',
|
||||
'zh-CN': 'COMFY CREATE'
|
||||
},
|
||||
'drops-landing.fresh.card1.title': {
|
||||
en: 'All the latest drops',
|
||||
'zh-CN': 'All the latest drops'
|
||||
},
|
||||
'drops-landing.fresh.card1.body': {
|
||||
en: 'Catch up on every new node, model, and feature from Comfy Create 2026.',
|
||||
'zh-CN':
|
||||
'Catch up on every new node, model, and feature from Comfy Create 2026.'
|
||||
},
|
||||
'drops-landing.fresh.card1.cta': {
|
||||
en: 'WATCH THE DROPS',
|
||||
'zh-CN': 'WATCH THE DROPS'
|
||||
},
|
||||
'drops-landing.fresh.card2.title': {
|
||||
en: 'From Comfy, to your world',
|
||||
'zh-CN': 'From Comfy, to your world'
|
||||
},
|
||||
'drops-landing.fresh.card2.body': {
|
||||
en: 'The workflows you love — now wearable, collectible, and designed for everyday inspiration.',
|
||||
'zh-CN':
|
||||
'The workflows you love — now wearable, collectible, and designed for everyday inspiration.'
|
||||
},
|
||||
'drops-landing.fresh.card2.cta': {
|
||||
en: 'SHOP COMFY ORIGINALS',
|
||||
'zh-CN': 'SHOP COMFY ORIGINALS'
|
||||
},
|
||||
|
||||
// Footer CTA
|
||||
'drops-landing.footerCta.line1': {
|
||||
en: 'THE MOST POWERFUL',
|
||||
'zh-CN': 'THE MOST POWERFUL'
|
||||
},
|
||||
'drops-landing.footerCta.line2': {
|
||||
en: 'WORKFLOW ENGINE FOR VISUAL AI.',
|
||||
'zh-CN': 'WORKFLOW ENGINE FOR VISUAL AI.'
|
||||
}
|
||||
} as const satisfies Record<string, Record<Locale, string>>
|
||||
|
||||
|
||||
23
apps/website/src/pages/drops/index.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import FooterCtaSection from '../../components/drops/FooterCtaSection.vue'
|
||||
import FreshFromComfyCreateSection from '../../components/drops/FreshFromComfyCreateSection.vue'
|
||||
import GetStartedSection from '../../components/drops/GetStartedSection.vue'
|
||||
import HeroSection from '../../components/drops/HeroSection.vue'
|
||||
import MoreDropsSection from '../../components/drops/MoreDropsSection.vue'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const locale: Locale = Astro.currentLocale === 'zh-CN' ? 'zh-CN' : 'en'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('drops-landing.page.title', locale)}
|
||||
description={t('drops-landing.page.description', locale)}
|
||||
>
|
||||
<HeroSection locale={locale} client:load />
|
||||
<MoreDropsSection locale={locale} client:load />
|
||||
<GetStartedSection locale={locale} />
|
||||
<FreshFromComfyCreateSection locale={locale} client:load />
|
||||
<FooterCtaSection locale={locale} />
|
||||
</BaseLayout>
|
||||