From 6cd63f1bdf202afe3a4a7815e49af97aa7ed21c8 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 18 Mar 2026 22:29:51 +0000 Subject: [PATCH] Add Page Builder: 15 block types + publish/unpublish via TinaCMS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New TinaCMS collection 'pages' with 15 block templates: Hero, TextBlock, TwoColumn, Features, Stats, Testimonials, Team, FAQ, CTABanner, Video, Gallery, Pricing, Timeline, Divider, ContactForm - Each page has published toggle (unpublished → 404) - Route /p/:slug renders dynamic pages from content/pages/*.json - scripts/copy-pages.mjs copies content/pages → public/pages at build - prerender.mjs extended to prerender published pages - All blocks styled with design tokens + Framer Motion animations Co-Authored-By: Claude Sonnet 4.6 --- content/pages/.gitkeep | 0 package.json | 2 +- scripts/copy-pages.mjs | 22 ++ scripts/prerender.mjs | 16 +- src/App.tsx | 2 + src/components/blocks/BlockCTABanner.css | 78 ++++++ src/components/blocks/BlockCTABanner.tsx | 38 +++ src/components/blocks/BlockContactForm.tsx | 13 + src/components/blocks/BlockDivider.css | 25 ++ src/components/blocks/BlockDivider.tsx | 15 ++ src/components/blocks/BlockFAQ.css | 72 ++++++ src/components/blocks/BlockFAQ.tsx | 32 +++ src/components/blocks/BlockFeatures.css | 59 +++++ src/components/blocks/BlockFeatures.tsx | 39 +++ src/components/blocks/BlockGallery.css | 38 +++ src/components/blocks/BlockGallery.tsx | 29 +++ src/components/blocks/BlockHero.css | 61 +++++ src/components/blocks/BlockHero.tsx | 37 +++ src/components/blocks/BlockPricing.css | 102 ++++++++ src/components/blocks/BlockPricing.tsx | 58 +++++ src/components/blocks/BlockRenderer.tsx | 40 +++ src/components/blocks/BlockStats.css | 32 +++ src/components/blocks/BlockStats.tsx | 27 ++ src/components/blocks/BlockTeam.css | 71 ++++++ src/components/blocks/BlockTeam.tsx | 45 ++++ src/components/blocks/BlockTestimonials.css | 72 ++++++ src/components/blocks/BlockTestimonials.tsx | 46 ++++ src/components/blocks/BlockTextBlock.css | 80 ++++++ src/components/blocks/BlockTextBlock.tsx | 28 +++ src/components/blocks/BlockTimeline.css | 94 +++++++ src/components/blocks/BlockTimeline.tsx | 51 ++++ src/components/blocks/BlockTwoColumn.css | 82 +++++++ src/components/blocks/BlockTwoColumn.tsx | 50 ++++ src/components/blocks/BlockVideo.css | 36 +++ src/components/blocks/BlockVideo.tsx | 43 ++++ src/components/blocks/index.ts | 9 + src/pages/DynamicPage.css | 33 +++ src/pages/DynamicPage.tsx | 58 +++++ src/types/pages.ts | 96 ++++++++ tina/config.ts | 259 ++++++++++++++++++++ 40 files changed, 1988 insertions(+), 2 deletions(-) create mode 100644 content/pages/.gitkeep create mode 100644 scripts/copy-pages.mjs create mode 100644 src/components/blocks/BlockCTABanner.css create mode 100644 src/components/blocks/BlockCTABanner.tsx create mode 100644 src/components/blocks/BlockContactForm.tsx create mode 100644 src/components/blocks/BlockDivider.css create mode 100644 src/components/blocks/BlockDivider.tsx create mode 100644 src/components/blocks/BlockFAQ.css create mode 100644 src/components/blocks/BlockFAQ.tsx create mode 100644 src/components/blocks/BlockFeatures.css create mode 100644 src/components/blocks/BlockFeatures.tsx create mode 100644 src/components/blocks/BlockGallery.css create mode 100644 src/components/blocks/BlockGallery.tsx create mode 100644 src/components/blocks/BlockHero.css create mode 100644 src/components/blocks/BlockHero.tsx create mode 100644 src/components/blocks/BlockPricing.css create mode 100644 src/components/blocks/BlockPricing.tsx create mode 100644 src/components/blocks/BlockRenderer.tsx create mode 100644 src/components/blocks/BlockStats.css create mode 100644 src/components/blocks/BlockStats.tsx create mode 100644 src/components/blocks/BlockTeam.css create mode 100644 src/components/blocks/BlockTeam.tsx create mode 100644 src/components/blocks/BlockTestimonials.css create mode 100644 src/components/blocks/BlockTestimonials.tsx create mode 100644 src/components/blocks/BlockTextBlock.css create mode 100644 src/components/blocks/BlockTextBlock.tsx create mode 100644 src/components/blocks/BlockTimeline.css create mode 100644 src/components/blocks/BlockTimeline.tsx create mode 100644 src/components/blocks/BlockTwoColumn.css create mode 100644 src/components/blocks/BlockTwoColumn.tsx create mode 100644 src/components/blocks/BlockVideo.css create mode 100644 src/components/blocks/BlockVideo.tsx create mode 100644 src/components/blocks/index.ts create mode 100644 src/pages/DynamicPage.css create mode 100644 src/pages/DynamicPage.tsx create mode 100644 src/types/pages.ts diff --git a/content/pages/.gitkeep b/content/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index d7623a5..4dec62b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "tinacms dev -c \"vite\"", - "build": "tinacms build && node scripts/sync-blog.mjs && node scripts/generate-sitemap.mjs && tsc -b && vite build && node scripts/prerender.mjs", + "build": "tinacms build && node scripts/sync-blog.mjs && node scripts/copy-pages.mjs && node scripts/generate-sitemap.mjs && tsc -b && vite build && node scripts/prerender.mjs", "lint": "eslint .", "preview": "vite preview", "sync-blog": "node scripts/sync-blog.mjs", diff --git a/scripts/copy-pages.mjs b/scripts/copy-pages.mjs new file mode 100644 index 0000000..8038384 --- /dev/null +++ b/scripts/copy-pages.mjs @@ -0,0 +1,22 @@ +import { readdirSync, copyFileSync, mkdirSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '..'); +const src = resolve(root, 'content/pages'); +const dest = resolve(root, 'public/pages'); + +if (!existsSync(src)) { + console.log('No content/pages directory, skipping.'); + process.exit(0); +} + +mkdirSync(dest, { recursive: true }); + +const files = readdirSync(src).filter(f => f.endsWith('.json')); +for (const file of files) { + copyFileSync(resolve(src, file), resolve(dest, file)); + console.log(`Copied: ${file}`); +} +console.log(`Pages copy complete: ${files.length} file(s).`); diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs index 945c779..f0b50e3 100644 --- a/scripts/prerender.mjs +++ b/scripts/prerender.mjs @@ -5,7 +5,7 @@ * * Usage: node scripts/prerender.mjs */ -import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -20,6 +20,19 @@ function getBlogRoutes() { } catch { return []; } } +function getPageRoutes() { + try { + const pagesDir = resolve(root, 'public/pages'); + const files = readdirSync(pagesDir).filter(f => f.endsWith('.json')); + return files + .map(f => { + const data = JSON.parse(readFileSync(resolve(pagesDir, f), 'utf-8')); + return data.published ? `/p/${f.replace('.json', '')}` : null; + }) + .filter(Boolean); + } catch { return []; } +} + const routes = [ '/', '/about', @@ -29,6 +42,7 @@ const routes = [ '/privacy-policy', '/terms-of-use', ...getBlogRoutes(), + ...getPageRoutes(), ]; async function prerender() { diff --git a/src/App.tsx b/src/App.tsx index 01bbf10..bf13b69 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import ScrollToTop from './components/ScrollToTop'; import CookieConsent from './components/CookieConsent'; import React from 'react'; const ChatWidget = React.lazy(() => import('./components/ChatWidget')); +const DynamicPage = React.lazy(() => import('./pages/DynamicPage')); import HomePage from './pages/HomePage'; import BlogPage from './pages/BlogPage'; import BlogPostPage from './pages/BlogPostPage'; @@ -30,6 +31,7 @@ function App() { } /> } /> } /> + } />