|
1 | | -import React from 'react'; |
2 | | -import { LucideIcon, ExternalLink } from 'lucide-react'; |
| 1 | +import React, { CSSProperties, ReactNode, forwardRef } from 'react'; |
3 | 2 | import Link from 'next/link'; |
4 | 3 |
|
5 | 4 | interface FeatureCardProps { |
6 | | - icon: LucideIcon; |
7 | | - title: string; |
8 | | - description: string; |
| 5 | + title: ReactNode; |
| 6 | + description: ReactNode; |
| 7 | + ctaHref: string; |
| 8 | + ctaText: string; |
| 9 | + media: ReactNode; |
9 | 10 | className?: string; |
10 | | - link?: string; |
| 11 | + isSlideChanging?: boolean; |
| 12 | + leftWrapperStyle?: CSSProperties; |
| 13 | + leftContentStyle?: CSSProperties; |
| 14 | + leftContentRef?: React.Ref<HTMLDivElement>; |
| 15 | + children?: ReactNode; |
11 | 16 | } |
12 | 17 |
|
13 | | -const DEFAULT_DOCS_URL = 'https://opsimate.vercel.app/docs/'; |
14 | | - |
15 | | -const FeatureCard: React.FC<FeatureCardProps> = ({ |
16 | | - icon: Icon, |
| 18 | +const FeatureCard = forwardRef<HTMLDivElement, FeatureCardProps>(({ |
17 | 19 | title, |
18 | 20 | description, |
| 21 | + ctaHref, |
| 22 | + ctaText, |
| 23 | + media, |
19 | 24 | className = '', |
20 | | - link, |
21 | | -}) => { |
22 | | - const resolvedLink = link ?? DEFAULT_DOCS_URL; |
| 25 | + isSlideChanging = false, |
| 26 | + leftWrapperStyle, |
| 27 | + leftContentStyle, |
| 28 | + leftContentRef, |
| 29 | + children, |
| 30 | +}, ref) => { |
| 31 | + const isExternal = /^https?:\/\//.test(ctaHref); |
23 | 32 |
|
24 | 33 | return ( |
25 | | - <div className={`feature-card group hover:scale-105 transition-all duration-300 ${className}`}> |
26 | | - <div className="flex items-center mb-3"> |
27 | | - <div className="bg-surface-200 dark:bg-surface-700 p-2 rounded-lg group-hover:bg-surface-300 dark:group-hover:bg-surface-600 transition-colors duration-300"> |
28 | | - <Icon className="h-5 w-5 text-surface-600 dark:text-surface-400" /> |
| 34 | + <div |
| 35 | + ref={ref} |
| 36 | + className={`relative isolate overflow-hidden border-2 border-surface-900 dark:border-white/20 bg-[#fdfbf7] dark:bg-surface-900 text-surface-900 dark:text-surface-50 shadow-[12px_12px_0_rgba(15,15,15,0.08)] p-6 md:p-10 lg:p-14 pb-28 md:pb-32 md:h-[520px] lg:h-[560px] ${className}`} |
| 37 | + > |
| 38 | + <div |
| 39 | + aria-hidden |
| 40 | + className="pointer-events-none absolute inset-0 opacity-30 mix-blend-multiply" |
| 41 | + style={{ |
| 42 | + backgroundImage: 'linear-gradient(90deg, rgba(15,15,15,0.08) 1px, transparent 1px), linear-gradient(rgba(15,15,15,0.08) 1px, transparent 1px)', |
| 43 | + backgroundSize: '48px 48px', |
| 44 | + }} |
| 45 | + /> |
| 46 | + <div className="absolute -right-16 -top-16 h-32 w-32 border-2 border-surface-900 dark:border-white/30 bg-[#0a5ad4] opacity-30 rotate-[25deg]" aria-hidden /> |
| 47 | + <div className="relative grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-14 items-start"> |
| 48 | + <div className="lg:col-span-5 overflow-hidden"> |
| 49 | + <div className="flex items-center gap-4 text-[11px] tracking-[0.45em] uppercase text-[#0a5ad4] dark:text-[#7cc6ff]"> |
| 50 | + <span className="h-px flex-1 bg-[#0a5ad4]/60 dark:bg-[#7cc6ff]/60" aria-hidden /> |
| 51 | + <span>O P S I M A T E</span> |
| 52 | + </div> |
| 53 | + <div |
| 54 | + className={`mt-6 transition-all duration-500 will-change-transform ease-[cubic-bezier(0.22,0.61,0.36,1)] ${isSlideChanging ? 'opacity-0 -translate-x-8 blur-sm' : 'opacity-100 translate-x-0 blur-0'}`} |
| 55 | + style={leftWrapperStyle} |
| 56 | + > |
| 57 | + <div |
| 58 | + ref={leftContentRef} |
| 59 | + style={leftContentStyle} |
| 60 | + > |
| 61 | + <h2 className="text-[32px] sm:text-5xl md:text-6xl font-black font-sans uppercase tracking-[0.08em] text-surface-900 dark:text-white leading-[1.05]"> |
| 62 | + {title} |
| 63 | + </h2> |
| 64 | + <p className="mt-6 text-base sm:text-lg leading-relaxed tracking-wide text-surface-600 dark:text-surface-200 max-w-md font-source-sans"> |
| 65 | + <span className="inline">{description}</span> |
| 66 | + <Link |
| 67 | + href={ctaHref} |
| 68 | + className="ml-3 inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.3em] font-semibold text-[#0a5ad4] dark:text-[#7cc6ff] border-b-2 border-current pb-0.5" |
| 69 | + {...(isExternal && { target: '_blank', rel: 'noopener noreferrer' })} |
| 70 | + > |
| 71 | + <span>{ctaText}</span> |
| 72 | + <span aria-hidden className="text-sm leading-none">↗</span> |
| 73 | + </Link> |
| 74 | + </p> |
| 75 | + </div> |
| 76 | + </div> |
| 77 | + </div> |
| 78 | + |
| 79 | + <div className="lg:col-span-7 hidden lg:block"> |
| 80 | + {media} |
29 | 81 | </div> |
30 | 82 | </div> |
31 | | - <h3 className="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">{title}</h3> |
32 | | - <p className="text-sm text-surface-600 dark:text-surface-400 leading-relaxed">{description}</p> |
33 | 83 |
|
34 | | - {resolvedLink && ( |
35 | | - <Link |
36 | | - href={resolvedLink} |
37 | | - className="mt-3 inline-flex items-center gap-1 text-blue-600 hover:text-blue-700 text-sm font-medium transition-colors duration-200" |
38 | | - {...(resolvedLink.startsWith('https') && { |
39 | | - target: "_blank", |
40 | | - rel: "noopener noreferrer" |
41 | | - })} |
42 | | - > |
43 | | - Learn more |
44 | | - {resolvedLink.startsWith('https') && <ExternalLink className="h-3 w-3" />} |
45 | | - </Link> |
46 | | - )} |
| 84 | + {children} |
47 | 85 | </div> |
48 | 86 | ); |
49 | | -}; |
| 87 | +}); |
| 88 | + |
| 89 | +FeatureCard.displayName = 'FeatureCard'; |
50 | 90 |
|
51 | 91 | export default FeatureCard; |
0 commit comments