mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-19 21:09:26 +00:00
Compare commits
4 Commits
jaeone/fix
...
feature/dr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d34d59a7b2 | ||
|
|
3e1039c624 | ||
|
|
eb83aa5253 | ||
|
|
90618c2c5a |
112
apps/website/e2e/drops.spec.ts
Normal file
112
apps/website/e2e/drops.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { externalLinks } from '../src/config/routes'
|
||||
import type { Locale } from '../src/i18n/translations'
|
||||
import { t } from '../src/i18n/translations'
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const PATH_EN = '/drops'
|
||||
const PATH_ZH = '/zh-CN/drops'
|
||||
const CLOUD_URL = 'https://cloud.comfy.org'
|
||||
|
||||
function heroSection(page: Page, locale: Locale) {
|
||||
return page.locator('section').filter({
|
||||
has: page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('drops.hero.title', locale)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Drops landing — desktop @smoke', () => {
|
||||
test('renders the configured title at /drops', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(page).toHaveTitle(t('drops.page.title', 'en'))
|
||||
})
|
||||
|
||||
test('renders the localized title at /zh-CN/drops', async ({ page }) => {
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(page).toHaveTitle(t('drops.page.title', 'zh-CN'))
|
||||
})
|
||||
|
||||
test('is indexable at both locales', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('hero h1 renders the localized title in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('drops.hero.title', 'en')
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('drops.hero.title', 'zh-CN')
|
||||
})
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('hero primary CTA links to /download per locale', async ({ page }) => {
|
||||
for (const [path, locale, expectedHref] of [
|
||||
[PATH_EN, 'en', '/download'],
|
||||
[PATH_ZH, 'zh-CN', '/zh-CN/download']
|
||||
] as const) {
|
||||
await page.goto(path)
|
||||
const primary = heroSection(page, locale).getByRole('link', {
|
||||
name: t('drops.hero.primary', locale)
|
||||
})
|
||||
await expect(primary).toBeVisible()
|
||||
await expect(primary).toHaveAttribute('href', expectedHref)
|
||||
}
|
||||
})
|
||||
|
||||
test('hero secondary CTA opens external Cloud in a new tab on both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of [
|
||||
[PATH_EN, 'en'],
|
||||
[PATH_ZH, 'zh-CN']
|
||||
] as const) {
|
||||
await page.goto(path)
|
||||
const secondary = heroSection(page, locale).getByRole('link', {
|
||||
name: t('drops.hero.secondary', locale)
|
||||
})
|
||||
await expect(secondary).toBeVisible()
|
||||
await expect(secondary).toHaveAttribute('href', CLOUD_URL)
|
||||
await expect(secondary).toHaveAttribute('target', '_blank')
|
||||
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
|
||||
test('subscribe banner shows text and a sign-up link in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of [
|
||||
[PATH_EN, 'en'],
|
||||
[PATH_ZH, 'zh-CN']
|
||||
] as const) {
|
||||
await page.goto(path)
|
||||
await expect(page.getByText(t('drops.banner.text', locale))).toBeVisible()
|
||||
|
||||
const signUp = page.getByRole('link', {
|
||||
name: t('drops.banner.cta', locale)
|
||||
})
|
||||
await expect(signUp).toBeVisible()
|
||||
await expect(signUp).toHaveAttribute('href', externalLinks.youtube)
|
||||
await expect(signUp).toHaveAttribute('target', '_blank')
|
||||
await expect(signUp).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
})
|
||||
95
apps/website/src/components/blocks/HeroCenter01.vue
Normal file
95
apps/website/src/components/blocks/HeroCenter01.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnchorHTMLAttributes } from 'vue'
|
||||
|
||||
import Button from '../ui/button/Button.vue'
|
||||
|
||||
type Cta = {
|
||||
label: string
|
||||
href: string
|
||||
target?: AnchorHTMLAttributes['target']
|
||||
rel?: AnchorHTMLAttributes['rel']
|
||||
}
|
||||
|
||||
type Visual = {
|
||||
src: string
|
||||
alt: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
const { visual, eyebrow, title, subtitle, primaryCta, secondaryCta } =
|
||||
defineProps<{
|
||||
visual?: Visual
|
||||
eyebrow?: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
primaryCta: Cta
|
||||
secondaryCta?: Cta
|
||||
}>()
|
||||
|
||||
function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
|
||||
return (
|
||||
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
|
||||
>
|
||||
<img
|
||||
v-if="visual"
|
||||
:src="visual.src"
|
||||
:alt="visual.alt"
|
||||
:width="visual.width"
|
||||
:height="visual.height"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-lg"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-if="eyebrow"
|
||||
class="mb-4 text-sm font-medium tracking-wide text-primary-comfy-canvas/70 uppercase"
|
||||
>
|
||||
{{ eyebrow }}
|
||||
</p>
|
||||
|
||||
<h1
|
||||
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="mt-6 max-w-2xl text-base text-primary-comfy-canvas/70 lg:text-lg"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
|
||||
<Button
|
||||
as="a"
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="resolveRel(primaryCta)"
|
||||
size="lg"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="secondaryCta"
|
||||
as="a"
|
||||
:href="secondaryCta.href"
|
||||
:target="secondaryCta.target"
|
||||
:rel="resolveRel(secondaryCta)"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{{ secondaryCta.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ const baseRoutes = {
|
||||
cloudEnterprise: '/cloud/enterprise',
|
||||
api: '/api',
|
||||
gallery: '/gallery',
|
||||
drops: '/drops',
|
||||
about: '/about',
|
||||
careers: '/careers',
|
||||
customers: '/customers',
|
||||
|
||||
@@ -4928,6 +4928,48 @@ const translations = {
|
||||
'affiliate.cta.termsLabel': {
|
||||
en: 'Read the affiliate program terms',
|
||||
'zh-CN': '阅读联盟计划条款'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — head metadata
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.page.title': {
|
||||
en: 'Drops — Everything new in ComfyUI',
|
||||
'zh-CN': 'Drops — ComfyUI 最新动态'
|
||||
},
|
||||
'drops.page.description': {
|
||||
en: 'Explore everything new in Comfy — releases, features, models, and resources across platform, cloud, community, and developer tools.',
|
||||
'zh-CN':
|
||||
'探索 Comfy 的最新动态 — 涵盖平台、云端、社区和开发者工具的发布、功能、模型和资源。'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — hero section
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.hero.title': {
|
||||
en: 'Everything new in ComfyUI',
|
||||
'zh-CN': 'ComfyUI 全新内容'
|
||||
},
|
||||
'drops.hero.primary': {
|
||||
en: 'Download Desktop',
|
||||
'zh-CN': '下载桌面版'
|
||||
},
|
||||
'drops.hero.secondary': {
|
||||
en: 'Launch Cloud',
|
||||
'zh-CN': '启动云端'
|
||||
},
|
||||
'drops.hero.visualAlt': {
|
||||
en: 'Comfy',
|
||||
'zh-CN': 'Comfy'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — subscribe banner
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.banner.text': {
|
||||
en: 'Subscribe to the live stream and get your questions answered in real time.',
|
||||
'zh-CN': '订阅直播,实时获得您的问题解答。'
|
||||
},
|
||||
'drops.banner.cta': {
|
||||
en: 'Sign up now',
|
||||
'zh-CN': '立即注册'
|
||||
}
|
||||
} as const satisfies Record<string, Record<Locale, string>>
|
||||
|
||||
|
||||
16
apps/website/src/pages/drops.astro
Normal file
16
apps/website/src/pages/drops.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../templates/drops/HeroSection.vue'
|
||||
import SubscribeBanner from '../templates/drops/SubscribeBanner.vue'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
const locale = 'en' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('drops.page.title', locale)}
|
||||
description={t('drops.page.description', locale)}
|
||||
>
|
||||
<SubscribeBanner locale={locale} />
|
||||
<HeroSection locale={locale} />
|
||||
</BaseLayout>
|
||||
16
apps/website/src/pages/zh-CN/drops.astro
Normal file
16
apps/website/src/pages/zh-CN/drops.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../../templates/drops/HeroSection.vue'
|
||||
import SubscribeBanner from '../../templates/drops/SubscribeBanner.vue'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const locale = 'zh-CN' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('drops.page.title', locale)}
|
||||
description={t('drops.page.description', locale)}
|
||||
>
|
||||
<SubscribeBanner locale={locale} />
|
||||
<HeroSection locale={locale} />
|
||||
</BaseLayout>
|
||||
32
apps/website/src/templates/drops/HeroSection.vue
Normal file
32
apps/website/src/templates/drops/HeroSection.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import HeroCenter01 from '../../components/blocks/HeroCenter01.vue'
|
||||
import { externalLinks, getRoutes } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const routes = getRoutes(locale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeroCenter01
|
||||
:visual="{
|
||||
src: '/affiliates/brand/comfy-amplified-logo.png',
|
||||
alt: t('drops.hero.visualAlt', locale),
|
||||
width: 632,
|
||||
height: 632
|
||||
}"
|
||||
:title="t('drops.hero.title', locale)"
|
||||
:primary-cta="{
|
||||
label: t('drops.hero.primary', locale),
|
||||
href: routes.download
|
||||
}"
|
||||
:secondary-cta="{
|
||||
label: t('drops.hero.secondary', locale),
|
||||
href: externalLinks.cloud,
|
||||
target: '_blank'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
28
apps/website/src/templates/drops/SubscribeBanner.vue
Normal file
28
apps/website/src/templates/drops/SubscribeBanner.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
// V1 fallback: point at the Comfy YouTube channel until content team supplies
|
||||
// a dedicated event-registration URL (Google Form, Eventbrite, etc.).
|
||||
const signUpHref = externalLinks.youtube
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-primary-comfy-plum text-primary-warm-white flex w-full flex-col items-center justify-center gap-2 px-6 py-3 text-center text-sm sm:flex-row sm:gap-4"
|
||||
>
|
||||
<p>{{ t('drops.banner.text', locale) }}</p>
|
||||
<a
|
||||
:href="signUpHref"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="font-semibold uppercase underline underline-offset-4 hover:no-underline"
|
||||
>
|
||||
{{ t('drops.banner.cta', locale) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user