Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 158 additions & 101 deletions app/[lang]/(home)/_components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@

import { ExternalLink } from 'lucide-react'
import { motion } from 'motion/react'
import Image from 'next/image'
import { useTranslations } from 'next-intl'
import * as React from 'react'
import { AwardBadge } from '@/components/ui/award-badge'

import { Button } from '@/components/ui/button'
import {
HUGGING_FACE_SPACE,
URL_TEN_AGENT,
URL_TEN_TURN_DETECTION,
URL_TEN_VAD
} from '@/constants'
import { HUGGING_FACE_SPACE, URL_TEN_AGENT } from '@/constants'
import { Link } from '@/lib/next-intl-navigation'
import { cn } from '@/lib/utils'
import { SAMPLE_PROJECTS } from './sample-projects'

const TITLES = ['titleLowlantency', 'titleMultimodal', 'titleEdgeCloud']
const awardLink = 'https://github.com/ten-framework/ten-framework'

const TITLES = ['titleLowlantency', 'titleMultimodal', 'titleEdgeCloud']
const titleVariants = {
visible: { y: 0, opacity: 1 },
hidden: (direction: number) => ({
Expand All @@ -30,75 +25,170 @@ const titleVariants = {

export function ProjectsShowcase(props: { className?: string }) {
const { className } = props
const t = useTranslations('homePage')

return (
<div
className={cn(
'w-full bg-gray-50/50 py-20 dark:bg-gray-900/50',
className
)}
>
<div className={cn('w-full py-16 md:py-24', className)}>
<div className='container mx-auto px-4'>
<div className='mb-12 text-center'>
<h2 className='mb-4 font-bold text-4xl text-black tracking-tight'>
From the Community
</h2>
<p className='mx-auto max-w-2xl text-black text-lg'>
Discover amazing projects built with TEN Framework
</p>
</div>
<div className='rounded-3xl border border-border bg-card/90 p-6 backdrop-blur-sm md:p-8'>
<div className='mb-6 md:mb-8'>
<h2 className='font-semibold text-xl tracking-tight md:text-2xl'>
{t('communityTitle')}
</h2>
</div>

<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
{SAMPLE_PROJECTS.map((project) => (
<Link
key={project.id}
href={project.href}
className='group hover:-translate-y-1 relative overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm transition-all duration-300 hover:shadow-lg dark:border-gray-700 dark:bg-gray-800'
>
<div className='aspect-video w-full overflow-hidden bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-900/20 dark:to-purple-900/20'>
<div className='flex h-full items-center justify-center text-gray-400'>
<div className='text-center'>
<div className='text-sm'>Preview</div>
</div>
<div className='grid gap-6 sm:grid-cols-2 md:gap-8 lg:grid-cols-3 xl:grid-cols-4'>
{SAMPLE_PROJECTS.map((project) => (
<Link
key={project.id}
href={project.href}
className='group hover:-translate-y-0.5 relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-all duration-300 hover:shadow-md dark:border-gray-700 dark:bg-gray-800'
>
<div className='aspect-video w-full overflow-hidden rounded-t-xl'>
<Image
src={project.image}
alt={project.title}
Comment on lines +48 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent broken images on project cards

The new ProjectsShowcase now renders project.image via <Image> for every card, but the sample data points to /api/placeholder/300/200 and this repo has no app/api/placeholder route (rg finds no implementation). Visiting the landing page will therefore request a 404 for every card thumbnail, leaving all preview images broken. Either switch back to the static gradient preview or provide a real image/source for these URLs.

Useful? React with 👍 / 👎.

fill={false}
width={480}
height={270}
className='h-full w-full object-cover'
loading='lazy'
/>
</div>
</div>

<div className='p-6'>
<div className='mb-3 flex items-center justify-between'>
<span className='inline-block rounded-full bg-blue-100 px-3 py-1 font-medium text-blue-700 text-xs dark:bg-blue-900/30 dark:text-blue-300'>
{project.category}
</span>
<span className='text-gray-500 text-sm dark:text-gray-400'>
{project.remixes} Remixes
</span>
</div>
<div className='p-5 md:p-6'>
<div className='mb-3 flex items-center justify-between'>
<span className='inline-block rounded-full bg-blue-100 px-2.5 py-0.5 font-medium text-blue-700 text-xs dark:bg-blue-900/30 dark:text-blue-300'>
{project.category}
</span>
</div>

<h3 className='mb-2 font-semibold text-gray-900 text-lg transition-colors group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400'>
{project.title}
</h3>
<h3 className='mb-2 font-semibold text-base text-gray-900 transition-colors group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400'>
{project.title}
</h3>

<p className='mb-4 line-clamp-2 text-gray-600 text-sm dark:text-gray-300'>
{project.description}
</p>
<p className='line-clamp-2 text-gray-600 text-xs dark:text-gray-300'>
{project.description}
</p>

<div className='flex items-center gap-2'>
<div className='flex h-6 w-6 items-center justify-center rounded-full bg-gray-200 font-medium text-gray-700 text-xs dark:bg-gray-700 dark:text-gray-300'>
{project.author.charAt(0).toUpperCase()}
<div className='mt-4 flex items-center gap-2'>
<div className='flex h-5 w-5 items-center justify-center rounded-full bg-gray-200 font-medium text-[10px] text-gray-700 dark:bg-gray-700 dark:text-gray-300'>
{project.author.charAt(0).toUpperCase()}
</div>
<span className='text-gray-600 text-xs dark:text-gray-400'>
{project.author}
</span>
</div>
<span className='text-gray-600 text-sm dark:text-gray-400'>
{project.author}
</span>
</div>
</div>
</Link>
</Link>
))}
</div>
</div>
</div>
</div>
)
}

export function CapabilitiesShowcase(props: { className?: string }) {
const { className } = props
const t = useTranslations('homePage')

const chips = [
t('chipVoice'),
t('chipMultimodal'),
t('chipEdgeCloud'),
t('chipProduction')
]

const makeGradient = (from: string, to: string) => {
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='640' height='360'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0%' stop-color='${from}'/><stop offset='100%' stop-color='${to}'/></linearGradient></defs><rect width='640' height='360' fill='url(%23g)'/></svg>`
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
}

const items = [
{
title: t('capabilities1Title'),
desc: t('capabilities1Desc'),
image: makeGradient('#A5F3FC', '#93C5FD')
},
{
title: t('capabilities2Title'),
desc: t('capabilities2Desc'),
image: makeGradient('#FDE68A', '#FCA5A5')
},
{
title: t('capabilities3Title'),
desc: t('capabilities3Desc'),
image: makeGradient('#C4B5FD', '#60A5FA')
},
{
title: t('capabilities4Title'),
desc: t('capabilities4Desc'),
image: makeGradient('#86EFAC', '#22D3EE')
},
{
title: t('capabilities5Title'),
desc: t('capabilities5Desc'),
image: makeGradient('#FBCFE8', '#A7F3D0')
},
{
title: t('capabilities6Title'),
desc: t('capabilities6Desc'),
image: makeGradient('#93C5FD', '#F59E0B')
}
]

return (
<div className={cn('w-full py-12 md:py-16', className)}>
<div className='container mx-auto px-4'>
<div className='mb-8 text-center'>
<h2 className='mb-3 font-bold text-4xl tracking-tight'>
{t('capabilitiesTitle')}
</h2>
<p className='mx-auto max-w-2xl text-lg text-muted-foreground'>
{t('capabilitiesDescription')}
</p>
</div>

<div className='mb-6 flex flex-wrap items-center justify-center gap-2'>
{chips.map((c) => (
<span
key={c}
className='inline-flex items-center rounded-full border border-border bg-card px-3 py-1 font-medium text-muted-foreground text-sm'
>
{c}
</span>
))}
</div>

<div className='mt-12 text-center'>
<Button variant='outline' size='lg' className='gap-2'>
View All Projects
<ExternalLink className='h-4 w-4' />
</Button>
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
{items.map((it) => {
return (
<div
key={`cap-${it.title}`}
className='group hover:-translate-y-1 relative overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm transition-all duration-300 hover:shadow-lg dark:border-gray-700 dark:bg-gray-800'
>
<div className='aspect-video w-full overflow-hidden rounded-t-2xl'>
<Image
src={it.image}
alt={it.title}
width={640}
height={360}
className='h-full w-full object-cover'
loading='lazy'
/>
</div>
<div className='p-6'>
<h3 className='mb-2 font-semibold text-gray-900 text-lg dark:text-white'>
{it.title}
</h3>
<p className='text-gray-600 text-sm dark:text-gray-300'>
{it.desc}
</p>
</div>
</div>
)
})}
</div>
</div>
</div>
Expand All @@ -109,9 +199,7 @@ export function Hero(props: { className?: string }) {
const { className } = props

const t = useTranslations('homePage')

const [titleNumber, setTitleNumber] = React.useState(0)

React.useEffect(() => {
const timeoutId = setTimeout(() => {
if (titleNumber === TITLES.length - 1) {
Expand All @@ -126,46 +214,15 @@ export function Hero(props: { className?: string }) {
return (
<div className={cn('w-full text-foreground', className)}>
<div className='container mx-auto'>
<div className='flex flex-col items-center justify-center gap-4 py-0 sm:py-1 md:gap-6 md:py-2 lg:py-3 xl:py-4 2xl:py-6'>
<div>
<Button
variant='secondary'
size='sm'
className='h-auto gap-2 bg-blue-600/[0.05] px-3 py-2 text-blue-600 transition-all duration-600 hover:scale-105 hover:bg-blue-600/[0.08] hover:text-blue-500 sm:h-8 sm:px-4 sm:py-0'
asChild
>
<span className='inline-flex flex-wrap items-center justify-center gap-x-2 gap-y-1 whitespace-normal text-center'>
🎉{' '}
<Link
href={URL_TEN_VAD}
className='font-medium text-blue-500 text-sm underline-offset-2 hover:underline sm:text-base dark:text-blue-300'
>
TEN VAD
</Link>
<span className='font-medium text-blue-500 text-sm sm:text-base dark:text-blue-300'>
and
</span>
<Link
href={URL_TEN_TURN_DETECTION}
className='font-medium text-blue-500 text-sm underline-offset-2 hover:underline sm:text-base dark:text-blue-300'
>
TEN Turn Detection
</Link>
<span className='font-medium text-blue-500 text-sm sm:text-base dark:text-blue-300'>
are now part of the TEN open-source ecosystem!
</span>
</span>
</Button>
</div>

<div className='flex flex-col items-center justify-center gap-2 py-1 md:gap-3 md:py-2 lg:py-3'>
<div className='relative z-10 flex flex-col items-center gap-4'>
<div className='flex flex-wrap items-center justify-center gap-6'>
<AwardBadge type='github-trending' link={awardLink} />
</div>
</div>

<div className='flex flex-col gap-3'>
<h1 className='text-center font-regular text-4xl tracking-tight md:text-5xl lg:text-6xl'>
<h1 className='text-center font-regular text-3xl tracking-tight md:text-4xl lg:text-5xl'>
<span className='font-medium text-spektr-cyan-50'>
{t('titlePrefix')}
</span>
Expand Down Expand Up @@ -199,14 +256,14 @@ export function Hero(props: { className?: string }) {
</p>
</div>

<div className='flex flex-col gap-3 sm:flex-row'>
<Button size='lg' className='gap-4' asChild>
<div className='flex flex-col gap-2 sm:flex-row'>
<Button className='gap-3' asChild>
<Link href={URL_TEN_AGENT} target='_blank'>
{t('heroBtnTryTenAgent')}
<ExternalLink className='size-4' />
</Link>
</Button>
<Button size='lg' className='gap-4' variant='outline' asChild>
<Button className='gap-3' variant='outline' asChild>
<Link href={HUGGING_FACE_SPACE} target='_blank'>
{t('huggingFaceSpace')}
<ExternalLink className='size-4' />
Expand Down
25 changes: 14 additions & 11 deletions app/[lang]/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useTheme } from 'next-themes'
import { useEffect, useRef, useState } from 'react'
import { Hero } from '@/app/[lang]/(home)/_components'
import { Hero, ProjectsShowcase } from '@/app/[lang]/(home)/_components'

const BackgroundVideo = () => {
const { resolvedTheme } = useTheme()
Expand All @@ -24,21 +24,21 @@ const BackgroundVideo = () => {
window.matchMedia('(pointer: coarse)').matches
const isSmallViewport =
typeof window !== 'undefined' && window.innerWidth < 768
type NetInfo = { saveData?: boolean; effectiveType?: string }
type NavigatorWithConnection = Navigator & { connection?: NetInfo }
const saveData =
typeof navigator !== 'undefined' &&
(navigator as any).connection?.saveData
(navigator as NavigatorWithConnection).connection?.saveData
const effectiveType =
typeof navigator !== 'undefined' &&
(navigator as any).connection?.effectiveType
(navigator as NavigatorWithConnection).connection?.effectiveType
const isSlowNetwork =
effectiveType && ['2g', '3g', 'slow-2g'].includes(effectiveType)

const allowAutoplay =
!prefersReducedMotion &&
!saveData &&
!isSlowNetwork &&
!isCoarsePointer &&
!isSmallViewport
const isMobileLike = isCoarsePointer || isSmallViewport
const allowAutoplay = isMobileLike
? !prefersReducedMotion && !saveData && !isSlowNetwork
: !prefersReducedMotion
setShouldRenderVideo(allowAutoplay)
}, [])

Expand Down Expand Up @@ -90,9 +90,12 @@ export default function HomePage() {

{/* Content */}
<div className='relative z-10'>
<div className='flex flex-1 flex-col justify-center text-center'>
<Hero className='flex h-full w-full items-center justify-center' />
<div className='flex flex-1 flex-col text-center'>
<div className='pt-6 pb-4 md:pt-8 lg:pt-10'>
<Hero className='flex w-full items-center justify-center' />
</div>
</div>
<ProjectsShowcase className='mt-2 md:mt-4' />
</div>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion components/ui/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { ExternalLink, Github } from 'lucide-react'
import { Github } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { Logo } from '@/components/ui/logo'
import { Link } from '@/lib/next-intl-navigation'
Expand Down
Loading
Loading