-
Notifications
You must be signed in to change notification settings - Fork 0
Wix-style section picker + demo content seed + project card SVG fix #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f5f5f8
7f4642e
2559a1d
964a771
04c6da9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,6 +221,14 @@ function getTrackingCode() { | |
| } catch (e) { return ''; } | ||
| } | ||
|
|
||
| function getCustomCss() { | ||
| try { | ||
| const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('customCss'); | ||
| // Strip any </style> to prevent breaking out of the injected style tag | ||
| return (setting?.value || '').replace(/<\/style>/gi, ''); | ||
| } catch (e) { return ''; } | ||
| } | ||
|
|
||
| function servePublicIndex(req, res) { | ||
| try { | ||
| // Check if a default dataset exists — serve from it instead of live DB | ||
|
|
@@ -256,6 +264,10 @@ function servePublicIndex(req, res) { | |
| // Inject default dataset slug (no DATASET_PREVIEW = no preview banner) | ||
| const datasetScript = `<script>window.DATASET_SLUG = "${defaultDataset.slug}";</script>`; | ||
| html = html.replace('</head>', `${datasetScript}</head>`); | ||
|
|
||
| // Inject custom CSS | ||
| const customCss = getCustomCss(); | ||
| if (customCss) html = html.replace('</head>', `<style id="custom-css">${customCss}</style></head>`); | ||
|
|
||
| return res.type('html').send(html); | ||
| } | ||
|
|
@@ -283,7 +295,11 @@ function servePublicIndex(req, res) { | |
| if (trackingCode) { | ||
| html = html.replace('<head>', `<head>\n${trackingCode}`); | ||
| } | ||
|
|
||
|
|
||
| // Inject custom CSS | ||
| const customCss = getCustomCss(); | ||
| if (customCss) html = html.replace('</head>', `<style id="custom-css">${customCss}</style></head>`); | ||
|
|
||
| res.type('html').send(html); | ||
| } catch (err) { res.sendFile(path.join(__dirname, '../public-readonly/index.html')); } | ||
| } | ||
|
|
@@ -321,6 +337,10 @@ function serveDatasetPage(req, res) { | |
| if (trackingCode) { | ||
| html = html.replace('<head>', `<head>\n${trackingCode}`); | ||
| } | ||
|
|
||
| // Inject custom CSS | ||
| const customCss = getCustomCss(); | ||
| if (customCss) html = html.replace('</head>', `<style id="custom-css">${customCss}</style></head>`); | ||
|
|
||
| res.type('html').send(html); | ||
| } catch (err) { | ||
|
|
@@ -770,7 +790,166 @@ if (!PUBLIC_ONLY) { | |
| db.prepare('INSERT OR IGNORE INTO pages (slug, title, sort_order) VALUES (?, ?, ?)').run(p.slug, p.title, p.sort_order); | ||
| }); | ||
|
|
||
| // Step 4: Auto-create "Default" dataset from live DB if no default exists | ||
| // Seed example blocks into default pages if none exist yet (fresh install demo content) | ||
| const blockCount = db.prepare('SELECT COUNT(*) as n FROM blocks').get().n; | ||
| if (blockCount === 0) { | ||
|
Comment on lines
+794
to
+795
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The startup seed runs whenever Useful? React with 👍 / 👎. |
||
| const insertBlock = db.prepare( | ||
| 'INSERT INTO blocks (page_slug, type, content, sort_order, enabled) VALUES (?, ?, ?, ?, 1)' | ||
| ); | ||
|
|
||
| // ── Home page ────────────────────────────────────────────────────────── | ||
| insertBlock.run('home', 'hero', JSON.stringify({ | ||
| title: "Hi, I'm Alex Rivera", | ||
| subtitle: "Full-stack developer building fast, accessible web experiences. Open to new opportunities.", | ||
| primary_button_text: "See My Work", | ||
| primary_button_link: "/projects", | ||
| background_image: "" | ||
| }), 0); | ||
|
|
||
| insertBlock.run('home', 'timeline', JSON.stringify({ | ||
| title: "Recent Experience", | ||
| items: [ | ||
| { | ||
| role: "Senior Frontend Engineer", | ||
| company: "Vercel", | ||
| start_date: "2022-03", | ||
| end_date: "", | ||
| description: "Leading UI performance initiatives across the dashboard, reducing Time-to-Interactive by 40%. Championing design-system adoption across 6 product teams." | ||
| }, | ||
| { | ||
| role: "Full-Stack Developer", | ||
| company: "Stripe", | ||
| start_date: "2019-07", | ||
| end_date: "2022-02", | ||
| description: "Built internal tooling and merchant-facing integrations in React and Go. Owned the invoicing module end-to-end." | ||
| } | ||
| ] | ||
| }), 1); | ||
|
|
||
| insertBlock.run('home', 'contact', JSON.stringify({ | ||
| email: "alex@example.com", | ||
| github: "https://github.com/alexrivera", | ||
| linkedin: "https://linkedin.com/in/alexrivera", | ||
| website: "https://alexrivera.dev" | ||
| }), 2); | ||
|
|
||
| // ── Projects page ────────────────────────────────────────────────────── | ||
| insertBlock.run('projects', 'hero', JSON.stringify({ | ||
| title: "Projects", | ||
| subtitle: "A selection of things I've built — from open-source tools to production apps.", | ||
| primary_button_text: "", | ||
| primary_button_link: "", | ||
| background_image: "" | ||
| }), 0); | ||
|
|
||
| insertBlock.run('projects', 'projects_grid', JSON.stringify({ | ||
| title: "", | ||
| projects: [ | ||
| { | ||
| name: "Logship", | ||
| description: "Real-time log aggregation pipeline handling 50k events/s. Built with Go, Kafka, and a React dashboard.", | ||
| link: "https://github.com/alexrivera/logship", | ||
| tech_stack: ["Go", "Kafka", "React", "PostgreSQL"], | ||
| image: "" | ||
| }, | ||
| { | ||
| name: "FormKit Pro", | ||
| description: "Headless form-builder library for React with schema-driven validation, conditional fields, and multi-step flows.", | ||
| link: "https://github.com/alexrivera/formkit-pro", | ||
| tech_stack: ["TypeScript", "React", "Zod"], | ||
| image: "" | ||
| }, | ||
| { | ||
| name: "PocketCV", | ||
| description: "Self-hosted resume manager (this very app!). Generates a clean public CV page from a SQLite database.", | ||
| link: "https://github.com/alexrivera/pocketcv", | ||
| tech_stack: ["Node.js", "Express", "SQLite", "Tailwind"], | ||
| image: "" | ||
| }, | ||
| { | ||
| name: "Dispatch", | ||
| description: "Lightweight webhook relay with retries, failure queues, and a simple dashboard for monitoring delivery status.", | ||
| link: "https://github.com/alexrivera/dispatch", | ||
| tech_stack: ["Go", "Redis", "Docker"], | ||
| image: "" | ||
| }, | ||
| { | ||
| name: "Inkdrop Theme Pack", | ||
| description: "Collection of 12 high-contrast and low-contrast editor themes for the Inkdrop note-taking app. 2k+ installs.", | ||
| link: "https://github.com/alexrivera/inkdrop-themes", | ||
| tech_stack: ["CSS"], | ||
| image: "" | ||
| }, | ||
| { | ||
| name: "OSS Contributions", | ||
| description: "Regular contributor to Next.js docs, Radix UI accessibility layer, and the Remix router. View all on GitHub.", | ||
| link: "https://github.com/alexrivera", | ||
| tech_stack: ["Various"], | ||
| image: "" | ||
| } | ||
| ] | ||
| }), 1); | ||
|
|
||
| // ── CV page ──────────────────────────────────────────────────────────── | ||
| insertBlock.run('cv', 'hero', JSON.stringify({ | ||
| title: "Curriculum Vitae", | ||
| subtitle: "Alex Rivera · Full-Stack Engineer · San Francisco, CA", | ||
| primary_button_text: "Download PDF", | ||
| primary_button_link: "#", | ||
| background_image: "" | ||
| }), 0); | ||
|
|
||
| insertBlock.run('cv', 'timeline', JSON.stringify({ | ||
| title: "Work Experience", | ||
| items: [ | ||
| { | ||
| role: "Senior Frontend Engineer", | ||
| company: "Vercel", | ||
| start_date: "2022-03", | ||
| end_date: "", | ||
| description: "Leading UI performance initiatives across the dashboard. Reduced Time-to-Interactive by 40% through code-splitting and streaming SSR. Mentoring 3 junior engineers." | ||
| }, | ||
| { | ||
| role: "Full-Stack Developer", | ||
| company: "Stripe", | ||
| start_date: "2019-07", | ||
| end_date: "2022-02", | ||
| description: "Built internal tooling and merchant-facing integrations in React and Go. Owned the invoicing module: design, implementation, testing, and on-call." | ||
| }, | ||
| { | ||
| role: "Software Engineer", | ||
| company: "Shopify", | ||
| start_date: "2017-06", | ||
| end_date: "2019-06", | ||
| description: "Worked on the Storefront APIs team, expanding GraphQL schema coverage and improving SDK developer experience." | ||
| } | ||
| ] | ||
| }), 1); | ||
|
|
||
| insertBlock.run('cv', 'timeline', JSON.stringify({ | ||
| title: "Education", | ||
| items: [ | ||
| { | ||
| role: "B.Sc. Computer Science", | ||
| company: "University of Waterloo", | ||
| start_date: "2013-09", | ||
| end_date: "2017-04", | ||
| description: "Specialization in Distributed Systems. Dean's Honor List. Capstone: real-time collaborative code editor (WebRTC + CRDT)." | ||
| } | ||
| ] | ||
| }), 2); | ||
|
|
||
| // ── Contacts page ────────────────────────────────────────────────────── | ||
| insertBlock.run('contacts', 'contact', JSON.stringify({ | ||
| email: "alex@example.com", | ||
| github: "https://github.com/alexrivera", | ||
| linkedin: "https://linkedin.com/in/alexrivera", | ||
| website: "https://alexrivera.dev" | ||
| }), 0); | ||
|
|
||
| console.log('Seeded example content into default pages'); | ||
| } | ||
|
|
||
| // Runs AFTER Step 3 so that profile and section_visibility rows are guaranteed to exist. | ||
| // Creates a Default dataset on every install (fresh or existing) so the Open modal is never empty. | ||
| try { | ||
|
|
@@ -2442,6 +2621,31 @@ if (PUBLIC_ONLY) { | |
| } catch (err) { res.status(500).json({ error: err.message }); } | ||
| }); | ||
|
|
||
| // Import a .json file as a named profile — does NOT overwrite live CV data. | ||
| // The file is read into memory, parsed, and stored as a dataset entry. The file itself is not kept. | ||
| app.post('/api/datasets/from-json', managerApiRateLimiter, uploadRateLimiter, jsonImportUpload.single('file'), (req, res) => { | ||
| if (!req.file) return res.status(400).json({ error: 'No JSON file uploaded.' }); | ||
| const name = (req.body.name || '').trim(); | ||
| if (!name) return res.status(400).json({ error: 'Profile name is required.' }); | ||
| let data; | ||
| try { data = JSON.parse(req.file.buffer.toString('utf8')); } | ||
| catch (e) { return res.status(400).json({ error: 'Invalid JSON file. The file could not be parsed.' }); } | ||
| if (!data || typeof data !== 'object' || Array.isArray(data)) return res.status(400).json({ error: 'Invalid CV JSON format.' }); | ||
| data = normalizeCVImportData(data); | ||
| try { | ||
| const existing = db.prepare('SELECT id FROM saved_datasets WHERE name = ?').get(name); | ||
| if (existing) { | ||
| db.prepare('UPDATE saved_datasets SET data = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(data), existing.id); | ||
| res.json({ success: true, id: existing.id, updated: true }); | ||
| } else { | ||
| const result = db.prepare('INSERT INTO saved_datasets (name, data) VALUES (?, ?)').run(name, JSON.stringify(data)); | ||
| const newId = result.lastInsertRowid; | ||
| try { const slug = generateSlug(name, newId); db.prepare('UPDATE saved_datasets SET slug = ? WHERE id = ?').run(slug, newId); } catch (e) {} | ||
| res.json({ success: true, id: newId, created: true }); | ||
| } | ||
| } catch (err) { res.status(500).json({ error: err.message }); } | ||
| }); | ||
|
|
||
| // ── Website Manager API routes ────────────────────────────────────────── | ||
|
|
||
| // Rate limiter for all website manager API routes (300 req/min per IP) | ||
|
|
@@ -2454,7 +2658,7 @@ if (PUBLIC_ONLY) { | |
| else { managerApiRateLimit[ip].count++; if (managerApiRateLimit[ip].count > 300) return res.status(429).json({ error: 'Too many requests' }); } | ||
| next(); | ||
| } | ||
| app.use(['/auth/me', '/auth/login', '/auth/logout', '/api/site', '/api/pages', '/api/blocks', '/api/uploads', '/api/media', '/api/uptime', '/manager'], managerApiRateLimiter); | ||
| app.use(['/auth/me', '/auth/login', '/auth/logout', '/api/site', '/api/custom-css', '/api/pages', '/api/blocks', '/api/uploads', '/api/media', '/api/uptime', '/manager'], managerApiRateLimiter); | ||
|
|
||
| // Auth JSON endpoints (used by manager UI) | ||
| app.get('/auth/me', (req, res) => { | ||
|
|
@@ -2509,6 +2713,21 @@ if (PUBLIC_ONLY) { | |
| } catch (err) { res.status(500).json({ error: err.message }); } | ||
| }); | ||
|
|
||
| // Custom CSS — stored in settings table and injected into the public CV page on every request | ||
| app.get('/api/custom-css', managerApiRateLimiter, requireAuth, (req, res) => { | ||
| try { | ||
| const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('customCss'); | ||
| res.json({ css: setting?.value || '' }); | ||
| } catch (err) { res.status(500).json({ error: err.message }); } | ||
| }); | ||
| app.put('/api/custom-css', managerApiRateLimiter, requireAuth, (req, res) => { | ||
| try { | ||
| const css = typeof req.body.css === 'string' ? req.body.css : ''; | ||
| db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run('customCss', css); | ||
| res.json({ success: true }); | ||
| } catch (err) { res.status(500).json({ error: err.message }); } | ||
| }); | ||
|
|
||
| // Pages | ||
| app.get('/api/pages', managerApiRateLimiter, requireAuth, (req, res) => { | ||
| try { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| { | ||
| "version": "1.16.3", | ||
| "version": "1.17.0", | ||
| "changelog": "https://github.com/Aedankerr/site-manager/blob/main/CHANGELOG.md" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new custom-CSS injection is implemented inside
servePublicIndex, but the public app still serves/,/projects, and/contactsvia directres.sendFile(...)handlers, so these pages never pass through this injection path. In practice, saved CSS only affects routes that useserveDatasetPage(for example/v/:slug), leaving the main public site unchanged.Useful? React with 👍 / 👎.