Files
supabase/apps/learn/components/chapter-completion.tsx
Terry Sutton dda0b526ac Feat/learn (#41566)
wip

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

# Release Notes

* **New Features**
* Added a new Learn application offering foundational Supabase courses
with interactive documentation
* Courses include Architecture, Authentication, Data Fundamentals,
Security, Storage, Realtime, and Edge Functions
  * Chapter tracking and progress indicators for course completions
  * Responsive sidebar navigation with search/command menu
  * Theme switching support (light, dark, classic dark modes)
  * Mobile-friendly course interface

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 21:36:24 -03:30

148 lines
4.5 KiB
TypeScript

'use client'
import { Check } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useLocalStorage } from './use-local-storage'
interface ChapterCompletionProps {
chapterNumber: number
completionMessage?: string
}
export function ChapterCompletion({ chapterNumber, completionMessage }: ChapterCompletionProps) {
const [completedChapters, setCompletedChapters] = useLocalStorage<number[]>(
'completed-chapters',
[]
)
const [isCompleted, setIsCompleted] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const timerRef = useRef<NodeJS.Timeout | null>(null)
// Check if chapter is already completed on mount
useEffect(() => {
if (completedChapters.includes(chapterNumber)) {
setIsCompleted(true)
}
}, [chapterNumber, completedChapters])
useEffect(() => {
const container = containerRef.current
if (!container) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true)
} else {
setIsVisible(false)
// Reset timer if user scrolls away
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}
})
},
{
threshold: 0.5, // Trigger when 50% of the component is visible
rootMargin: '0px',
}
)
observer.observe(container)
return () => {
observer.disconnect()
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [])
useEffect(() => {
if (isVisible && !isCompleted) {
// Start timer when component becomes visible
timerRef.current = setTimeout(() => {
setIsCompleted(true)
// Save to local storage
if (!completedChapters.includes(chapterNumber)) {
setCompletedChapters([...completedChapters, chapterNumber])
}
}, 5000) // 5 seconds
} else if (!isVisible && timerRef.current) {
// Clear timer if user scrolls away before completion
clearTimeout(timerRef.current)
timerRef.current = null
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [isVisible, isCompleted, chapterNumber, completedChapters, setCompletedChapters])
return (
<div ref={containerRef} className="mb-16 mt-8">
<div className="flex items-center gap-12">
{/* Large circle with chapter number */}
<div className="relative mb-6">
<div
className={`w-24 h-24 rounded-full flex items-center justify-center transition-all duration-500 ${
isCompleted ? 'bg-green-50' : 'bg-muted'
}`}
>
<span
className={`text-4xl font-bold transition-all duration-500 ${
isCompleted ? 'text-brand-500' : 'text-foreground-muted'
}`}
>
{chapterNumber}
</span>
</div>
{/* Small checkmark circle overlapping bottom-right */}
<div
className={`absolute -bottom-1 -right-1 w-8 h-8 rounded-full flex items-center justify-center shadow-md transition-all duration-500 ${
isCompleted ? 'bg-brand-500 scale-100 opacity-100' : 'bg-muted scale-75 opacity-0'
}`}
>
<Check
className={`h-6 w-6 text-white transition-all duration-300 ${
isCompleted ? 'scale-100 opacity-100' : 'scale-0 opacity-0'
}`}
strokeWidth={3}
/>
</div>
</div>
<div>
{/* Completion text */}
<h3
className={`text-2xl font-bold mb-2 transition-all duration-500 ${
isCompleted
? 'text-foreground opacity-100 translate-y-0'
: 'text-foreground-muted opacity-60 translate-y-2'
}`}
>
You&apos;ve completed Chapter {chapterNumber}
</h3>
{completionMessage && (
<p
className={`text-base max-w-2xl transition-all duration-500 delay-100 ${
isCompleted
? 'text-foreground-light opacity-100 translate-y-0'
: 'text-foreground-light opacity-0 translate-y-2'
}`}
>
{completionMessage}
</p>
)}
</div>
</div>
</div>
)
}