1
1
<script setup lang="ts">
2
2
import { animate } from ' motion-v'
3
+ import { joinURL } from ' ufo'
3
4
4
5
const { data : page } = await useAsyncData (' figma' , () => queryCollection (' figma' ).first ())
5
6
if (! page .value ) {
6
7
throw createError ({ statusCode: 404 , statusMessage: ' Page not found' , fatal: true })
7
8
}
8
9
10
+ const { url } = useSiteConfig ()
11
+
9
12
useSeoMeta ({
10
13
title: page .value .title ,
11
14
description: page .value .description ,
12
15
ogTitle: page .value .title ,
13
- ogDescription: page .value .description
14
- })
15
-
16
- defineOgImageComponent (' Docs' , {
17
- headline: ' Community'
16
+ ogDescription: page .value .description ,
17
+ ogImage: joinURL (url , ' /figma/og-image.png' )
18
18
})
19
19
20
20
const video = ref <HTMLVideoElement | null >(null )
21
21
const played = ref (false )
22
22
23
+ const { width : windowWidth } = useWindowSize ()
24
+ const isMobile = computed (() => windowWidth .value < 768 )
25
+
23
26
onMounted (async () => {
24
- // Animate cursors
25
27
await new Promise (resolve => setTimeout (resolve , 1000 ))
26
28
const figmaWordPosition = document .querySelector (' #figma' )?.getBoundingClientRect ()
27
29
const nuxtWordPosition = document .querySelector (' #nuxt' )?.getBoundingClientRect ()
28
30
const initialScrollX = window .scrollX
29
31
const initialScrollY = window .scrollY
30
32
31
33
if (figmaWordPosition && nuxtWordPosition ) {
32
- const cursor1Sequence = async () => {
33
- await animate (' #cursor1' , { left: Math .round (Math .random () * window .outerWidth ), top: Math .round (Math .random () * window .outerHeight ) }, { duration: 0.1 }).finished
34
- await animate (' #cursor1' , { opacity: 1 }, { duration: 0.3 }).finished
35
- await animate (' #cursor1' , {
36
- left: Math .round (figmaWordPosition .left + initialScrollX + figmaWordPosition .width / 2 ),
37
- top: Math .round (figmaWordPosition .top + initialScrollY - figmaWordPosition .height / 4 )
38
- }, { duration: 1.5 , delay: 0.2 , ease: ' easeInOut' }).finished
39
- await animate (' #cursor1' , { scale: 0.8 }, { duration: 0.1 , ease: ' easeOut' }).finished
40
- await animate (' #cursor1' , { scale: 1 }, { duration: 0.1 , ease: ' easeOut' }).finished
41
- await animate (' #figma' , { color: ' var(--ui-info)' }, { duration: 0.3 , ease: ' easeOut' }).finished
42
- await animate (' #cursor1' , {
43
- left: Math .round (figmaWordPosition .left + initialScrollX + figmaWordPosition .width ),
44
- top: Math .round (figmaWordPosition .top + initialScrollY )
45
- }, { duration: 0.6 , ease: ' easeInOut' }).finished
46
- }
34
+ const createCursorSequence = async (
35
+ cursorId : string ,
36
+ targetWord : DOMRect ,
37
+ targetColor : string ,
38
+ wordId : string ,
39
+ delay : number = 0
40
+ ) => {
41
+ const maxWidth = isMobile .value ? windowWidth .value * 0.8 : window .outerWidth
42
+ const maxHeight = isMobile .value ? window .innerHeight * 0.6 : window .outerHeight
43
+
44
+ await animate (cursorId , {
45
+ left: Math .round (Math .random () * maxWidth ),
46
+ top: Math .round (Math .random () * maxHeight )
47
+ }, { duration: 0.1 , delay }).finished
48
+
49
+ await animate (cursorId , { opacity: 1 }, { duration: 0.3 }).finished
47
50
48
- const cursor2Sequence = async () => {
49
- await animate (' #cursor2' , { left: Math .round (Math .random () * window .outerWidth ), top: Math .round (Math .random () * window .outerHeight ) }, { duration: 0.1 , delay: 0.6 }).finished
50
- await animate (' #cursor2' , { opacity: 1 }, { duration: 0.3 }).finished
51
- await animate (' #cursor2' , {
52
- left: Math .round (nuxtWordPosition .left + initialScrollX + nuxtWordPosition .width / 2 ),
53
- top: Math .round (nuxtWordPosition .top + initialScrollY - nuxtWordPosition .height / 4 )
54
- }, { duration: 1.5 , delay: 0.2 , ease: ' easeInOut' }).finished
55
- await animate (' #cursor2' , { scale: 0.8 }, { duration: 0.1 , ease: ' easeOut' }).finished
56
- await animate (' #cursor2' , { scale: 1 }, { duration: 0.1 , ease: ' easeOut' }).finished
57
- await animate (' #nuxt' , { color: ' var(--ui-success)' }, { duration: 0.3 , ease: ' easeOut' }).finished
58
- await animate (' #cursor2' , {
59
- left: Math .round (nuxtWordPosition .left + initialScrollX + nuxtWordPosition .width ),
60
- top: Math .round (nuxtWordPosition .top + initialScrollY )
61
- }, { duration: 0.6 , ease: ' easeInOut' }).finished
51
+ const clickPositionX = isMobile .value
52
+ ? Math .round (targetWord .left + initialScrollX + targetWord .width * 0.5 )
53
+ : Math .round (targetWord .left + initialScrollX + targetWord .width / 2 )
54
+ const clickPositionY = isMobile .value
55
+ ? Math .round (targetWord .top + initialScrollY - targetWord .height / 0.7 )
56
+ : Math .round (targetWord .top + initialScrollY - targetWord .height / 1.2 )
57
+
58
+ await animate (cursorId , {
59
+ left: clickPositionX ,
60
+ top: clickPositionY
61
+ }, { duration: 1 , delay: 0.2 , ease: ' easeInOut' }).finished
62
+
63
+ await animate (cursorId , { scale: 0.8 }, { duration: 0.1 , ease: ' easeOut' }).finished
64
+ await animate (cursorId , { scale: 1 }, { duration: 0.1 , ease: ' easeOut' }).finished
65
+ await animate (wordId , { color: targetColor }, { duration: 0.3 , ease: ' easeOut' }).finished
66
+
67
+ const finalPositionX = isMobile .value
68
+ ? Math .round (targetWord .left + initialScrollX + targetWord .width * 1 )
69
+ : Math .round (targetWord .left + initialScrollX + targetWord .width )
70
+ const finalPositionY = isMobile .value
71
+ ? Math .round (targetWord .top + initialScrollY + targetWord .height * - 1.2 )
72
+ : Math .round (targetWord .top + initialScrollY - targetWord .height / 2 )
73
+
74
+ await animate (cursorId , {
75
+ left: finalPositionX ,
76
+ top: finalPositionY
77
+ }, { duration: 0.5 , ease: ' easeInOut' }).finished
62
78
}
63
79
64
- await Promise .all ([cursor1Sequence (), cursor2Sequence ()])
80
+ await Promise .all ([
81
+ createCursorSequence (' #cursor1' , figmaWordPosition , ' var(--ui-info)' , ' #figma' ),
82
+ createCursorSequence (' #cursor2' , nuxtWordPosition , ' var(--ui-success)' , ' #nuxt' , 0.5 )
83
+ ])
65
84
}
66
85
})
67
86
</script >
@@ -148,7 +167,8 @@ onMounted(async () => {
148
167
:ui =" {
149
168
container: 'lg:grid-cols-0 !gap-0 px-4 sm:px-6 lg:px-8',
150
169
wrapper: 'grid grid-cols-1 lg:grid-cols-2',
151
- description: 'lg:mt-0' }"
170
+ description: 'mt-2'
171
+ }"
152
172
orientation =" horizontal"
153
173
class =" rounded-none bg-gradient-to-b from-elevated/50 to-default"
154
174
>
@@ -174,20 +194,10 @@ onMounted(async () => {
174
194
</UTabs >
175
195
</UPageSection >
176
196
<UPageSection v-bind =" page.section2" orientation =" horizontal" :ui =" { container: 'py-16 sm:py-16 lg:py-16' }" >
177
- <NuxtImg
178
- v-if =" page.section2.image"
179
- v-bind =" page.section2.image"
180
- class =" w-full h-auto rounded-lg"
181
- loading =" lazy"
182
- />
197
+ <NuxtImg v-if =" page.section2.image" v-bind =" page.section2.image" class =" w-full h-auto rounded-lg" loading =" lazy" />
183
198
</UPageSection >
184
199
<UPageSection v-bind =" page.section3" orientation =" horizontal" :ui =" { container: 'py-16 sm:pt-16 lg:pt-16' }" >
185
- <NuxtImg
186
- v-if =" page.section3.image"
187
- v-bind =" page.section3.image"
188
- class =" w-full h-auto rounded-lg"
189
- loading =" lazy"
190
- />
200
+ <NuxtImg v-if =" page.section3.image" v-bind =" page.section3.image" class =" w-full h-auto rounded-lg" loading =" lazy" />
191
201
</UPageSection >
192
202
<USeparator />
193
203
<UPageSection
@@ -207,12 +217,7 @@ onMounted(async () => {
207
217
<div aria-hidden =" true" class =" absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
208
218
<ul class =" grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 items-start justify-center border border-default border-b-0 sm:divide-x divide-y lg:divide-y-0 divide-default" >
209
219
<li v-for =" (step, index) in page?.section4.steps" :key =" step.title" class =" flex flex-col gap-y-4 justify-start group h-full p-4" >
210
- <NuxtImg
211
- v-if =" step.image"
212
- v-bind =" step.image"
213
- class =" rounded-sm"
214
- loading =" lazy"
215
- />
220
+ <NuxtImg v-if =" step.image" v-bind =" step.image" class =" rounded-sm" loading =" lazy" />
216
221
<div >
217
222
<h2 class =" font-semibold inline-flex items-center gap-x-1" >
218
223
<UBadge :label =" index + 1" size =" sm" color =" neutral" variant =" subtle" class =" rounded-full tabular-nums" /> {{ step.title }}
@@ -225,77 +230,15 @@ onMounted(async () => {
225
230
</ul >
226
231
</UPageSection >
227
232
<UPageSection v-bind =" page.features2" :ui =" { container: 'py-16 sm:py-16 lg:py-16', features: 'mt-0' }" class =" border-y border-default" />
228
- <UPageSection
229
- v-if =" page.pricing"
230
- :title =" page.pricing.title"
231
- :description =" page.pricing.description"
232
- orientation =" vertical"
233
- :ui =" {
234
- title: 'sm:text-left',
235
- description: 'sm:text-left',
236
- links: 'sm:justify-start',
237
- container: 'relative !pb-0',
238
- wrapper: 'sm:pl-8'
239
- }"
240
- >
241
- <div aria-hidden =" true" class =" absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
242
- <UPricingPlans compact class =" -space-x-px" >
243
- <UPricingPlan
244
- v-for =" (plan, index) in page.pricing.plans"
245
- :key =" index"
246
- :title =" plan.title"
247
- :description =" plan.description"
248
- :price =" plan.price"
249
- :discount =" plan.discount"
250
- :billing-period =" plan.billing_period"
251
- :billing-cycle =" plan.billing_cycle"
252
- :highlight =" plan.highlight"
253
- :features =" plan.features"
254
- :button =" plan.button"
255
- :terms =" plan.terms"
256
- class =" rounded-none"
257
- :class =" plan.class"
258
- >
259
- <template #features >
260
- <li v-for =" (feature, i) in plan.features" :key =" i" class =" flex items-center gap-2 min-w-0" >
261
- <UIcon name =" i-lucide-circle-check" class =" size-5 shrink-0 text-primary" />
262
- <MDC :value =" feature" unwrap =" p" tag =" span" class =" text-sm truncate text-accented" :cache-key =" `figma-pricing-plan-${index}-feature-${i}`" />
263
- </li >
264
- </template >
265
- <template #button >
266
- <div class =" flex flex-col w-full items-center gap-2" >
267
- <UButton v-bind =" plan.button" block size =" lg" />
268
- <UButton
269
- v-if =" plan.extraButton"
270
- v-bind =" plan.extraButton"
271
- block
272
- size =" lg"
273
- variant =" outline"
274
- color =" neutral"
275
- />
276
- </div >
277
- </template >
278
- </UPricingPlan >
279
- </UPricingPlans >
280
- </UPageSection >
233
+
281
234
<UPageCTA v-if =" page.customers" :title =" page.customers.title" :ui =" { title: '!text-base font-medium', container: 'sm:py-12 sm:gap-8' }" variant =" outline" class =" rounded-none" >
282
235
<UPageMarquee pause-on-hover :ui =" { root: '[--duration:40s]' }" >
283
- <img
284
- v-for =" (logo, index) in page.customers.items"
285
- :key =" index"
286
- v-bind =" logo"
287
- class =" h-6 shrink-0 max-w-[140px] filter invert dark:invert-0"
288
- loading =" lazy"
289
- >
236
+ <img v-for =" (logo, index) in page.customers.items" :key =" index" v-bind =" logo" class =" h-6 shrink-0 max-w-[140px] filter invert dark:invert-0" loading =" lazy" >
290
237
</UPageMarquee >
291
238
</UPageCTA >
292
239
<UPageSection v-bind =" page.faq" :ui =" { container: 'relative' }" >
293
240
<div aria-hidden =" true" class =" hidden lg:block absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
294
- <UPageAccordion
295
- multiple
296
- :items =" (page.faq.items as any[])"
297
- class =" max-w-4xl mx-auto"
298
- >
241
+ <UPageAccordion multiple :items =" (page.faq.items as any[])" class =" max-w-4xl mx-auto" >
299
242
<template #body =" { item , index } " >
300
243
<MDC :value =" item.content" unwrap =" p" :cache-key =" `figma-faq-${index}-content`" />
301
244
</template >
0 commit comments