'use client'
import { motion } from 'framer-motion'
import { ExternalLink, MessageSquare, CheckCircle, XCircle, Loader2, AlertCircle } from 'lucide-react'
import { useState, useEffect } from 'react'
import { useTerminal } from '@/lib/TerminalContext'
function RepoCIBadge({ label, repo, forgejoBase }: { label: string; repo: string; forgejoBase: string }) {
const [ci, setCI] = useState<{ sha: string; status: string } | null>(null)
useEffect(() => {
fetch(`/api/ci-status?repo=${repo}`)
.then(r => r.json())
.then(data => setCI({
sha: data.sha?.substring(0, 7) || '',
status: data.conclusion || data.status || 'unknown',
}))
.catch(() => null)
}, [repo])
const icon = !ci ?
: ci.status === 'success' ?
: ci.status === 'failure' ?
:
return (
{label}
{ci?.sha ? (
{ci.sha}
) : (
···
)}
|
{icon}
)
}
interface CIRepo {
label: string
repo: string
forgejoBase: string
}
interface ProjectShowcaseProps {
id?: string
title: string
description: string
longDescription?: string
stat: string
highlight?: string
status: 'live' | 'building' | 'planning' | 'completed'
media?: {
type: 'image' | 'video'
url: string
}
videoUrl?: string
articleUrl?: string
watchVideoText?: string
readArticleText?: string
askPoimenText?: string
bullets?: string[]
deepDive?: {
label: string
url: string
}
ciRepos?: CIRepo[]
}
export function ProjectShowcase({
id,
title,
description,
longDescription,
stat,
highlight,
status,
media,
videoUrl,
articleUrl,
watchVideoText = 'Watch Video',
readArticleText = 'Read Article',
askPoimenText = 'Ask Poimen for technical details',
bullets,
deepDive,
ciRepos,
}: ProjectShowcaseProps) {
const { setIsOpen } = useTerminal()
const statusColors = {
live: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
completed: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
building: 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
planning: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
}
return (
{/* Optional Media (Image or Video) */}
{media && (
{media.type === 'video' ? (
(() => {
const youtubeMatch = media.url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)
if (youtubeMatch) {
return (
)
}
return null
})()
) : (

)}
)}
{/* Content */}
{title}
{description}
{longDescription && (
{longDescription}
)}
{bullets && bullets.length > 0 && (
{bullets.map((bullet, i) => {
// Highlight skills: parentheses + keywords
const skillKeywords = ['Terraform', 'Golang', 'K8s', 'Slack', 'Grafana', 'IaC', 'Docker', 'OpenShift', 'Temporal', 'Kafka', 'gRPC', 'AWS', 'Java', 'Python', 'Go', 'C++', 'CloudWatch', 'DynamoDB']
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const pattern = new RegExp(`(\\([^)]+\\)|${skillKeywords.map(escapeRegex).join('|')})`, 'g')
const parts = bullet.split(pattern).filter(Boolean)
return (
-
•
{parts.map((part, j) =>
(part.startsWith('(') && part.endsWith(')')) || skillKeywords.includes(part) ? (
{part}
) : (
{part}
)
)}
)
})}
)}
{deepDive && (
{deepDive.label}
)}
{ciRepos ? (
{ciRepos.map((r) => (
))}
) : (
{status.charAt(0).toUpperCase() + status.slice(1)}
)}
{/* Stat */}
📊 {stat}
{highlight && (
📊 {highlight}
)}
{/* CTA Buttons */}
{videoUrl && videoUrl !== '#' && !videoUrl.includes('youtube.com') && !videoUrl.includes('youtu.be') && (
{watchVideoText}
)}
{articleUrl && articleUrl !== '#' && (
{readArticleText}
)}
{/* Ask Poimen */}
)
}