mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-12 01:09:36 +00:00
Compare commits
5 Commits
glary/supp
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa74d2d6b | ||
|
|
93ff253625 | ||
|
|
2b6cc16575 | ||
|
|
85ad748c06 | ||
|
|
4b81b127dd |
@@ -1,20 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
Locale,
|
||||
LocalizedText,
|
||||
TranslationKey
|
||||
} from '../../i18n/translations'
|
||||
import type { EventItem } from '../../content.config'
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from './BrandButton.vue'
|
||||
|
||||
export type EventItem = {
|
||||
label: LocalizedText
|
||||
title: LocalizedText
|
||||
cta: LocalizedText
|
||||
href: string
|
||||
}
|
||||
|
||||
const {
|
||||
locale = 'en',
|
||||
headingKey,
|
||||
@@ -40,12 +30,12 @@ const {
|
||||
<div class="grid grid-cols-1 gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div class="flex flex-col gap-8">
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-4xl font-light tracking-tight lg:text-6xl"
|
||||
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
|
||||
>
|
||||
{{ t(headingKey, locale) }}
|
||||
</h2>
|
||||
<p
|
||||
class="text-primary-comfy-canvas max-w-sm text-sm/relaxed lg:text-base"
|
||||
class="max-w-sm text-sm/relaxed text-primary-comfy-canvas lg:text-base"
|
||||
>
|
||||
{{ t(descriptionKey, locale) }}
|
||||
</p>
|
||||
@@ -66,20 +56,20 @@ const {
|
||||
v-for="(event, i) in events"
|
||||
:key="i"
|
||||
:href="event.href"
|
||||
class="group border-primary-comfy-canvas/15 flex items-center gap-4 border-b py-6 lg:gap-8"
|
||||
class="group flex items-center gap-4 border-b border-primary-comfy-canvas/15 py-6 lg:gap-8"
|
||||
>
|
||||
<span
|
||||
class="text-primary-comfy-canvas shrink-0 text-sm font-medium"
|
||||
class="shrink-0 text-sm font-medium text-primary-comfy-canvas"
|
||||
>
|
||||
{{ event.label[locale] }}
|
||||
{{ event.label }}
|
||||
</span>
|
||||
<span class="text-primary-warm-gray flex-1 text-sm">
|
||||
{{ event.title[locale] }}
|
||||
{{ event.title }}
|
||||
</span>
|
||||
<span
|
||||
class="text-primary-comfy-yellow flex shrink-0 items-center gap-2 text-sm"
|
||||
>
|
||||
{{ event.cta[locale] }}
|
||||
{{ event.cta }}
|
||||
<svg
|
||||
class="size-4 transition-transform group-hover:translate-x-0.5"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -101,15 +101,15 @@ onMounted(() => {
|
||||
<div class="min-h-[640px] w-full">
|
||||
<p
|
||||
v-if="hasEmbedLoadError"
|
||||
class="text-sm/6 text-primary-comfy-canvas"
|
||||
class="text-primary-comfy-canvas text-sm/6"
|
||||
role="status"
|
||||
>
|
||||
{{ t('contact.form.embedLoadErrorPrefix', locale) }}
|
||||
<a
|
||||
class="text-primary-comfy-yellow underline"
|
||||
href="mailto:support@comfy.org"
|
||||
href="mailto:hello@comfy.org"
|
||||
>
|
||||
support@comfy.org
|
||||
hello@comfy.org
|
||||
</a>
|
||||
{{ t('contact.form.embedLoadErrorSuffix', locale) }}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import GalleryItemAttribution from './GalleryItemAttribution.vue'
|
||||
|
||||
@@ -53,7 +53,7 @@ defineEmits<{ click: [] }>()
|
||||
<div class="flex w-full items-end justify-between p-4">
|
||||
<div class="gap-2">
|
||||
<p class="text-sm font-bold text-white">{{ item.title }}</p>
|
||||
<p class="text-primary-comfy-canvas text-xs">
|
||||
<p class="text-xs text-primary-comfy-canvas">
|
||||
<GalleryItemAttribution :item :locale />
|
||||
</p>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ defineEmits<{ click: [] }>()
|
||||
<!-- Mobile metadata -->
|
||||
<div v-if="mobile" class="mt-2 gap-2">
|
||||
<p class="text-sm font-bold text-white">{{ item.title }}</p>
|
||||
<p class="text-primary-comfy-canvas text-xs">
|
||||
<p class="text-xs text-primary-comfy-canvas">
|
||||
<GalleryItemAttribution :item :locale />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import { lockScroll, unlockScroll } from '../../composables/scrollLock'
|
||||
import { prefersReducedMotion } from '../../composables/useReducedMotion'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
@@ -123,11 +123,11 @@ onUnmounted(() => {
|
||||
<!-- Close button -->
|
||||
<button
|
||||
:aria-label="t('gallery.detail.close', locale)"
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink hover:bg-primary-comfy-yellow group absolute right-10 z-10 flex size-10 cursor-pointer items-center justify-center rounded-2xl border-2 transition-colors lg:top-8 lg:right-26"
|
||||
class="border-primary-comfy-yellow hover:bg-primary-comfy-yellow group absolute right-10 z-10 flex size-10 cursor-pointer items-center justify-center rounded-2xl border-2 bg-primary-comfy-ink transition-colors lg:top-8 lg:right-26"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<span
|
||||
class="bg-primary-comfy-yellow group-hover:bg-primary-comfy-ink size-5 transition-colors"
|
||||
class="bg-primary-comfy-yellow size-5 transition-colors group-hover:bg-primary-comfy-ink"
|
||||
style="mask: url('/icons/close.svg') center / contain no-repeat"
|
||||
/>
|
||||
</button>
|
||||
@@ -136,7 +136,7 @@ onUnmounted(() => {
|
||||
<div class="relative hidden min-h-0 w-full flex-1 pt-12 lg:flex">
|
||||
<!-- Left: info card -->
|
||||
<div
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-5xl relative z-10 flex w-80 shrink-0 flex-col justify-between self-start p-8"
|
||||
class="bg-primary-comfy-yellow rounded-5xl relative z-10 flex w-80 shrink-0 flex-col justify-between self-start p-8 text-primary-comfy-ink"
|
||||
>
|
||||
<div
|
||||
:class="transitioning ? 'opacity-0' : 'opacity-100'"
|
||||
@@ -170,7 +170,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Right: large image -->
|
||||
<div
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink rounded-5xl flex max-h-full min-h-0 flex-1 items-center justify-center overflow-hidden border-2 p-4"
|
||||
class="border-primary-comfy-yellow rounded-5xl flex max-h-full min-h-0 flex-1 items-center justify-center overflow-hidden border-2 bg-primary-comfy-ink p-4"
|
||||
>
|
||||
<component
|
||||
:is="activeItem.video ? 'video' : 'img'"
|
||||
@@ -197,7 +197,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<!-- Image -->
|
||||
<div
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink flex w-full flex-1 items-center overflow-hidden rounded-4xl border-2 p-3"
|
||||
class="border-primary-comfy-yellow flex w-full flex-1 items-center overflow-hidden rounded-4xl border-2 bg-primary-comfy-ink p-3"
|
||||
>
|
||||
<component
|
||||
:is="activeItem.video ? 'video' : 'img'"
|
||||
@@ -223,7 +223,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Info card -->
|
||||
<div
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink w-full rounded-4xl p-6"
|
||||
class="bg-primary-comfy-yellow w-full rounded-4xl p-6 text-primary-comfy-ink"
|
||||
>
|
||||
<div
|
||||
:class="transitioning ? 'opacity-0' : 'opacity-100'"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { visibleGalleryItems as items } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import GalleryCard from './GalleryCard.vue'
|
||||
import GalleryDetailModal from './GalleryDetailModal.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
const { items, locale = 'en' } = defineProps<{
|
||||
items: GalleryItem[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const modalIndex = ref(0)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||
|
||||
import type { LearningTutorial } from '../../data/learningTutorials'
|
||||
import type { ResolvedTutorial } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { lockScroll, unlockScroll } from '../../composables/scrollLock'
|
||||
@@ -9,7 +9,7 @@ import { t } from '../../i18n/translations'
|
||||
import VideoPlayer from '../common/VideoPlayer.vue'
|
||||
|
||||
const { tutorial, locale = 'en' } = defineProps<{
|
||||
tutorial: LearningTutorial
|
||||
tutorial: ResolvedTutorial
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
@@ -39,7 +39,7 @@ onUnmounted(() => {
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
ref="dialogRef"
|
||||
:aria-label="tutorial.title[locale]"
|
||||
:aria-label="tutorial.title"
|
||||
class="fixed inset-0 z-50 flex size-full max-h-none max-w-none flex-col items-center justify-center border-0 bg-transparent px-4 py-8 backdrop-blur-xl backdrop:bg-transparent lg:px-20 lg:py-8"
|
||||
@click="handleBackdropClick"
|
||||
@keydown="handleKeydown"
|
||||
@@ -60,7 +60,7 @@ onUnmounted(() => {
|
||||
class="border-primary-comfy-yellow rounded-5xl flex w-full max-w-7xl items-center justify-center overflow-hidden border-2 bg-primary-comfy-ink p-3 lg:p-4"
|
||||
>
|
||||
<VideoPlayer
|
||||
:key="tutorial.id"
|
||||
:key="tutorial.slug"
|
||||
:locale
|
||||
:src="tutorial.videoSrc"
|
||||
:poster="tutorial.poster"
|
||||
@@ -73,7 +73,7 @@ onUnmounted(() => {
|
||||
class="mt-6 text-center text-lg font-medium text-primary-comfy-canvas lg:text-xl"
|
||||
>
|
||||
{{ t('learning.tutorials.titlePrefix', locale) }}
|
||||
{{ tutorial.title[locale] }}
|
||||
{{ tutorial.title }}
|
||||
</h2>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { ResolvedTutorial } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import {
|
||||
getTutorialPosterSrc,
|
||||
learningTutorials
|
||||
} from '../../data/learningTutorials'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { getTutorialPosterSrc } from '../../utils/tutorial'
|
||||
import Badge from '../common/Badge.vue'
|
||||
import MaskRevealButton from '../common/MaskRevealButton.vue'
|
||||
import TutorialDetailDialog from './TutorialDetailDialog.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
const { tutorials, locale = 'en' } = defineProps<{
|
||||
tutorials: readonly ResolvedTutorial[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
const activeTutorialId = ref<string | null>(null)
|
||||
const activeTutorialSlug = ref<string | null>(null)
|
||||
const activeTutorial = () =>
|
||||
learningTutorials.find((tutorial) => tutorial.id === activeTutorialId.value)
|
||||
tutorials.find((tutorial) => tutorial.slug === activeTutorialSlug.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,15 +32,15 @@ const activeTutorial = () =>
|
||||
class="grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 lg:grid-cols-3 lg:gap-x-8"
|
||||
>
|
||||
<li
|
||||
v-for="tutorial in learningTutorials"
|
||||
:key="tutorial.id"
|
||||
v-for="tutorial in tutorials"
|
||||
:key="tutorial.slug"
|
||||
class="bg-transparency-white-t4 flex flex-col gap-4 overflow-hidden rounded-3xl border-0 p-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="group relative block aspect-video cursor-pointer overflow-hidden rounded-3xl"
|
||||
:aria-label="`${t('learning.tutorials.titlePrefix', locale)} ${tutorial.title[locale]}`"
|
||||
@click="activeTutorialId = tutorial.id"
|
||||
:aria-label="`${t('learning.tutorials.titlePrefix', locale)} ${tutorial.title}`"
|
||||
@click="activeTutorialSlug = tutorial.slug"
|
||||
>
|
||||
<video
|
||||
:src="getTutorialPosterSrc(tutorial)"
|
||||
@@ -74,7 +75,7 @@ const activeTutorial = () =>
|
||||
class="text-sm/snug text-primary-comfy-canvas lg:text-base/snug"
|
||||
>
|
||||
{{ t('learning.tutorials.titlePrefix', locale) }}<br />
|
||||
{{ tutorial.title[locale] }}
|
||||
{{ tutorial.title }}
|
||||
</h3>
|
||||
<MaskRevealButton
|
||||
v-if="tutorial.href"
|
||||
@@ -103,7 +104,7 @@ const activeTutorial = () =>
|
||||
|
||||
<ul class="flex flex-wrap gap-2">
|
||||
<li v-for="tag in tutorial.tags" :key="tag">
|
||||
<Badge>{{ t(tag, locale) }}</Badge>
|
||||
<Badge>{{ tag }}</Badge>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -114,7 +115,7 @@ const activeTutorial = () =>
|
||||
v-if="activeTutorial()"
|
||||
:tutorial="activeTutorial()!"
|
||||
:locale="locale"
|
||||
@close="activeTutorialId = null"
|
||||
@close="activeTutorialSlug = null"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
@@ -9,64 +9,14 @@ import BrandButton from '../common/BrandButton.vue'
|
||||
import GalleryCard from '../gallery/GalleryCard.vue'
|
||||
import GalleryDetailModal from '../gallery/GalleryDetailModal.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
const { items, locale = 'en' } = defineProps<{
|
||||
items: readonly GalleryItem[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
const modelName = 'Grok'
|
||||
const ctaHref = 'https://comfy.org/workflows/model/grok'
|
||||
|
||||
const items: GalleryItem[] = [
|
||||
{
|
||||
id: 'subway-swan',
|
||||
image: 'https://media.comfy.org/website/gallery/subway-swan_compressed.png',
|
||||
title: 'Subway Swan',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats'
|
||||
},
|
||||
{
|
||||
id: 'milos-little-wonder',
|
||||
video:
|
||||
'https://media.comfy.org/website/gallery/milos-little-wonder_compressed.mp4',
|
||||
title: 'Milos Little Wonder',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats'
|
||||
},
|
||||
{
|
||||
id: 'amber-passage',
|
||||
image:
|
||||
'https://media.comfy.org/website/gallery/amber-passage_compressed.jpg',
|
||||
title: 'Amber Passage',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats',
|
||||
objectPosition: 'bottom'
|
||||
},
|
||||
{
|
||||
id: 'neon-revenant',
|
||||
video:
|
||||
'https://media.comfy.org/website/gallery/neon-revenant_compressed.mp4',
|
||||
title: 'Neon Revenant',
|
||||
userAlias: 'Eric Solorio',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.instagram.com/enigmatic_e'
|
||||
},
|
||||
{
|
||||
id: 'midnight-umami',
|
||||
image:
|
||||
'https://media.comfy.org/website/gallery/midnight_umami_compressed.png',
|
||||
title: 'Midnight Umami',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats'
|
||||
}
|
||||
]
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const modalIndex = ref(0)
|
||||
|
||||
|
||||
131
apps/website/src/content.config.test.ts
Normal file
131
apps/website/src/content.config.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('astro:content', () => ({
|
||||
defineCollection: (config: unknown) => config
|
||||
}))
|
||||
vi.mock('astro/loaders', () => ({
|
||||
glob: () => ({})
|
||||
}))
|
||||
|
||||
import { eventsSchema, gallerySchema, tutorialsSchema } from './content.config'
|
||||
|
||||
const validEntry = {
|
||||
order: 1,
|
||||
title: 'Until Our Eye Interlink harajuku',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'ThinkDiffusion',
|
||||
tool: 'ComfyUI',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/eye.webm',
|
||||
href: 'https://www.thinkdiffusion.com/studio'
|
||||
}
|
||||
|
||||
describe('gallerySchema', () => {
|
||||
it('accepts a valid entry', () => {
|
||||
const result = gallerySchema.safeParse(validEntry)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an entry missing a required field with a Zod error naming the field', () => {
|
||||
const { title: _omit, ...withoutTitle } = validEntry
|
||||
const result = gallerySchema.safeParse(withoutTitle)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join('.'))
|
||||
expect(paths).toContain('title')
|
||||
}
|
||||
})
|
||||
|
||||
it('defaults visible to true when omitted', () => {
|
||||
const result = gallerySchema.safeParse(validEntry)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.visible).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves an explicit visible: false', () => {
|
||||
const result = gallerySchema.safeParse({ ...validEntry, visible: false })
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.visible).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an invalid URL in image/video/href', () => {
|
||||
const result = gallerySchema.safeParse({
|
||||
...validEntry,
|
||||
href: 'not a url'
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
const validEvent = {
|
||||
order: 1,
|
||||
label: 'Live Stream:',
|
||||
title: 'Zero to Node: Building Your First Workflow',
|
||||
cta: 'Link',
|
||||
href: 'https://example.com/event'
|
||||
}
|
||||
|
||||
describe('eventsSchema', () => {
|
||||
it('accepts a valid entry', () => {
|
||||
const result = eventsSchema.safeParse(validEvent)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an entry missing a required field with a Zod error naming the field', () => {
|
||||
const { title: _omit, ...withoutTitle } = validEvent
|
||||
const result = eventsSchema.safeParse(withoutTitle)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join('.'))
|
||||
expect(paths).toContain('title')
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts "#" as a placeholder href', () => {
|
||||
const result = eventsSchema.safeParse({ ...validEvent, href: '#' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a truly malformed href that is neither a URL nor "#"', () => {
|
||||
const result = eventsSchema.safeParse({ ...validEvent, href: 'not a url' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
const validTutorial = {
|
||||
order: 1,
|
||||
tags: ['Partner Nodes', 'Image To Video'],
|
||||
title: 'Cleanplate Walkthrough',
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg'
|
||||
}
|
||||
|
||||
describe('tutorialsSchema', () => {
|
||||
it('accepts a valid entry', () => {
|
||||
const result = tutorialsSchema.safeParse(validTutorial)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an entry missing a required field with a Zod error naming the field', () => {
|
||||
const { title: _omit, ...withoutTitle } = validTutorial
|
||||
const result = tutorialsSchema.safeParse(withoutTitle)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join('.'))
|
||||
expect(paths).toContain('title')
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an invalid videoSrc URL', () => {
|
||||
const result = tutorialsSchema.safeParse({
|
||||
...validTutorial,
|
||||
videoSrc: 'not a url'
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
78
apps/website/src/content.config.ts
Normal file
78
apps/website/src/content.config.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { defineCollection } from 'astro:content'
|
||||
import { glob } from 'astro/loaders'
|
||||
import { z } from 'astro/zod'
|
||||
|
||||
export const gallerySchema = z.object({
|
||||
order: z.number().int(),
|
||||
image: z.string().url().optional(),
|
||||
video: z.string().url().optional(),
|
||||
title: z.string(),
|
||||
userAlias: z.string(),
|
||||
teamAlias: z.string(),
|
||||
tool: z.string(),
|
||||
href: z.string().url().optional(),
|
||||
objectPosition: z.string().optional(),
|
||||
objectFit: z.string().optional(),
|
||||
visible: z.boolean().default(true)
|
||||
})
|
||||
|
||||
export type GalleryItem = z.infer<typeof gallerySchema>
|
||||
|
||||
export const eventsSchema = z.object({
|
||||
order: z.number().int(),
|
||||
label: z.string(),
|
||||
title: z.string(),
|
||||
cta: z.string(),
|
||||
href: z.string().url().or(z.literal('#'))
|
||||
})
|
||||
|
||||
export type EventItem = z.infer<typeof eventsSchema>
|
||||
|
||||
export const tutorialsSchema = z.object({
|
||||
order: z.number().int(),
|
||||
tags: z.array(z.string()),
|
||||
title: z.string(),
|
||||
videoSrc: z.string().url(),
|
||||
href: z.string().url().optional(),
|
||||
poster: z.string().url().optional(),
|
||||
posterTime: z.number().optional()
|
||||
})
|
||||
|
||||
export type LearningTutorial = z.infer<typeof tutorialsSchema>
|
||||
export type ResolvedTutorial = LearningTutorial & { slug: string }
|
||||
|
||||
// The default `generateId` lowercases path segments (e.g. `zh-CN/foo` becomes
|
||||
// `zh-cn/foo`), which collides with the BCP-47 locale codes Astro's i18n
|
||||
// config uses elsewhere. Strip the file extension to keep the path — and
|
||||
// therefore the locale prefix — verbatim.
|
||||
const preservePathId = ({ entry }: { entry: string }): string =>
|
||||
entry.replace(/\.[^.]+$/, '')
|
||||
|
||||
const gallery = defineCollection({
|
||||
loader: glob({
|
||||
pattern: '**/*.json',
|
||||
base: './src/content/gallery',
|
||||
generateId: preservePathId
|
||||
}),
|
||||
schema: gallerySchema
|
||||
})
|
||||
|
||||
const events = defineCollection({
|
||||
loader: glob({
|
||||
pattern: '**/*.json',
|
||||
base: './src/content/events',
|
||||
generateId: preservePathId
|
||||
}),
|
||||
schema: eventsSchema
|
||||
})
|
||||
|
||||
const tutorials = defineCollection({
|
||||
loader: glob({
|
||||
pattern: '**/*.md',
|
||||
base: './src/content/tutorials',
|
||||
generateId: preservePathId
|
||||
}),
|
||||
schema: tutorialsSchema
|
||||
})
|
||||
|
||||
export const collections = { gallery, events, tutorials }
|
||||
7
apps/website/src/content/events/en/event-london.json
Normal file
7
apps/website/src/content/events/en/event-london.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": 2,
|
||||
"label": "Event 1",
|
||||
"title": "Lorem ipsum dollar sita met",
|
||||
"cta": "London, UK",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": 3,
|
||||
"label": "Event 2",
|
||||
"title": "Lorem ipsum dollar sita met",
|
||||
"cta": "San Francisco",
|
||||
"href": "#"
|
||||
}
|
||||
7
apps/website/src/content/events/en/zero-to-node.json
Normal file
7
apps/website/src/content/events/en/zero-to-node.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": 1,
|
||||
"label": "Live Stream:",
|
||||
"title": "Zero to Node: Building Your First Workflow",
|
||||
"cta": "Link",
|
||||
"href": "#"
|
||||
}
|
||||
7
apps/website/src/content/events/zh-CN/event-london.json
Normal file
7
apps/website/src/content/events/zh-CN/event-london.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": 2,
|
||||
"label": "活动 1",
|
||||
"title": "此处为活动描述的占位文本",
|
||||
"cta": "英国伦敦",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": 3,
|
||||
"label": "活动 2",
|
||||
"title": "此处为活动描述的占位文本",
|
||||
"cta": "旧金山",
|
||||
"href": "#"
|
||||
}
|
||||
7
apps/website/src/content/events/zh-CN/zero-to-node.json
Normal file
7
apps/website/src/content/events/zh-CN/zero-to-node.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": 1,
|
||||
"label": "直播:",
|
||||
"title": "从零到节点:构建你的第一个工作流",
|
||||
"cta": "链接",
|
||||
"href": "#"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/amber-astronaut.json
Normal file
9
apps/website/src/content/gallery/en/amber-astronaut.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 17,
|
||||
"image": "https://media.comfy.org/website/gallery/gallery.webp",
|
||||
"title": "Amber Astronaut",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
11
apps/website/src/content/gallery/en/amber-passage.json
Normal file
11
apps/website/src/content/gallery/en/amber-passage.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"order": 21,
|
||||
"image": "https://media.comfy.org/website/gallery/amber-passage_compressed.jpg",
|
||||
"title": "Amber Passage",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"objectPosition": "bottom",
|
||||
"visible": false
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/animation-reel.json
Normal file
9
apps/website/src/content/gallery/en/animation-reel.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 16,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds_statue.webm",
|
||||
"title": "Animation Reel",
|
||||
"userAlias": "Andidea",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.youtube.com/watch?v=qu3eIQ1uln8"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/autopoiesis.json
Normal file
9
apps/website/src/content/gallery/en/autopoiesis.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 5,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/cigarette.webm",
|
||||
"title": "Autopoiesis",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/visualfrisson/?hl=en"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 13,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/paul_trillo.webm",
|
||||
"title": "Cuco - A Love Letter To LA",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "CoffeeVectors",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1062859798"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/ddu-du-ddu-du.json
Normal file
9
apps/website/src/content/gallery/en/ddu-du-ddu-du.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 12,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dududu.webm",
|
||||
"title": "DDU-DU DDU-DU",
|
||||
"userAlias": "Purz",
|
||||
"teamAlias": "Andidea",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://vimeo.com/1019924290"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/desert-landing.json
Normal file
9
apps/website/src/content/gallery/en/desert-landing.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 18,
|
||||
"image": "https://media.comfy.org/website/gallery/desert.webp",
|
||||
"title": "Desert Landing",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/eat-it-dance.json
Normal file
9
apps/website/src/content/gallery/en/eat-it-dance.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 6,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/Eat%20It%20-%20Dance%20%5BWanAnimate%5D2.webm",
|
||||
"title": "Eat It - Dance",
|
||||
"userAlias": "Johana Lyu",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.joannalyu.com/"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/fall.json
Normal file
9
apps/website/src/content/gallery/en/fall.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 7,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/flower.webm",
|
||||
"title": "Fall",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C3k9t_6vH5F/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 11,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds.webm",
|
||||
"title": "It's gonna be a good good summer",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685900"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/goodbye-beijing.json
Normal file
9
apps/website/src/content/gallery/en/goodbye-beijing.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 15,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/swings.webm",
|
||||
"title": "Goodbye Beijing",
|
||||
"userAlias": "Rui",
|
||||
"teamAlias": "makeitrad",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://x.com/rui40000"
|
||||
}
|
||||
10
apps/website/src/content/gallery/en/midnight-umami.json
Normal file
10
apps/website/src/content/gallery/en/midnight-umami.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"order": 23,
|
||||
"image": "https://media.comfy.org/website/gallery/midnight_umami_compressed.png",
|
||||
"title": "Midnight Umami",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"visible": false
|
||||
}
|
||||
10
apps/website/src/content/gallery/en/milos-little-wonder.json
Normal file
10
apps/website/src/content/gallery/en/milos-little-wonder.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"order": 20,
|
||||
"video": "https://media.comfy.org/website/gallery/milos-little-wonder_compressed.mp4",
|
||||
"title": "Milos Little Wonder",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"visible": false
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/neon-nights.json
Normal file
9
apps/website/src/content/gallery/en/neon-nights.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 3,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/arcade.webm",
|
||||
"title": "Neon Nights",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C1kG1oErzUV/"
|
||||
}
|
||||
10
apps/website/src/content/gallery/en/neon-revenant.json
Normal file
10
apps/website/src/content/gallery/en/neon-revenant.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"order": 22,
|
||||
"video": "https://media.comfy.org/website/gallery/neon-revenant_compressed.mp4",
|
||||
"title": "Neon Revenant",
|
||||
"userAlias": "Eric Solorio",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.instagram.com/enigmatic_e",
|
||||
"visible": false
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/origami-world.json
Normal file
9
apps/website/src/content/gallery/en/origami-world.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 9,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/origami_shortened.webm",
|
||||
"title": "Origami world",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 2,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/kyrie.webm",
|
||||
"title": "Origins - Kyrie Irving",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1021360563"
|
||||
}
|
||||
9
apps/website/src/content/gallery/en/shot-on-instax.json
Normal file
9
apps/website/src/content/gallery/en/shot-on-instax.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 10,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/biking.webm",
|
||||
"title": "Shot on InstaX",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 14,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/chibi_fish_tank_shortened.webm",
|
||||
"title": "Show you my garden",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685479"
|
||||
}
|
||||
10
apps/website/src/content/gallery/en/subway-swan.json
Normal file
10
apps/website/src/content/gallery/en/subway-swan.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"order": 19,
|
||||
"image": "https://media.comfy.org/website/gallery/subway-swan_compressed.png",
|
||||
"title": "Subway Swan",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"visible": false
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 1,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/eye.webm",
|
||||
"title": "Until Our Eye Interlink harajuku",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.thinkdiffusion.com/studio#success-stories-anta"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 8,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/buildings.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C6rEuJ4p9xU/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 4,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dusk_mountains.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "MidJourney man",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/midjourney.man/?hl=fr"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 17,
|
||||
"image": "https://media.comfy.org/website/gallery/gallery.webp",
|
||||
"title": "Amber Astronaut",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 16,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds_statue.webm",
|
||||
"title": "Animation Reel",
|
||||
"userAlias": "Andidea",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.youtube.com/watch?v=qu3eIQ1uln8"
|
||||
}
|
||||
9
apps/website/src/content/gallery/zh-CN/autopoiesis.json
Normal file
9
apps/website/src/content/gallery/zh-CN/autopoiesis.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 5,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/cigarette.webm",
|
||||
"title": "Autopoiesis",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/visualfrisson/?hl=en"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 13,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/paul_trillo.webm",
|
||||
"title": "Cuco - A Love Letter To LA",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "CoffeeVectors",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1062859798"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 12,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dududu.webm",
|
||||
"title": "DDU-DU DDU-DU",
|
||||
"userAlias": "Purz",
|
||||
"teamAlias": "Andidea",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://vimeo.com/1019924290"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 18,
|
||||
"image": "https://media.comfy.org/website/gallery/desert.webp",
|
||||
"title": "Desert Landing",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
9
apps/website/src/content/gallery/zh-CN/eat-it-dance.json
Normal file
9
apps/website/src/content/gallery/zh-CN/eat-it-dance.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 6,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/Eat%20It%20-%20Dance%20%5BWanAnimate%5D2.webm",
|
||||
"title": "Eat It - Dance",
|
||||
"userAlias": "Johana Lyu",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.joannalyu.com/"
|
||||
}
|
||||
9
apps/website/src/content/gallery/zh-CN/fall.json
Normal file
9
apps/website/src/content/gallery/zh-CN/fall.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 7,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/flower.webm",
|
||||
"title": "Fall",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C3k9t_6vH5F/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 11,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds.webm",
|
||||
"title": "It's gonna be a good good summer",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685900"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 15,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/swings.webm",
|
||||
"title": "Goodbye Beijing",
|
||||
"userAlias": "Rui",
|
||||
"teamAlias": "makeitrad",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://x.com/rui40000"
|
||||
}
|
||||
9
apps/website/src/content/gallery/zh-CN/neon-nights.json
Normal file
9
apps/website/src/content/gallery/zh-CN/neon-nights.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 3,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/arcade.webm",
|
||||
"title": "Neon Nights",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C1kG1oErzUV/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 9,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/origami_shortened.webm",
|
||||
"title": "Origami world",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 2,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/kyrie.webm",
|
||||
"title": "Origins - Kyrie Irving",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1021360563"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 10,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/biking.webm",
|
||||
"title": "Shot on InstaX",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 14,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/chibi_fish_tank_shortened.webm",
|
||||
"title": "Show you my garden",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685479"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 1,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/eye.webm",
|
||||
"title": "Until Our Eye Interlink harajuku",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.thinkdiffusion.com/studio#success-stories-anta"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 8,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/buildings.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C6rEuJ4p9xU/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": 4,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dusk_mountains.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "MidJourney man",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/midjourney.man/?hl=fr"
|
||||
}
|
||||
264
apps/website/src/content/queries.test.ts
Normal file
264
apps/website/src/content/queries.test.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
import {
|
||||
getEventsByLocale,
|
||||
getGalleryByIds,
|
||||
getTutorialsByLocale,
|
||||
getVisibleGalleryByLocale,
|
||||
slugOf
|
||||
} from './queries'
|
||||
|
||||
vi.mock('astro:content', () => ({
|
||||
getCollection: vi.fn()
|
||||
}))
|
||||
|
||||
const getCollectionMock = vi.mocked(getCollection)
|
||||
|
||||
interface FixtureEntry {
|
||||
id: string
|
||||
collection: 'gallery'
|
||||
data: {
|
||||
order: number
|
||||
title: string
|
||||
userAlias: string
|
||||
teamAlias: string
|
||||
tool: string
|
||||
image?: string
|
||||
video?: string
|
||||
href?: string
|
||||
objectPosition?: string
|
||||
objectFit?: string
|
||||
visible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
function entry(
|
||||
id: string,
|
||||
overrides: Partial<FixtureEntry['data']> = {}
|
||||
): FixtureEntry {
|
||||
return {
|
||||
id,
|
||||
collection: 'gallery',
|
||||
data: {
|
||||
order: 0,
|
||||
title: 'Title',
|
||||
userAlias: 'User',
|
||||
teamAlias: 'Team',
|
||||
tool: 'ComfyUI',
|
||||
visible: true,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
getCollectionMock.mockReset()
|
||||
})
|
||||
|
||||
describe('slugOf', () => {
|
||||
it('strips the locale prefix from an entry id', () => {
|
||||
const entry = { id: 'en/until-our-eye-interlink-harajuku' }
|
||||
expect(slugOf(entry)).toBe('until-our-eye-interlink-harajuku')
|
||||
})
|
||||
|
||||
it('handles zh-CN prefix', () => {
|
||||
const entry = { id: 'zh-CN/desert-landing' }
|
||||
expect(slugOf(entry)).toBe('desert-landing')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getVisibleGalleryByLocale', () => {
|
||||
it('returns only entries whose id starts with the requested locale prefix', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/alpha'),
|
||||
entry('en/beta'),
|
||||
entry('zh-CN/alpha'),
|
||||
entry('zh-CN/gamma')
|
||||
] as never)
|
||||
|
||||
const en = await getVisibleGalleryByLocale('en')
|
||||
expect(en.map((e) => e.id)).toEqual(['en/alpha', 'en/beta'])
|
||||
|
||||
const zh = await getVisibleGalleryByLocale('zh-CN')
|
||||
expect(zh.map((e) => e.id)).toEqual(['zh-CN/alpha', 'zh-CN/gamma'])
|
||||
})
|
||||
|
||||
it('excludes entries with visible: false', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/shown', { visible: true }),
|
||||
entry('en/hidden', { visible: false }),
|
||||
entry('en/also-shown', { visible: true })
|
||||
] as never)
|
||||
|
||||
const result = await getVisibleGalleryByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual(['en/shown', 'en/also-shown'])
|
||||
})
|
||||
|
||||
it('sorts entries by the order field ascending, not by id', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/charlie', { order: 1 }),
|
||||
entry('en/alpha', { order: 3 }),
|
||||
entry('en/bravo', { order: 2 })
|
||||
] as never)
|
||||
|
||||
const result = await getVisibleGalleryByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual([
|
||||
'en/charlie',
|
||||
'en/bravo',
|
||||
'en/alpha'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGalleryByIds', () => {
|
||||
it('returns entries in the order of the input slug array', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/alpha'),
|
||||
entry('en/beta'),
|
||||
entry('en/gamma'),
|
||||
entry('zh-CN/alpha')
|
||||
] as never)
|
||||
|
||||
const result = await getGalleryByIds(['gamma', 'alpha', 'beta'], 'en')
|
||||
expect(result.map((e) => e.id)).toEqual(['en/gamma', 'en/alpha', 'en/beta'])
|
||||
})
|
||||
|
||||
it('drops slugs with no matching entry in the requested locale', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/alpha'),
|
||||
entry('zh-CN/beta')
|
||||
] as never)
|
||||
|
||||
const result = await getGalleryByIds(['alpha', 'missing', 'beta'], 'en')
|
||||
expect(result.map((e) => e.id)).toEqual(['en/alpha'])
|
||||
})
|
||||
})
|
||||
|
||||
interface EventsFixtureEntry {
|
||||
id: string
|
||||
collection: 'events'
|
||||
data: {
|
||||
order: number
|
||||
label: string
|
||||
title: string
|
||||
cta: string
|
||||
href: string
|
||||
}
|
||||
}
|
||||
|
||||
function eventEntry(
|
||||
id: string,
|
||||
overrides: Partial<EventsFixtureEntry['data']> = {}
|
||||
): EventsFixtureEntry {
|
||||
return {
|
||||
id,
|
||||
collection: 'events',
|
||||
data: {
|
||||
order: 0,
|
||||
label: 'Label',
|
||||
title: 'Title',
|
||||
cta: 'CTA',
|
||||
href: '#',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('getEventsByLocale', () => {
|
||||
it('returns only entries whose id starts with the requested locale prefix', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
eventEntry('en/alpha'),
|
||||
eventEntry('en/beta'),
|
||||
eventEntry('zh-CN/alpha')
|
||||
] as never)
|
||||
|
||||
const en = await getEventsByLocale('en')
|
||||
expect(en.map((e) => e.id)).toEqual(['en/alpha', 'en/beta'])
|
||||
})
|
||||
|
||||
it('sorts entries by the order field ascending, not by id', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
eventEntry('en/charlie', { order: 1 }),
|
||||
eventEntry('en/alpha', { order: 3 }),
|
||||
eventEntry('en/bravo', { order: 2 })
|
||||
] as never)
|
||||
|
||||
const result = await getEventsByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual([
|
||||
'en/charlie',
|
||||
'en/bravo',
|
||||
'en/alpha'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns an empty array for a locale with no entries', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
eventEntry('en/alpha'),
|
||||
eventEntry('en/beta')
|
||||
] as never)
|
||||
|
||||
const result = await getEventsByLocale('zh-CN')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
interface TutorialsFixtureEntry {
|
||||
id: string
|
||||
collection: 'tutorials'
|
||||
data: {
|
||||
order: number
|
||||
tags: string[]
|
||||
title: string
|
||||
videoSrc: string
|
||||
href?: string
|
||||
poster?: string
|
||||
posterTime?: number
|
||||
}
|
||||
}
|
||||
|
||||
function tutorialEntry(
|
||||
id: string,
|
||||
overrides: Partial<TutorialsFixtureEntry['data']> = {}
|
||||
): TutorialsFixtureEntry {
|
||||
return {
|
||||
id,
|
||||
collection: 'tutorials',
|
||||
data: {
|
||||
order: 0,
|
||||
tags: ['Tag'],
|
||||
title: 'Title',
|
||||
videoSrc: 'https://example.com/video.mp4',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('getTutorialsByLocale', () => {
|
||||
it('returns only entries whose id starts with the requested locale prefix', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
tutorialEntry('en/alpha'),
|
||||
tutorialEntry('en/beta'),
|
||||
tutorialEntry('zh-CN/alpha')
|
||||
] as never)
|
||||
|
||||
const en = await getTutorialsByLocale('en')
|
||||
expect(en.map((e) => e.id)).toEqual(['en/alpha', 'en/beta'])
|
||||
})
|
||||
|
||||
it('sorts entries by the order field ascending, not by id', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
tutorialEntry('en/charlie', { order: 1 }),
|
||||
tutorialEntry('en/alpha', { order: 3 }),
|
||||
tutorialEntry('en/bravo', { order: 2 })
|
||||
] as never)
|
||||
|
||||
const result = await getTutorialsByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual([
|
||||
'en/charlie',
|
||||
'en/bravo',
|
||||
'en/alpha'
|
||||
])
|
||||
})
|
||||
})
|
||||
61
apps/website/src/content/queries.ts
Normal file
61
apps/website/src/content/queries.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { getCollection } from 'astro:content'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
import type { Locale } from '../i18n/translations'
|
||||
|
||||
export type GalleryEntry = CollectionEntry<'gallery'>
|
||||
export type EventsEntry = CollectionEntry<'events'>
|
||||
export type TutorialsEntry = CollectionEntry<'tutorials'>
|
||||
|
||||
export function slugOf(entry: { id: string }): string {
|
||||
const slash = entry.id.indexOf('/')
|
||||
return slash === -1 ? entry.id : entry.id.slice(slash + 1)
|
||||
}
|
||||
|
||||
export async function getVisibleGalleryByLocale(
|
||||
locale: Locale
|
||||
): Promise<GalleryEntry[]> {
|
||||
const prefix = `${locale}/`
|
||||
const entries: GalleryEntry[] = await getCollection('gallery')
|
||||
return entries
|
||||
.filter(
|
||||
(entry) => entry.id.startsWith(prefix) && entry.data.visible !== false
|
||||
)
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
}
|
||||
|
||||
export async function getGalleryByIds(
|
||||
slugs: readonly string[],
|
||||
locale: Locale
|
||||
): Promise<GalleryEntry[]> {
|
||||
const entries: GalleryEntry[] = await getCollection('gallery')
|
||||
const bySlug = new Map<string, GalleryEntry>()
|
||||
for (const entry of entries) {
|
||||
if (entry.id.startsWith(`${locale}/`)) {
|
||||
bySlug.set(slugOf(entry), entry)
|
||||
}
|
||||
}
|
||||
return slugs
|
||||
.map((slug) => bySlug.get(slug))
|
||||
.filter((entry): entry is GalleryEntry => entry !== undefined)
|
||||
}
|
||||
|
||||
export async function getEventsByLocale(
|
||||
locale: Locale
|
||||
): Promise<EventsEntry[]> {
|
||||
const prefix = `${locale}/`
|
||||
const entries: EventsEntry[] = await getCollection('events')
|
||||
return entries
|
||||
.filter((entry) => entry.id.startsWith(prefix))
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
}
|
||||
|
||||
export async function getTutorialsByLocale(
|
||||
locale: Locale
|
||||
): Promise<TutorialsEntry[]> {
|
||||
const prefix = `${locale}/`
|
||||
const entries: TutorialsEntry[] = await getCollection('tutorials')
|
||||
return entries
|
||||
.filter((entry) => entry.id.startsWith(prefix))
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
order: 1
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Cleanplate Walkthrough
|
||||
videoSrc: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg
|
||||
---
|
||||
10
apps/website/src/content/tutorials/en/deaging-workflow.md
Normal file
10
apps/website/src/content/tutorials/en/deaging-workflow.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 2
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Deaging Workflow
|
||||
videoSrc: https://media.comfy.org/website/learning/deaging_workflow_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/deaging_workflow_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=93f286fbc2c8
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 3
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Frame Adjustments Demo
|
||||
videoSrc: https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/frame_adjustments_demo_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=7dca0438edf4
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 4
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Mattes and Utilities
|
||||
videoSrc: https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/mattes_and_utilities_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=be0889296f65
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 5
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Seedance Demo ComfyUI
|
||||
videoSrc: https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/seedance seedance_demo_comfyui_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=ef543bd4a773
|
||||
---
|
||||
10
apps/website/src/content/tutorials/en/sky-replacement.md
Normal file
10
apps/website/src/content/tutorials/en/sky-replacement.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 6
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Sky Replacement
|
||||
videoSrc: https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4
|
||||
poster: https://media.comfy.org/website/learning/skyreplacement_smaller_v06_thumbnail.jpg
|
||||
href: https://comfy.org/workflows/537cf7f1f745-537cf7f1f745/
|
||||
---
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
order: 1
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 净板演练
|
||||
videoSrc: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg
|
||||
---
|
||||
10
apps/website/src/content/tutorials/zh-CN/deaging-workflow.md
Normal file
10
apps/website/src/content/tutorials/zh-CN/deaging-workflow.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 2
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 减龄工作流
|
||||
videoSrc: https://media.comfy.org/website/learning/deaging_workflow_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/deaging_workflow_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=93f286fbc2c8
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 3
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 帧调整演示
|
||||
videoSrc: https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/frame_adjustments_demo_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=7dca0438edf4
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 4
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 遮罩与实用工具
|
||||
videoSrc: https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/mattes_and_utilities_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=be0889296f65
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 5
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: Seedance ComfyUI 演示
|
||||
videoSrc: https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/seedance seedance_demo_comfyui_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=ef543bd4a773
|
||||
---
|
||||
10
apps/website/src/content/tutorials/zh-CN/sky-replacement.md
Normal file
10
apps/website/src/content/tutorials/zh-CN/sky-replacement.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
order: 6
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 天空替换
|
||||
videoSrc: https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4
|
||||
poster: https://media.comfy.org/website/learning/skyreplacement_smaller_v06_thumbnail.jpg
|
||||
href: https://comfy.org/workflows/537cf7f1f745-537cf7f1f745/
|
||||
---
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { EventItem } from '../components/common/EventsSection.vue'
|
||||
|
||||
export const learningEvents: readonly EventItem[] = [
|
||||
{
|
||||
label: { en: 'Live Stream:', 'zh-CN': '直播:' },
|
||||
title: {
|
||||
en: 'Zero to Node: Building Your First Workflow',
|
||||
'zh-CN': '从零到节点:构建你的第一个工作流'
|
||||
},
|
||||
cta: { en: 'Link', 'zh-CN': '链接' },
|
||||
href: '#'
|
||||
},
|
||||
{
|
||||
label: { en: 'Event 1', 'zh-CN': '活动 1' },
|
||||
title: {
|
||||
en: 'Lorem ipsum dollar sita met',
|
||||
'zh-CN': '此处为活动描述的占位文本'
|
||||
},
|
||||
cta: { en: 'London, UK', 'zh-CN': '英国伦敦' },
|
||||
href: '#'
|
||||
},
|
||||
{
|
||||
label: { en: 'Event 2', 'zh-CN': '活动 2' },
|
||||
title: {
|
||||
en: 'Lorem ipsum dollar sita met',
|
||||
'zh-CN': '此处为活动描述的占位文本'
|
||||
},
|
||||
cta: { en: 'San Francisco', 'zh-CN': '旧金山' },
|
||||
href: '#'
|
||||
}
|
||||
] as const
|
||||
@@ -1,191 +0,0 @@
|
||||
export interface GalleryItem {
|
||||
id: string
|
||||
image?: string
|
||||
video?: string
|
||||
title: string
|
||||
userAlias: string
|
||||
teamAlias: string
|
||||
tool: string
|
||||
href?: string
|
||||
objectPosition?: string
|
||||
objectFit?: string
|
||||
/** Defaults to true. Set to false to hide this item from rendered lists. */
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
const galleryItems: GalleryItem[] = [
|
||||
{
|
||||
id: 'until-our-eye-interlink-harajuku',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/eye.webm',
|
||||
title: 'Until Our Eye Interlink harajuku',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'ThinkDiffusion',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.thinkdiffusion.com/studio#success-stories-anta'
|
||||
},
|
||||
{
|
||||
id: 'origins-kyrie-irving',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/kyrie.webm',
|
||||
title: 'Origins - Kyrie Irving',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'ThinkDiffusion',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://vimeo.com/1021360563'
|
||||
},
|
||||
{
|
||||
id: 'neon-nights',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/arcade.webm',
|
||||
title: 'Neon Nights',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'DOGSTUDIO/DEPT®',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/p/C1kG1oErzUV/'
|
||||
},
|
||||
{
|
||||
id: 'untitled-dusk-mountains',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/dusk_mountains.webm',
|
||||
title: 'Untitled',
|
||||
userAlias: 'MidJourney man',
|
||||
teamAlias: 'DOGSTUDIO/DEPT®',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/midjourney.man/?hl=fr'
|
||||
},
|
||||
{
|
||||
id: 'autopoiesis',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/cigarette.webm',
|
||||
title: 'Autopoiesis',
|
||||
userAlias: 'Yogo',
|
||||
teamAlias: 'Visual Frisson',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/visualfrisson/?hl=en'
|
||||
},
|
||||
{
|
||||
id: 'eat-it-dance',
|
||||
video:
|
||||
'https://media.comfy.org/videos/compressed_512/Eat%20It%20-%20Dance%20%5BWanAnimate%5D2.webm',
|
||||
title: 'Eat It - Dance',
|
||||
userAlias: 'Johana Lyu',
|
||||
teamAlias: 'Visual Frisson',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.joannalyu.com/'
|
||||
},
|
||||
{
|
||||
id: 'fall',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/flower.webm',
|
||||
title: 'Fall',
|
||||
userAlias: 'Nathan Shipley',
|
||||
teamAlias: 'Visual Frisson',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/p/C3k9t_6vH5F/'
|
||||
},
|
||||
{
|
||||
id: 'untitled-buildings',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/buildings.webm',
|
||||
title: 'Untitled',
|
||||
userAlias: 'Nathan Shipley',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/p/C6rEuJ4p9xU/'
|
||||
},
|
||||
{
|
||||
id: 'origami-world',
|
||||
video:
|
||||
'https://media.comfy.org/videos/compressed_512/origami_shortened.webm',
|
||||
title: 'Origami world',
|
||||
userAlias: 'Karen X',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/karenxcheng/'
|
||||
},
|
||||
{
|
||||
id: 'shot-on-instax',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/biking.webm',
|
||||
title: 'Shot on InstaX',
|
||||
userAlias: 'Karen X',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/karenxcheng/'
|
||||
},
|
||||
{
|
||||
id: 'good-good-summer',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/clouds.webm',
|
||||
title: "It's gonna be a good good summer",
|
||||
userAlias: 'Paul Trillo',
|
||||
teamAlias: '',
|
||||
tool: 'CogvideoX',
|
||||
href: 'https://vimeo.com/1019685900'
|
||||
},
|
||||
{
|
||||
id: 'ddu-du-ddu-du',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/dududu.webm',
|
||||
title: 'DDU-DU DDU-DU',
|
||||
userAlias: 'Purz',
|
||||
teamAlias: 'Andidea',
|
||||
tool: 'Animatediff',
|
||||
href: 'https://vimeo.com/1019924290'
|
||||
},
|
||||
{
|
||||
id: 'cuco-love-letter-to-la',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/paul_trillo.webm',
|
||||
title: 'Cuco - A Love Letter To LA',
|
||||
userAlias: 'Paul Trillo',
|
||||
teamAlias: 'CoffeeVectors',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://vimeo.com/1062859798'
|
||||
},
|
||||
{
|
||||
id: 'show-you-my-garden',
|
||||
video:
|
||||
'https://media.comfy.org/videos/compressed_512/chibi_fish_tank_shortened.webm',
|
||||
title: 'Show you my garden',
|
||||
userAlias: 'Paul Trillo',
|
||||
teamAlias: '',
|
||||
tool: 'CogvideoX',
|
||||
href: 'https://vimeo.com/1019685479'
|
||||
},
|
||||
{
|
||||
id: 'goodbye-beijing',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/swings.webm',
|
||||
title: 'Goodbye Beijing',
|
||||
userAlias: 'Rui',
|
||||
teamAlias: 'makeitrad',
|
||||
tool: 'Animatediff',
|
||||
href: 'https://x.com/rui40000'
|
||||
},
|
||||
{
|
||||
id: 'animation-reel',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/clouds_statue.webm',
|
||||
title: 'Animation Reel',
|
||||
userAlias: 'Andidea',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.youtube.com/watch?v=qu3eIQ1uln8'
|
||||
},
|
||||
{
|
||||
id: 'amber-astronaut',
|
||||
image: 'https://media.comfy.org/website/gallery/gallery.webp',
|
||||
title: 'Amber Astronaut',
|
||||
userAlias: 'Yogo',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://de.linkedin.com/in/milan-kastenmueller-18778a174'
|
||||
},
|
||||
{
|
||||
id: 'desert-landing',
|
||||
image: 'https://media.comfy.org/website/gallery/desert.webp',
|
||||
title: 'Desert Landing',
|
||||
userAlias: 'Yogo',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://de.linkedin.com/in/milan-kastenmueller-18778a174'
|
||||
}
|
||||
]
|
||||
|
||||
export const visibleGalleryItems: GalleryItem[] = galleryItems.filter(
|
||||
(item) => item.visible !== false
|
||||
)
|
||||
|
||||
/** @knipIgnoreUsedByStackedPR */
|
||||
export function getGalleryItemById(id: string): GalleryItem | undefined {
|
||||
return galleryItems.find((item) => item.id === id)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { LocalizedText, TranslationKey } from '../i18n/translations'
|
||||
|
||||
export interface LearningTutorial {
|
||||
id: string
|
||||
tags: readonly TranslationKey[]
|
||||
title: LocalizedText
|
||||
videoSrc: string
|
||||
href?: string
|
||||
poster?: string
|
||||
posterTime?: number
|
||||
}
|
||||
|
||||
const DEFAULT_POSTER_TIME_SECONDS = 1
|
||||
|
||||
const partnerNodesTag: TranslationKey = 'tags.partnerNodes'
|
||||
const imageToVideoTag: TranslationKey = 'tags.imageToVideo'
|
||||
|
||||
export const getTutorialPosterSrc = (tutorial: LearningTutorial): string =>
|
||||
tutorial.poster
|
||||
? tutorial.poster
|
||||
: `${tutorial.videoSrc}#t=${tutorial.posterTime ?? DEFAULT_POSTER_TIME_SECONDS}`
|
||||
|
||||
export const learningTutorials: readonly LearningTutorial[] = [
|
||||
{
|
||||
id: 'cleanplate_walkthrough_v03',
|
||||
title: { en: 'Cleanplate Walkthrough', 'zh-CN': '净板演练' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg',
|
||||
// href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'deaging_workflow_v03',
|
||||
title: { en: 'Deaging Workflow', 'zh-CN': '减龄工作流' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/deaging_workflow_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/deaging_workflow_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=93f286fbc2c8',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'frame_adjustments_demo_v03',
|
||||
title: { en: 'Frame Adjustments Demo', 'zh-CN': '帧调整演示' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/frame_adjustments_demo_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=7dca0438edf4',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'mattes_and_utilities_v03',
|
||||
title: { en: 'Mattes and Utilities', 'zh-CN': '遮罩与实用工具' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/mattes_and_utilities_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=be0889296f65',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'seedance_demo_comfyui_v03',
|
||||
title: { en: 'Seedance Demo ComfyUI', 'zh-CN': 'Seedance ComfyUI 演示' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/seedance seedance_demo_comfyui_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=ef543bd4a773',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'skyreplacement_smaller_v06',
|
||||
title: { en: 'Sky Replacement', 'zh-CN': '天空替换' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/skyreplacement_smaller_v06_thumbnail.jpg',
|
||||
href: 'https://comfy.org/workflows/537cf7f1f745-537cf7f1f745/',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
}
|
||||
] as const
|
||||
@@ -3,10 +3,14 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../components/gallery/HeroSection.vue'
|
||||
import GallerySection from '../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../components/gallery/ContactSection.vue'
|
||||
import { getVisibleGalleryByLocale } from '../content/queries'
|
||||
|
||||
const entries = await getVisibleGalleryByLocale('en')
|
||||
const items = entries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout title="Gallery — Comfy">
|
||||
<HeroSection />
|
||||
<GallerySection client:load />
|
||||
<GallerySection items={items} client:load />
|
||||
<ContactSection />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -8,14 +8,20 @@ import CallToActionSection from '../components/common/CallToActionSection.vue'
|
||||
import { getRoutes } from '../config/routes'
|
||||
import { externalLinks } from '../config/routes'
|
||||
// import { learningEvents } from '../data/events'
|
||||
import { getTutorialsByLocale, slugOf } from '../content/queries'
|
||||
|
||||
const routes = getRoutes('en')
|
||||
const tutorialEntries = await getTutorialsByLocale('en')
|
||||
const tutorials = tutorialEntries.map((entry) => ({
|
||||
slug: slugOf(entry),
|
||||
...entry.data
|
||||
}))
|
||||
---
|
||||
|
||||
<BaseLayout title="Learning — Comfy">
|
||||
<HeroSection client:load />
|
||||
<FeaturedWorkflowSection client:visible />
|
||||
<TutorialsSection client:visible />
|
||||
<TutorialsSection tutorials={tutorials} client:visible />
|
||||
<CallToActionSection
|
||||
headingKey="learning.cta.heading"
|
||||
primaryLabelKey="learning.cta.contactSales"
|
||||
|
||||
@@ -3,7 +3,18 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ModelsHeroSection from '../components/models/ModelsHeroSection.vue'
|
||||
import ModelCreationsSection from '../components/models/ModelCreationsSection.vue'
|
||||
import AIModelsSection from '../components/product/shared/AIModelsSection.vue'
|
||||
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
|
||||
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
|
||||
import { getGalleryByIds } from '../content/queries'
|
||||
|
||||
const featuredCreationSlugs = [
|
||||
'subway-swan',
|
||||
'milos-little-wonder',
|
||||
'amber-passage',
|
||||
'neon-revenant',
|
||||
'midnight-umami'
|
||||
]
|
||||
const featuredEntries = await getGalleryByIds(featuredCreationSlugs, 'en')
|
||||
const featuredItems = featuredEntries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -16,7 +27,7 @@ import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vu
|
||||
videoSrc="https://media.comfy.org/website/models/video_ComfdyUI_00001_.mp4"
|
||||
videoAriaLabel="Grok Imagine output created with ComfyUI"
|
||||
/>
|
||||
<ModelCreationsSection client:load />
|
||||
<ModelCreationsSection items={featuredItems} client:load />
|
||||
<AIModelsSection client:load />
|
||||
<ProductShowcaseSection client:load />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,10 +3,14 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../../components/gallery/HeroSection.vue'
|
||||
import GallerySection from '../../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../../components/gallery/ContactSection.vue'
|
||||
import { getVisibleGalleryByLocale } from '../../content/queries'
|
||||
|
||||
const entries = await getVisibleGalleryByLocale('zh-CN')
|
||||
const items = entries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout title="作品集 — Comfy">
|
||||
<HeroSection locale="zh-CN" />
|
||||
<GallerySection locale="zh-CN" client:load />
|
||||
<GallerySection locale="zh-CN" items={items} client:load />
|
||||
<ContactSection locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -6,15 +6,34 @@ import TutorialsSection from '../../components/learning/TutorialsSection.vue'
|
||||
import CallToActionSection from '../../components/common/CallToActionSection.vue'
|
||||
import EventsSection from '../../components/common/EventsSection.vue'
|
||||
import { getRoutes, externalLinks } from '../../config/routes'
|
||||
import { learningEvents } from '../../data/events'
|
||||
import {
|
||||
getEventsByLocale,
|
||||
getTutorialsByLocale,
|
||||
slugOf
|
||||
} from '../../content/queries'
|
||||
|
||||
const routes = getRoutes('zh-CN')
|
||||
const eventEntries = await getEventsByLocale('zh-CN')
|
||||
const events = eventEntries.map((entry) => entry.data)
|
||||
const tutorialEntries = await getTutorialsByLocale('zh-CN')
|
||||
const tutorials = tutorialEntries.map((entry) => ({
|
||||
slug: slugOf(entry),
|
||||
...entry.data
|
||||
}))
|
||||
---
|
||||
|
||||
<BaseLayout title="学习 — Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<FeaturedWorkflowSection locale="zh-CN" client:visible />
|
||||
<TutorialsSection locale="zh-CN" client:visible />
|
||||
<TutorialsSection locale="zh-CN" tutorials={tutorials} client:visible />
|
||||
<EventsSection
|
||||
locale="zh-CN"
|
||||
headingKey="learning.events.heading"
|
||||
descriptionKey="learning.events.description"
|
||||
notifyLabelKey="learning.events.getNotified"
|
||||
events={events}
|
||||
client:visible
|
||||
/>
|
||||
<CallToActionSection
|
||||
locale="zh-CN"
|
||||
headingKey="learning.cta.heading"
|
||||
|
||||
@@ -4,6 +4,17 @@ import ModelsHeroSection from '../../components/models/ModelsHeroSection.vue'
|
||||
import ModelCreationsSection from '../../components/models/ModelCreationsSection.vue'
|
||||
import AIModelsSection from '../../components/product/shared/AIModelsSection.vue'
|
||||
import ProductShowcaseSection from '../../components/home/ProductShowcaseSection.vue'
|
||||
import { getGalleryByIds } from '../../content/queries'
|
||||
|
||||
const featuredCreationSlugs = [
|
||||
'subway-swan',
|
||||
'milos-little-wonder',
|
||||
'amber-passage',
|
||||
'neon-revenant',
|
||||
'midnight-umami'
|
||||
]
|
||||
const featuredEntries = await getGalleryByIds(featuredCreationSlugs, 'en')
|
||||
const featuredItems = featuredEntries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -17,7 +28,7 @@ import ProductShowcaseSection from '../../components/home/ProductShowcaseSection
|
||||
videoSrc="https://media.comfy.org/website/models/video_ComfdyUI_00001_.mp4"
|
||||
videoAriaLabel="使用 ComfyUI 创建的 Grok Imagine 作品"
|
||||
/>
|
||||
<ModelCreationsSection client:load locale="zh-CN" />
|
||||
<ModelCreationsSection items={featuredItems} client:load locale="zh-CN" />
|
||||
<AIModelsSection client:load locale="zh-CN" />
|
||||
<ProductShowcaseSection client:load locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
40
apps/website/src/utils/tutorial.test.ts
Normal file
40
apps/website/src/utils/tutorial.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getTutorialPosterSrc } from './tutorial'
|
||||
|
||||
describe('getTutorialPosterSrc', () => {
|
||||
it('returns the explicit poster URL when provided', () => {
|
||||
expect(
|
||||
getTutorialPosterSrc({
|
||||
order: 1,
|
||||
tags: [],
|
||||
title: 'T',
|
||||
videoSrc: 'https://example.com/v.mp4',
|
||||
poster: 'https://example.com/poster.jpg'
|
||||
})
|
||||
).toBe('https://example.com/poster.jpg')
|
||||
})
|
||||
|
||||
it('falls back to videoSrc#t=<posterTime> when poster is missing', () => {
|
||||
expect(
|
||||
getTutorialPosterSrc({
|
||||
order: 1,
|
||||
tags: [],
|
||||
title: 'T',
|
||||
videoSrc: 'https://example.com/v.mp4',
|
||||
posterTime: 7
|
||||
})
|
||||
).toBe('https://example.com/v.mp4#t=7')
|
||||
})
|
||||
|
||||
it('uses the default poster time when neither poster nor posterTime is set', () => {
|
||||
expect(
|
||||
getTutorialPosterSrc({
|
||||
order: 1,
|
||||
tags: [],
|
||||
title: 'T',
|
||||
videoSrc: 'https://example.com/v.mp4'
|
||||
})
|
||||
).toBe('https://example.com/v.mp4#t=1')
|
||||
})
|
||||
})
|
||||
8
apps/website/src/utils/tutorial.ts
Normal file
8
apps/website/src/utils/tutorial.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { LearningTutorial } from '../content.config'
|
||||
|
||||
const DEFAULT_POSTER_TIME_SECONDS = 1
|
||||
|
||||
export const getTutorialPosterSrc = (tutorial: LearningTutorial): string =>
|
||||
tutorial.poster
|
||||
? tutorial.poster
|
||||
: `${tutorial.videoSrc}#t=${tutorial.posterTime ?? DEFAULT_POSTER_TIME_SECONDS}`
|
||||
@@ -421,6 +421,15 @@ export default defineConfig([
|
||||
}
|
||||
},
|
||||
|
||||
// Astro virtual modules (astro:content, astro:assets, etc.) are not
|
||||
// resolvable by the TS resolver — they are injected by the Astro build.
|
||||
{
|
||||
files: ['apps/website/**/*.{ts,vue,astro}'],
|
||||
rules: {
|
||||
'import-x/no-unresolved': ['error', { ignore: ['^astro:'] }]
|
||||
}
|
||||
},
|
||||
|
||||
// i18n import enforcement
|
||||
// Vue components must use the useI18n() composable, not the global t/d/st/te
|
||||
{
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
{{ t('auth.login.questionsContactPrefix') }}
|
||||
<a href="mailto:support@comfy.org" class="cursor-pointer text-blue-500">
|
||||
support@comfy.org</a
|
||||
<a href="mailto:hello@comfy.org" class="cursor-pointer text-blue-500">
|
||||
hello@comfy.org</a
|
||||
>.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user