website-components.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. "use client";
  2. import type { BaseComponentProps } from "@json-render/react";
  3. import type { WebsiteProps } from "./website-catalog";
  4. // =============================================================================
  5. // Header
  6. // =============================================================================
  7. function HeaderComponent({
  8. props,
  9. }: BaseComponentProps<WebsiteProps<"Header">>) {
  10. const variant = props.variant ?? "simple";
  11. const links = props.links ?? [];
  12. if (variant === "centered") {
  13. return (
  14. <header className="border-b border-border bg-background">
  15. <div className="mx-auto max-w-6xl px-6 py-6 flex flex-col items-center gap-4">
  16. <span className="text-xl font-bold tracking-tight">
  17. {props.brand}
  18. </span>
  19. <nav className="flex items-center gap-6">
  20. {links.map((link) => (
  21. <a
  22. key={link.href}
  23. href={link.href}
  24. className="text-sm text-muted-foreground hover:text-foreground transition-colors"
  25. >
  26. {link.label}
  27. </a>
  28. ))}
  29. </nav>
  30. </div>
  31. </header>
  32. );
  33. }
  34. return (
  35. <header className="border-b border-border bg-background sticky top-0 z-50">
  36. <div className="mx-auto max-w-6xl px-6 h-16 flex items-center justify-between">
  37. <span className="text-lg font-bold tracking-tight">{props.brand}</span>
  38. <nav className="flex items-center gap-6">
  39. {links.map((link) => (
  40. <a
  41. key={link.href}
  42. href={link.href}
  43. className="text-sm text-muted-foreground hover:text-foreground transition-colors"
  44. >
  45. {link.label}
  46. </a>
  47. ))}
  48. </nav>
  49. </div>
  50. </header>
  51. );
  52. }
  53. // =============================================================================
  54. // Hero
  55. // =============================================================================
  56. function HeroComponent({ props }: BaseComponentProps<WebsiteProps<"Hero">>) {
  57. const variant = props.variant ?? "centered";
  58. const isLeft = variant === "left-aligned";
  59. return (
  60. <section className="bg-background">
  61. <div
  62. className={`mx-auto max-w-4xl px-6 py-24 md:py-32 ${isLeft ? "text-left" : "text-center"}`}
  63. >
  64. <div
  65. className={`flex flex-col gap-6 ${isLeft ? "items-start" : "items-center"}`}
  66. >
  67. {props.badge && (
  68. <span className="inline-flex items-center rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground">
  69. {props.badge}
  70. </span>
  71. )}
  72. <h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl text-foreground">
  73. {props.headline}
  74. </h1>
  75. <p
  76. className={`text-lg text-muted-foreground ${isLeft ? "max-w-xl" : "max-w-2xl"}`}
  77. >
  78. {props.description}
  79. </p>
  80. {(props.primaryCta || props.secondaryCta) && (
  81. <div className="flex flex-wrap gap-3 pt-2">
  82. {props.primaryCta && (
  83. <a
  84. href={props.primaryCta.href}
  85. className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
  86. >
  87. {props.primaryCta.label}
  88. </a>
  89. )}
  90. {props.secondaryCta && (
  91. <a
  92. href={props.secondaryCta.href}
  93. className="inline-flex h-11 items-center justify-center rounded-md border border-border bg-background px-6 text-sm font-medium text-foreground hover:bg-muted transition-colors"
  94. >
  95. {props.secondaryCta.label}
  96. </a>
  97. )}
  98. </div>
  99. )}
  100. </div>
  101. </div>
  102. </section>
  103. );
  104. }
  105. // =============================================================================
  106. // Features
  107. // =============================================================================
  108. function FeaturesComponent({
  109. props,
  110. }: BaseComponentProps<WebsiteProps<"Features">>) {
  111. const variant = props.variant ?? "cards";
  112. const columns = props.columns ?? 3;
  113. const items = props.items ?? [];
  114. const colClass =
  115. columns === 2
  116. ? "sm:grid-cols-2"
  117. : columns === 4
  118. ? "sm:grid-cols-2 lg:grid-cols-4"
  119. : "sm:grid-cols-2 lg:grid-cols-3";
  120. return (
  121. <section className="bg-background">
  122. <div className="mx-auto max-w-6xl px-6 py-20">
  123. {(props.headline || props.description) && (
  124. <div className="mb-12 text-center">
  125. {props.headline && (
  126. <h2 className="text-3xl font-bold tracking-tight text-foreground">
  127. {props.headline}
  128. </h2>
  129. )}
  130. {props.description && (
  131. <p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
  132. {props.description}
  133. </p>
  134. )}
  135. </div>
  136. )}
  137. <div className={`grid grid-cols-1 gap-6 ${colClass}`}>
  138. {items.map((item, i) =>
  139. variant === "cards" ? (
  140. <div
  141. key={i}
  142. className="rounded-lg border border-border bg-card p-6 shadow-sm"
  143. >
  144. <h3 className="text-lg font-semibold text-card-foreground">
  145. {item.title}
  146. </h3>
  147. <p className="mt-2 text-sm text-muted-foreground">
  148. {item.description}
  149. </p>
  150. </div>
  151. ) : (
  152. <div key={i} className="py-2">
  153. <h3 className="text-lg font-semibold text-foreground">
  154. {item.title}
  155. </h3>
  156. <p className="mt-1 text-sm text-muted-foreground">
  157. {item.description}
  158. </p>
  159. </div>
  160. ),
  161. )}
  162. </div>
  163. </div>
  164. </section>
  165. );
  166. }
  167. // =============================================================================
  168. // Team
  169. // =============================================================================
  170. function TeamComponent({ props }: BaseComponentProps<WebsiteProps<"Team">>) {
  171. const variant = props.variant ?? "grid";
  172. const members = props.members ?? [];
  173. return (
  174. <section className="bg-background">
  175. <div className="mx-auto max-w-6xl px-6 py-20">
  176. {(props.headline || props.description) && (
  177. <div className="mb-12 text-center">
  178. {props.headline && (
  179. <h2 className="text-3xl font-bold tracking-tight text-foreground">
  180. {props.headline}
  181. </h2>
  182. )}
  183. {props.description && (
  184. <p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
  185. {props.description}
  186. </p>
  187. )}
  188. </div>
  189. )}
  190. {variant === "list" ? (
  191. <div className="space-y-6 max-w-2xl mx-auto">
  192. {members.map((m, i) => (
  193. <div key={i} className="flex flex-col gap-1">
  194. <h3 className="text-lg font-semibold text-foreground">
  195. {m.name}
  196. </h3>
  197. <p className="text-sm text-muted-foreground">{m.role}</p>
  198. {m.bio && (
  199. <p className="mt-1 text-sm text-muted-foreground">{m.bio}</p>
  200. )}
  201. </div>
  202. ))}
  203. </div>
  204. ) : (
  205. <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
  206. {members.map((m, i) => (
  207. <div
  208. key={i}
  209. className="rounded-lg border border-border bg-card p-6 text-center"
  210. >
  211. <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-bold text-muted-foreground">
  212. {m.name
  213. .split(" ")
  214. .map((n) => n[0])
  215. .join("")
  216. .slice(0, 2)
  217. .toUpperCase()}
  218. </div>
  219. <h3 className="text-base font-semibold text-card-foreground">
  220. {m.name}
  221. </h3>
  222. <p className="text-sm text-muted-foreground">{m.role}</p>
  223. {m.bio && (
  224. <p className="mt-2 text-sm text-muted-foreground">{m.bio}</p>
  225. )}
  226. </div>
  227. ))}
  228. </div>
  229. )}
  230. </div>
  231. </section>
  232. );
  233. }
  234. // =============================================================================
  235. // Testimonials
  236. // =============================================================================
  237. function TestimonialsComponent({
  238. props,
  239. }: BaseComponentProps<WebsiteProps<"Testimonials">>) {
  240. const items = props.items ?? [];
  241. return (
  242. <section className="bg-muted/30">
  243. <div className="mx-auto max-w-6xl px-6 py-20">
  244. {props.headline && (
  245. <h2 className="mb-12 text-center text-3xl font-bold tracking-tight text-foreground">
  246. {props.headline}
  247. </h2>
  248. )}
  249. <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
  250. {items.map((item, i) => (
  251. <blockquote
  252. key={i}
  253. className="rounded-lg border border-border bg-background p-6"
  254. >
  255. <p className="text-sm text-foreground leading-relaxed">
  256. &ldquo;{item.quote}&rdquo;
  257. </p>
  258. <footer className="mt-4 flex flex-col">
  259. <span className="text-sm font-semibold text-foreground">
  260. {item.author}
  261. </span>
  262. {item.role && (
  263. <span className="text-xs text-muted-foreground">
  264. {item.role}
  265. </span>
  266. )}
  267. </footer>
  268. </blockquote>
  269. ))}
  270. </div>
  271. </div>
  272. </section>
  273. );
  274. }
  275. // =============================================================================
  276. // ContactForm
  277. // =============================================================================
  278. function ContactFormComponent({
  279. props,
  280. }: BaseComponentProps<WebsiteProps<"ContactForm">>) {
  281. const fields = props.fields ?? [];
  282. return (
  283. <section className="bg-background">
  284. <div className="mx-auto max-w-xl px-6 py-20">
  285. {(props.headline || props.description) && (
  286. <div className="mb-8 text-center">
  287. {props.headline && (
  288. <h2 className="text-3xl font-bold tracking-tight text-foreground">
  289. {props.headline}
  290. </h2>
  291. )}
  292. {props.description && (
  293. <p className="mt-3 text-muted-foreground">{props.description}</p>
  294. )}
  295. </div>
  296. )}
  297. <form
  298. className="rounded-lg border border-border bg-card p-6 shadow-sm"
  299. onSubmit={(e) => e.preventDefault()}
  300. >
  301. <div className="flex flex-col gap-4">
  302. {fields.map((field, i) =>
  303. field.type === "textarea" ? (
  304. <div key={i} className="flex flex-col gap-1.5">
  305. <label className="text-sm font-medium text-foreground">
  306. {field.label}
  307. </label>
  308. <textarea
  309. placeholder={field.placeholder ?? ""}
  310. rows={4}
  311. className="rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
  312. />
  313. </div>
  314. ) : (
  315. <div key={i} className="flex flex-col gap-1.5">
  316. <label className="text-sm font-medium text-foreground">
  317. {field.label}
  318. </label>
  319. <input
  320. type={field.type}
  321. placeholder={field.placeholder ?? ""}
  322. className="h-10 rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
  323. />
  324. </div>
  325. ),
  326. )}
  327. <button
  328. type="submit"
  329. className="mt-2 h-11 rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
  330. >
  331. {props.submitLabel}
  332. </button>
  333. </div>
  334. </form>
  335. </div>
  336. </section>
  337. );
  338. }
  339. // =============================================================================
  340. // Footer
  341. // =============================================================================
  342. function FooterComponent({
  343. props,
  344. }: BaseComponentProps<WebsiteProps<"Footer">>) {
  345. const variant = props.variant ?? "simple";
  346. const links = props.links ?? [];
  347. if (variant === "columns") {
  348. return (
  349. <footer className="border-t border-border bg-muted/30">
  350. <div className="mx-auto max-w-6xl px-6 py-12">
  351. <div className="flex flex-col gap-8 sm:flex-row sm:justify-between">
  352. <div>
  353. {props.brand && (
  354. <span className="text-lg font-bold text-foreground">
  355. {props.brand}
  356. </span>
  357. )}
  358. </div>
  359. {links.length > 0 && (
  360. <nav className="flex flex-col gap-2 sm:flex-row sm:gap-6">
  361. {links.map((link) => (
  362. <a
  363. key={link.href}
  364. href={link.href}
  365. className="text-sm text-muted-foreground hover:text-foreground transition-colors"
  366. >
  367. {link.label}
  368. </a>
  369. ))}
  370. </nav>
  371. )}
  372. </div>
  373. {props.copyright && (
  374. <p className="mt-8 text-xs text-muted-foreground">
  375. {props.copyright}
  376. </p>
  377. )}
  378. </div>
  379. </footer>
  380. );
  381. }
  382. return (
  383. <footer className="border-t border-border bg-muted/30">
  384. <div className="mx-auto max-w-6xl px-6 py-10 flex flex-col items-center gap-4">
  385. {props.brand && (
  386. <span className="text-sm font-semibold text-foreground">
  387. {props.brand}
  388. </span>
  389. )}
  390. {links.length > 0 && (
  391. <nav className="flex items-center gap-4">
  392. {links.map((link) => (
  393. <a
  394. key={link.href}
  395. href={link.href}
  396. className="text-xs text-muted-foreground hover:text-foreground transition-colors"
  397. >
  398. {link.label}
  399. </a>
  400. ))}
  401. </nav>
  402. )}
  403. {props.copyright && (
  404. <p className="text-xs text-muted-foreground">{props.copyright}</p>
  405. )}
  406. </div>
  407. </footer>
  408. );
  409. }
  410. // =============================================================================
  411. // CTA
  412. // =============================================================================
  413. function CTAComponent({ props }: BaseComponentProps<WebsiteProps<"CTA">>) {
  414. const variant = props.variant ?? "centered";
  415. if (variant === "banner") {
  416. return (
  417. <section className="bg-primary text-primary-foreground">
  418. <div className="mx-auto max-w-6xl px-6 py-16 flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
  419. <div>
  420. <h2 className="text-2xl font-bold">{props.headline}</h2>
  421. {props.description && (
  422. <p className="mt-1 text-sm opacity-90">{props.description}</p>
  423. )}
  424. </div>
  425. <a
  426. href={props.buttonHref}
  427. className="inline-flex h-11 items-center justify-center rounded-md bg-background px-6 text-sm font-medium text-foreground shadow-sm hover:bg-background/90 transition-colors shrink-0"
  428. >
  429. {props.buttonLabel}
  430. </a>
  431. </div>
  432. </section>
  433. );
  434. }
  435. return (
  436. <section className="bg-muted/30">
  437. <div className="mx-auto max-w-3xl px-6 py-20 text-center">
  438. <h2 className="text-3xl font-bold tracking-tight text-foreground">
  439. {props.headline}
  440. </h2>
  441. {props.description && (
  442. <p className="mt-3 text-lg text-muted-foreground">
  443. {props.description}
  444. </p>
  445. )}
  446. <div className="mt-8">
  447. <a
  448. href={props.buttonHref}
  449. className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
  450. >
  451. {props.buttonLabel}
  452. </a>
  453. </div>
  454. </div>
  455. </section>
  456. );
  457. }
  458. // =============================================================================
  459. // Export map
  460. // =============================================================================
  461. export const websiteComponents = {
  462. Header: HeaderComponent,
  463. Hero: HeroComponent,
  464. Features: FeaturesComponent,
  465. Team: TeamComponent,
  466. Testimonials: TestimonialsComponent,
  467. ContactForm: ContactFormComponent,
  468. Footer: FooterComponent,
  469. CTA: CTAComponent,
  470. };