Skip to content
Open
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
108 changes: 56 additions & 52 deletions app/home-page-client.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import Script from 'next/script'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { AIToolsSection } from '@/components/sections/ai-tools-section'
import { CommunityFeaturesSection } from '@/components/sections/community-features-section'
Expand All @@ -15,8 +16,7 @@ import { Navbar } from '@/components/ui/navbar'
import { YouTubeVideoShowcase } from '@/components/ui/youtube-video-showcase'
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'
import { useProjectFilters } from '@/hooks/useProjectFilters'
import { FAQ_DATA } from '@/lib/constants/faqs'
import type { Project, User, VibeVideo } from '@/types/homepage'
import type { FAQ, Project, User, VibeVideo } from '@/types/homepage'

interface HomePageClientProps {
initialIsLoggedIn: boolean
Expand All @@ -33,6 +33,11 @@ export default function HomePageClient({
initialFilterOptions,
initialVibeVideos,
}: HomePageClientProps) {
const commonT = useTranslations('common')
const metadataT = useTranslations('metadata')
const faqT = useTranslations('faq')
const faqItems = Object.values(faqT.raw('items') as Record<string, FAQ>)
const organizationKeywords = metadataT.raw('organization.keywords') as string[]
const projectFilters = useProjectFilters({
authReady: true,
initialProjects,
Expand Down Expand Up @@ -61,6 +66,13 @@ export default function HomePageClient({

return (
<div className="bg-background min-h-screen">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 focus:z-50 focus:p-4 focus:bg-background focus:text-foreground focus:rounded-br-md focus:shadow-md focus:font-medium"
>
{commonT('skipToMainContent')}
</a>

<Script
id="organization-schema"
type="application/ld+json"
Expand All @@ -73,8 +85,7 @@ export default function HomePageClient({
alternateName: ['Komunitas Vibe Coding Indonesia', 'VibeDev Indonesia'],
url: 'https://vibedevid.com',
logo: 'https://vibedevid.com/vibedev-logo.png',
description:
'Komunitas vibe coding Indonesia No. 1 untuk developer, AI enthusiasts, dan tech innovators. Tempat belajar coding pake AI, kolaborasi project open source, dan networking dengan vibe coder Indonesia terbaik.',
description: metadataT('description'),
foundingDate: '2024',
address: {
'@type': 'PostalAddress',
Expand All @@ -83,7 +94,7 @@ export default function HomePageClient({
},
contactPoint: {
'@type': 'ContactPoint',
contactType: 'Community Support',
contactType: metadataT('organization.contactType'),
email: 'hello@vibedevid.com',
},
sameAs: [
Expand All @@ -95,19 +106,10 @@ export default function HomePageClient({
'@type': 'Organization',
name: 'Indonesian Developer Community',
},
keywords: [
'vibe coding',
'komunitas vibe coding',
'komunitas vibe coding indonesia',
'vibe coder indonesia',
'coding pake AI',
'AI untuk coding',
'developer indonesia',
'open source indonesia',
],
keywords: [...organizationKeywords],
audience: {
'@type': 'Audience',
audienceType: 'Developers, AI Enthusiasts, Tech Innovators',
audienceType: metadataT('organization.audienceType'),
geographicArea: 'Indonesia',
},
}),
Expand All @@ -122,7 +124,7 @@ export default function HomePageClient({
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: FAQ_DATA.map((faq) => ({
mainEntity: faqItems.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
Expand All @@ -140,44 +142,46 @@ export default function HomePageClient({
user={initialUser ?? undefined}
/>

<HeroSection
handleJoinWithUs={handleJoinWithUs}
handleViewShowcase={handleViewShowcase}
/>

<ErrorBoundary sectionName="Project Showcase">
<ProjectShowcase
projects={projectFilters.projects}
loading={projectFilters.loading}
selectedFilter={projectFilters.selectedFilter}
setSelectedFilter={projectFilters.setSelectedFilter}
selectedTrending={projectFilters.selectedTrending}
setSelectedTrending={projectFilters.setSelectedTrending}
filterOptions={projectFilters.filterOptions}
<main id="main-content">
<HeroSection
handleJoinWithUs={handleJoinWithUs}
handleViewShowcase={handleViewShowcase}
/>
</ErrorBoundary>

<ErrorBoundary sectionName="Video Showcase">
<section className="py-12 sm:py-16 lg:py-20">
<div className="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
<YouTubeVideoShowcase vibeVideos={initialVibeVideos} />
</div>
</section>
</ErrorBoundary>

<CommunityFeaturesSection />

<AIToolsSection />

<ReviewsSection />

<FAQSection
openFAQ={openFAQ}
toggleFAQ={toggleFAQ}
isVisible={isVisible.faq}
/>
<ErrorBoundary sectionName="Project Showcase">
<ProjectShowcase
projects={projectFilters.projects}
loading={projectFilters.loading}
selectedFilter={projectFilters.selectedFilter}
setSelectedFilter={projectFilters.setSelectedFilter}
selectedTrending={projectFilters.selectedTrending}
setSelectedTrending={projectFilters.setSelectedTrending}
filterOptions={projectFilters.filterOptions}
/>
</ErrorBoundary>

<ErrorBoundary sectionName="Video Showcase">
<section className="py-12 sm:py-16 lg:py-20">
<div className="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
<YouTubeVideoShowcase vibeVideos={initialVibeVideos} />
</div>
</section>
</ErrorBoundary>

<CommunityFeaturesSection />

<AIToolsSection />

<ReviewsSection />

<FAQSection
openFAQ={openFAQ}
toggleFAQ={toggleFAQ}
isVisible={isVisible.faq}
/>

<CTASection handleJoinWithUs={handleJoinWithUs} />
<CTASection handleJoinWithUs={handleJoinWithUs} />
</main>

<Footer />
</div>
Expand Down
6 changes: 5 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchProjectsWithSorting } from '@/lib/actions'
import { ALL_PROJECT_FILTER_VALUE } from '@/lib/constants/project-filters'
import { createAdminClient } from '@/lib/supabase/admin'
import { createClient } from '@/lib/supabase/server'
import { getVideoIconKey } from '@/lib/video-icon-key'
Expand Down Expand Up @@ -97,7 +98,10 @@ export default async function HomePage() {
getVibeVideos(),
])

const initialFilterOptions = ['All', ...((categories ?? []).map((category) => category.display_name) as string[])]
const initialFilterOptions = [
ALL_PROJECT_FILTER_VALUE,
...((categories ?? []).map((category) => category.display_name) as string[]),
]

let userData: User | null = null
if (user) {
Expand Down
29 changes: 19 additions & 10 deletions components/sections/faq-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,40 @@ export function FAQSection({ openFAQ, toggleFAQ, isVisible }: FAQSectionProps) {
{faqArray.map((faq, index) => (
<Card
key={faq.question}
className={`cursor-pointer transition-all duration-700 hover:shadow-md ${
className={`transition duration-700 hover:shadow-md motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100 ${
isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'
}`}
style={{ transitionDelay: `${index * 100}ms` }}
onClick={() => toggleFAQ(index)}
>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<CardContent className="p-0">
<button
type="button"
onClick={() => toggleFAQ(index)}
className="flex w-full cursor-pointer items-center justify-between p-6"
aria-expanded={openFAQ === index}
aria-controls={`faq-answer-${index}`}
id={`faq-question-${index}`}
>
<h3 className="text-left font-semibold">{faq.question}</h3>
Comment on lines +52 to 60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n components/sections/faq-section.tsx | head -80

Repository: julianromli/vibedevid

Length of output: 3648


🌐 Web query:

HTML5 button element content model what elements can go inside button

💡 Result:

The HTML5 <button> element's content model is phrasing content, but with the restriction that there must be no interactive content descendants and no descendants with the tabindex attribute specified. If the <button> is the first child of a customizable select element, it may also contain zero or one <select> element. Phrasing content includes text and elements such as <a>, <abbr>, <b>, <bdi>, <bdo>, <br>, <cite>, <code>, <data>, <em>, <i>, <kbd>, <label>, <mark>, <output>, <progress>, <q>, <ruby>, <samp>, <small>, <span>, <strong>, <sub>, <sup>, <time>, <u>, <var>, <wbr>, and certain embedded content like <img> and <video> (without interactive features). Interactive elements like <a> (with href), <button>, <details>, <input>, <select>, <textarea> are prohibited inside <button>.

Citations:


🌐 Web query:

WAI-ARIA accordion pattern button heading structure best practice

💡 Result:

The best practice for the WAI-ARIA accordion pattern button heading structure, per official W3C ARIA Authoring Practices Guide (APG), is to wrap each accordion header button in a heading element (e.g., h2, h3) with an appropriate aria-level for the page's information architecture. The button is the only interactive element inside the heading; other persistent visual elements (e.g., icons, snippets) should be outside the heading. Example structure:

Accordion Header Title

Panel content...
Key attributes on button: aria-expanded (true/false), aria-controls (ID of panel). Optionally, use role="region" and aria-labelledby on the panel, but avoid if >6 panels to prevent landmark proliferation. This uses native HTML semantics where possible (heading + button), ensures proper keyboard navigation (Enter/Space to toggle), and maintains document outline for screen readers.

Citations:


Wrap the accordion button in the heading, not the other way around.

Line 60 nests <h3> inside <button>, which violates HTML5 content model rules—heading elements are not permitted inside buttons. This breaks heading navigation and document outline in assistive tech. Per W3C WAI-ARIA accordion pattern, the correct structure is <h3><button>…</button></h3>.

🔧 Suggested markup
-              <CardContent className="p-0">
-                <button
-                  type="button"
-                  onClick={() => toggleFAQ(index)}
-                  className="flex w-full cursor-pointer items-center justify-between p-6"
-                  aria-expanded={openFAQ === index}
-                  aria-controls={`faq-answer-${index}`}
-                  id={`faq-question-${index}`}
-                >
-                  <h3 className="text-left font-semibold">{faq.question}</h3>
-                  <div className="ml-4 flex-shrink-0 transition-transform duration-300 motion-reduce:transition-none">
+              <CardContent className="p-0">
+                <h3>
+                  <button
+                    type="button"
+                    onClick={() => toggleFAQ(index)}
+                    className="flex w-full cursor-pointer items-center justify-between p-6"
+                    aria-expanded={openFAQ === index}
+                    aria-controls={`faq-answer-${index}`}
+                    id={`faq-question-${index}`}
+                  >
+                    <span className="text-left font-semibold">{faq.question}</span>
+                    <div className="ml-4 flex-shrink-0 transition-transform duration-300 motion-reduce:transition-none">
                     {openFAQ === index ? (
                       <Minus className="text-muted-foreground h-5 w-5" />
                     ) : (
                       <Plus className="text-muted-foreground h-5 w-5" />
                     )}
-                  </div>
-                </button>
+                    </div>
+                  </button>
+                </h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/sections/faq-section.tsx` around lines 52 - 60, The current markup
places <h3> inside the <button>, which violates HTML5 and a11y rules; refactor
the JSX so the heading wraps the button: change the structure from
<button>...<h3>{faq.question}</h3>...</button> to <h3
id={`faq-question-${index}`}><button type="button" onClick={() =>
toggleFAQ(index)} aria-expanded={openFAQ === index}
aria-controls={`faq-answer-${index}`}>{faq.question}</button></h3>, preserving
classes (move "flex w-full cursor-pointer items-center justify-between p-6" to
the button or adjust as needed) and keep the existing toggleFAQ, openFAQ, and
id/aria-controls/aria-expanded attributes unchanged.

<div className="ml-4 flex-shrink-0 transition-transform duration-300">
<div className="ml-4 flex-shrink-0 transition-transform duration-300 motion-reduce:transition-none">
{openFAQ === index ? (
<Minus className="text-muted-foreground h-5 w-5" />
) : (
<Plus className="text-muted-foreground h-5 w-5" />
)}
</div>
</div>
</button>

<div
className={`overflow-hidden transition-all duration-500 ease-in-out ${
openFAQ === index ? 'mt-4 max-h-96 opacity-100' : 'max-h-0 opacity-0'
<section
id={`faq-answer-${index}`}
aria-labelledby={`faq-question-${index}`}
aria-hidden={openFAQ !== index}
className={`overflow-hidden px-6 transition-all duration-500 ease-in-out motion-reduce:transition-none ${
openFAQ === index ? 'max-h-96 pb-6 opacity-100' : 'max-h-0 opacity-0'
}`}
>
<p className="text-muted-foreground text-left leading-relaxed">{faq.answer}</p>
</div>
</section>
</CardContent>
</Card>
))}
Expand Down
16 changes: 12 additions & 4 deletions components/sections/hero-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export function HeroSection({ handleJoinWithUs, handleViewShowcase }: HeroSectio
setSubtitleVisible(false)

const words = [...titleLine1, ...titleLine2]
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches

if (prefersReducedMotion) {
setAnimatedWords(words.map((_, i) => i))
setSubtitleVisible(true)
return
}

const timers: ReturnType<typeof setTimeout>[] = []

words.forEach((_word, index) => {
Expand Down Expand Up @@ -91,7 +99,7 @@ export function HeroSection({ handleJoinWithUs, handleViewShowcase }: HeroSectio
href="/blog"
className="inline-block cursor-pointer transition-transform duration-200 hover:scale-105"
>
<AnimatedGradientText className="transition-all duration-300 hover:shadow-[inset_0_-5px_10px_hsl(var(--foreground)/0.15)]">
<AnimatedGradientText className="transition duration-300 hover:shadow-[inset_0_-5px_10px_hsl(var(--foreground)/0.15)] motion-reduce:transition-none">
<span
className={cn(
'animate-gradient inline bg-gradient-to-r from-foreground via-foreground/70 to-foreground bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent dark:from-primary dark:via-primary/80 dark:to-primary',
Expand All @@ -108,7 +116,7 @@ export function HeroSection({ handleJoinWithUs, handleViewShowcase }: HeroSectio
<span
key={item.key}
className={cn(
'mr-2 inline-block transition-all duration-700 ease-out sm:mr-3',
'mr-2 inline-block transition duration-700 ease-out sm:mr-3 motion-reduce:transition-none',
animatedWords.includes(item.index)
? 'blur-0 translate-y-0 opacity-100'
: 'translate-y-8 opacity-0 blur-sm',
Expand All @@ -122,7 +130,7 @@ export function HeroSection({ handleJoinWithUs, handleViewShowcase }: HeroSectio
<span
key={item.key}
className={cn(
'mr-2 inline-block transition-all duration-700 ease-out sm:mr-3',
'mr-2 inline-block transition duration-700 ease-out sm:mr-3 motion-reduce:transition-none',
animatedWords.includes(item.index + titleLine1.length)
? 'blur-0 translate-y-0 opacity-100'
: 'translate-y-8 opacity-0 blur-sm',
Expand All @@ -135,7 +143,7 @@ export function HeroSection({ handleJoinWithUs, handleViewShowcase }: HeroSectio

<p
className={cn(
'text-muted-foreground mx-auto max-w-2xl text-center text-lg leading-relaxed transition-all duration-700 ease-out md:text-xl',
'text-muted-foreground mx-auto max-w-2xl text-center text-lg leading-relaxed transition duration-700 ease-out md:text-xl motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100 motion-reduce:blur-0',
subtitleVisible ? 'blur-0 translate-y-0 opacity-100' : 'translate-y-8 opacity-0 blur-sm',
)}
>
Expand Down
Loading
Loading