From 5edb54cbb3d817f2625341e7d4ff15142644a7a2 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Wed, 17 Sep 2025 18:01:53 +0530 Subject: [PATCH 1/3] feat: add author and blog scripts --- package.json | 4 +- scripts/blog/create-author.js | 296 ++++++++++++++++++++++++++++ scripts/blog/create-blog.js | 349 ++++++++++++++++++++++++++++++++++ scripts/blog/utils.js | 302 +++++++++++++++++++++++++++++ 4 files changed, 950 insertions(+), 1 deletion(-) create mode 100644 scripts/blog/create-author.js create mode 100644 scripts/blog/create-blog.js create mode 100644 scripts/blog/utils.js diff --git a/package.json b/package.json index 2df14fd05d..28d149a080 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "test:integration": "playwright test", "test:unit": "vitest", "optimize": "node ./scripts/optimize-assets.js", - "optimize:all": "node ./scripts/optimize-all.js" + "optimize:all": "node ./scripts/optimize-all.js", + "create-blog": "node ./scripts/blog/create-blog.js", + "create-author": "node ./scripts/blog/create-author.js" }, "packageManager": "pnpm@10.15.1", "dependencies": { diff --git a/scripts/blog/create-author.js b/scripts/blog/create-author.js new file mode 100644 index 0000000000..894df5e281 --- /dev/null +++ b/scripts/blog/create-author.js @@ -0,0 +1,296 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { + COLORS, + printHeader, + question, + questionWithDefault, + selectFromList, + imagePathInput, + copyImage, + closeReadline +} from './utils.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.join(__dirname, '..', '..'); + +function printAuthorHeader() { + console.clear(); + console.log(`${COLORS.pink}${COLORS.bright}`); + console.log(' ___ _ __ '); + console.log(' / _ | ___ ___ _ ___ ____(_) /____ '); + console.log(' / __ |/ _ \\/ _ \\| |/|/ / __/ / / __/ -_)'); + console.log(' /_/ |_/ .__/ .__/|__,__/_/ /_/_/\\__/\\__/ '); + console.log(' /_/ /_/ '); + console.log(''); + console.log(' AUTHOR CREATOR'); + console.log(`${COLORS.reset}\n`); + console.log(`${COLORS.dim}Add a new author to the Appwrite blog${COLORS.reset}\n`); +} + +function slugify(text) { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/--+/g, '-') + .trim(); +} + +function validateSlug(slug) { + const authorPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'author', slug); + + if (fs.existsSync(authorPath)) { + return { + valid: false, + message: `An author with slug "${slug}" already exists!` + }; + } + + if (!/^[a-z0-9-]+$/.test(slug)) { + return { + valid: false, + message: 'Slug should only contain lowercase letters, numbers, and hyphens' + }; + } + + return { valid: true }; +} + +function validateUrl(url) { + if (!url) return true; // Optional field + try { + new URL(url); + return true; + } catch { + return false; + } +} + +async function collectAuthorInfo() { + const authorInfo = {}; + + console.log(`\n${COLORS.bright}👤 Author Information${COLORS.reset}\n`); + + // Name + let name = ''; + while (!name) { + name = await question('Full Name: '); + if (!name) { + console.log(`${COLORS.red}Name is required!${COLORS.reset}`); + } + } + authorInfo.name = name; + + // Auto-generate slug from name + const suggestedSlug = slugify(authorInfo.name); + let slug = await questionWithDefault('Slug', suggestedSlug); + + let validation = validateSlug(slug); + while (!validation.valid) { + console.log(`${COLORS.red}${validation.message}${COLORS.reset}`); + slug = await question('Please enter a different slug: '); + validation = validateSlug(slug); + } + authorInfo.slug = slug; + + // Role + console.log(`\n${COLORS.bright}💼 Professional Information${COLORS.reset}\n`); + + let role = ''; + while (!role) { + role = await question('Role (e.g., Developer Advocate, Software Engineer): '); + if (!role) { + console.log(`${COLORS.red}Role is required!${COLORS.reset}`); + } + } + authorInfo.role = role; + + // Bio + console.log(`\n${COLORS.bright}📝 Bio${COLORS.reset}`); + console.log(`${COLORS.dim}Write a short bio (1-2 sentences)${COLORS.reset}\n`); + const bio = await question('Bio: '); + + authorInfo.bio = bio || `${authorInfo.role} at Appwrite`; + + // Avatar + console.log(`\n${COLORS.bright}🖼️ Avatar${COLORS.reset}`); + const avatarFileName = `${authorInfo.slug}.png`; + const avatarPath = `/images/avatars/${avatarFileName}`; + const fullAvatarPath = path.join(ROOT_DIR, 'static', 'images', 'avatars', avatarFileName); + + const imageResult = await imagePathInput('Add Avatar Image', fullAvatarPath); + authorInfo.avatar = avatarPath; + authorInfo.avatarSourcePath = imageResult.sourcePath; + authorInfo.avatarTargetPath = imageResult.targetPath; + + // Social Links + console.log(`\n${COLORS.bright}🔗 Social Links${COLORS.reset}`); + console.log(`${COLORS.dim}Leave blank to skip any social link${COLORS.reset}\n`); + + // Twitter/X + let twitter = await question('Twitter/X URL (e.g., https://x.com/username): '); + while (twitter && twitter.trim() && !validateUrl(twitter)) { + console.log(`${COLORS.red}Invalid URL format${COLORS.reset}`); + twitter = await question('Twitter/X URL (leave blank to skip): '); + } + authorInfo.twitter = twitter ? twitter.trim() : ''; + + // GitHub + let github = await question('GitHub URL (e.g., https://github.com/username): '); + while (github && github.trim() && !validateUrl(github)) { + console.log(`${COLORS.red}Invalid URL format${COLORS.reset}`); + github = await question('GitHub URL (leave blank to skip): '); + } + authorInfo.github = github ? github.trim() : ''; + + // LinkedIn + let linkedin = await question('LinkedIn URL (e.g., https://www.linkedin.com/in/username): '); + while (linkedin && linkedin.trim() && !validateUrl(linkedin)) { + console.log(`${COLORS.red}Invalid URL format${COLORS.reset}`); + linkedin = await question('LinkedIn URL (leave blank to skip): '); + } + authorInfo.linkedin = linkedin ? linkedin.trim() : ''; + + return authorInfo; +} + +function generateAuthorMarkdoc(authorInfo) { + let content = `--- +layout: author +slug: ${authorInfo.slug} +name: ${authorInfo.name} +role: ${authorInfo.role} +bio: ${authorInfo.bio} +avatar: ${authorInfo.avatar}`; + + if (authorInfo.twitter) { + content += `\ntwitter: ${authorInfo.twitter}`; + } + + if (authorInfo.github) { + content += `\ngithub: ${authorInfo.github}`; + } + + if (authorInfo.linkedin) { + content += `\nlinkedin: ${authorInfo.linkedin}`; + } + + content += '\n---\n'; + + return content; +} + +async function createAuthorFiles(authorInfo) { + const authorPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'author', authorInfo.slug); + + console.log(`\n${COLORS.bright}📁 Creating author files...${COLORS.reset}\n`); + + try { + // Create author directory + fs.mkdirSync(authorPath, { recursive: true }); + console.log( + `${COLORS.pink}✓${COLORS.reset} Created directory: ${COLORS.dim}${authorPath}${COLORS.reset}` + ); + + // Create the markdoc file + const markdocPath = path.join(authorPath, '+page.markdoc'); + const content = generateAuthorMarkdoc(authorInfo); + fs.writeFileSync(markdocPath, content); + console.log( + `${COLORS.pink}✓${COLORS.reset} Created file: ${COLORS.dim}${markdocPath}${COLORS.reset}` + ); + + // Copy avatar image if provided + let avatarCopied = false; + if (authorInfo.avatarSourcePath && authorInfo.avatarTargetPath) { + avatarCopied = await copyImage(authorInfo.avatarSourcePath, authorInfo.avatarTargetPath); + } + + return { success: true, authorPath, markdocPath, avatarCopied }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +async function main() { + printAuthorHeader(); + + try { + const authorInfo = await collectAuthorInfo(); + + // Review information + console.log(`\n${COLORS.bright}📋 Review Author Information${COLORS.reset}\n`); + console.log(`${COLORS.pink}Name:${COLORS.reset} ${authorInfo.name}`); + console.log(`${COLORS.pink}Slug:${COLORS.reset} ${authorInfo.slug}`); + console.log(`${COLORS.pink}Role:${COLORS.reset} ${authorInfo.role}`); + console.log(`${COLORS.pink}Bio:${COLORS.reset} ${authorInfo.bio}`); + console.log(`${COLORS.pink}Avatar:${COLORS.reset} ${authorInfo.avatar}`); + + if (authorInfo.twitter) { + console.log(`${COLORS.pink}Twitter:${COLORS.reset} ${authorInfo.twitter}`); + } + if (authorInfo.github) { + console.log(`${COLORS.pink}GitHub:${COLORS.reset} ${authorInfo.github}`); + } + if (authorInfo.linkedin) { + console.log(`${COLORS.pink}LinkedIn:${COLORS.reset} ${authorInfo.linkedin}`); + } + + const confirm = await selectFromList('\nCreate this author profile?', [ + { label: 'Yes, create it!', value: true }, + { label: 'No, cancel', value: false } + ]); + + if (!confirm) { + console.log(`\n${COLORS.yellow}Author creation cancelled.${COLORS.reset}`); + closeReadline(); + return; + } + + const result = await createAuthorFiles(authorInfo); + + if (result.success) { + console.log( + `\n${COLORS.pink}${COLORS.bright}✨ Success! Author profile created successfully!${COLORS.reset}\n` + ); + console.log(`${COLORS.bright}Created files:${COLORS.reset}`); + console.log(` • Author profile: ${COLORS.pink}${result.markdocPath}${COLORS.reset}`); + + console.log(`\n${COLORS.bright}Next steps:${COLORS.reset}`); + if (!result.avatarCopied) { + const avatarPath = path.join(ROOT_DIR, 'static', 'images', 'avatars', `${authorInfo.slug}.png`); + console.log( + `1. Add avatar image to: ${COLORS.pink}${avatarPath}${COLORS.reset}` + ); + console.log( + `2. The author "${authorInfo.name}" is now available for blog posts` + ); + } else { + console.log( + `The author "${authorInfo.name}" is now available for blog posts` + ); + } + console.log( + `\n${COLORS.dim}Run 'pnpm create-blog' to create a blog post with this author!${COLORS.reset}` + ); + } else { + console.log(`\n${COLORS.red}Error creating author files: ${result.error}${COLORS.reset}`); + } + + } catch (error) { + console.log(`\n${COLORS.red}Error: ${error.message}${COLORS.reset}`); + } + + closeReadline(); +} + +process.on('SIGINT', () => { + console.log(`\n${COLORS.yellow}Process interrupted. Exiting...${COLORS.reset}`); + closeReadline(); + process.exit(0); +}); + +main(); \ No newline at end of file diff --git a/scripts/blog/create-blog.js b/scripts/blog/create-blog.js new file mode 100644 index 0000000000..69e8cf70ab --- /dev/null +++ b/scripts/blog/create-blog.js @@ -0,0 +1,349 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { + COLORS, + printHeader, + question, + questionWithDefault, + selectFromList, + imagePathInput, + copyImage, + closeReadline +} from './utils.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.join(__dirname, '..', '..'); + +function getAuthors() { + const authorsPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'author'); + const authors = []; + + try { + const entries = fs.readdirSync(authorsPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + const markdocPath = path.join(authorsPath, entry.name, '+page.markdoc'); + if (fs.existsSync(markdocPath)) { + const content = fs.readFileSync(markdocPath, 'utf8'); + const match = content.match(/^---\n([\s\S]*?)\n---/); + + if (match) { + const frontmatter = match[1]; + const nameMatch = frontmatter.match(/name:\s*(.+)/); + const slugMatch = frontmatter.match(/slug:\s*(.+)/); + + if (nameMatch && slugMatch) { + authors.push({ + label: nameMatch[1].trim(), + value: slugMatch[1].trim() + }); + } + } + } + } + } + } catch (error) { + console.error(`${COLORS.red}Error reading authors: ${error.message}${COLORS.reset}`); + } + + return authors.sort((a, b) => a.label.localeCompare(b.label)); +} + +function getCategories() { + const categoriesPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'category'); + const categories = []; + + try { + const entries = fs.readdirSync(categoriesPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory() && !entry.name.startsWith('.')) { + categories.push(entry.name); + } + } + } catch (error) { + console.error(`${COLORS.red}Error reading categories: ${error.message}${COLORS.reset}`); + } + + return categories.sort(); +} + +function slugify(text) { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/--+/g, '-') + .trim(); +} + +function validateSlug(slug) { + const blogPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'post', slug); + + if (fs.existsSync(blogPath)) { + return { + valid: false, + message: `A blog post with slug "${slug}" already exists!` + }; + } + + if (!/^[a-z0-9-]+$/.test(slug)) { + return { + valid: false, + message: 'Slug should only contain lowercase letters, numbers, and hyphens' + }; + } + + return { valid: true }; +} + +async function collectBlogInfo(authors, categories) { + const blogInfo = {}; + + console.log(`\n${COLORS.bright}📝 Blog Post Information${COLORS.reset}\n`); + + blogInfo.title = await question('Title: '); + + if (!blogInfo.title) { + throw new Error('Title is required'); + } + + // Auto-generate slug from title + const suggestedSlug = slugify(blogInfo.title); + let slug = await questionWithDefault('Slug', suggestedSlug); + + let validation = validateSlug(slug); + while (!validation.valid) { + console.log(`${COLORS.red}${validation.message}${COLORS.reset}`); + slug = await question('Please enter a different slug: '); + validation = validateSlug(slug); + } + + blogInfo.slug = slug; + + blogInfo.description = await question('Description (SEO meta description): '); + + if (!blogInfo.description) { + throw new Error('Description is required'); + } + + const today = new Date().toISOString().split('T')[0]; + blogInfo.date = await questionWithDefault('Date (YYYY-MM-DD)', today); + + blogInfo.timeToRead = await questionWithDefault('Time to read (minutes)', '5'); + + // Author selection + console.log(`\n${COLORS.bright}👤 Select Author${COLORS.reset}`); + if (authors.length > 0) { + blogInfo.author = await selectFromList('Choose an author:', authors); + } else { + console.log( + `${COLORS.yellow}No authors found. You'll need to create an author file first.${COLORS.reset}` + ); + blogInfo.author = await question('Enter author slug manually (e.g., john-doe): '); + } + + // Category selection + console.log(`\n${COLORS.bright}🏷️ Select Category${COLORS.reset}`); + if (categories.length > 0) { + blogInfo.category = await selectFromList('Choose a category:', categories); + } else { + console.log( + `${COLORS.yellow}No categories found. You can enter one manually.${COLORS.reset}` + ); + blogInfo.category = await question('Enter category: '); + } + + const isFeatured = await selectFromList('Featured post?', [ + { label: 'No', value: false }, + { label: 'Yes', value: true } + ]); + blogInfo.featured = isFeatured; + + // Cover Image + console.log(`\n${COLORS.bright}🖼️ Cover Image${COLORS.reset}`); + const coverFileName = 'cover.png'; + const coverPath = `/images/blog/${blogInfo.slug}/${coverFileName}`; + const fullCoverPath = path.join( + ROOT_DIR, + 'static', + 'images', + 'blog', + blogInfo.slug, + coverFileName + ); + + const imageResult = await imagePathInput('Add Cover Image', fullCoverPath); + blogInfo.cover = coverPath; + blogInfo.coverSourcePath = imageResult.sourcePath; + blogInfo.coverTargetPath = imageResult.targetPath; + + return blogInfo; +} + +function generateMarkdocContent(blogInfo) { + const frontmatter = `--- +layout: post +title: ${blogInfo.title} +description: ${blogInfo.description} +date: ${blogInfo.date} +cover: ${blogInfo.cover} +timeToRead: ${blogInfo.timeToRead} +author: ${blogInfo.author} +category: ${blogInfo.category} +featured: ${blogInfo.featured} +--- + +# ${blogInfo.title} + +Start writing your blog post here... + +## Introduction + +Your introduction paragraph goes here. + +## Main Content + +Add your main content sections here. + +## Conclusion + +Wrap up your blog post with a conclusion. +`; + + return frontmatter; +} + +async function createBlogFiles(blogInfo) { + const blogPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'post', blogInfo.slug); + const imagePath = path.join(ROOT_DIR, 'static', 'images', 'blog', blogInfo.slug); + + console.log(`\n${COLORS.bright}📁 Creating blog files...${COLORS.reset}\n`); + + try { + fs.mkdirSync(blogPath, { recursive: true }); + console.log( + `${COLORS.pink}✓${COLORS.reset} Created directory: ${COLORS.dim}${blogPath}${COLORS.reset}` + ); + + fs.mkdirSync(imagePath, { recursive: true }); + console.log( + `${COLORS.pink}✓${COLORS.reset} Created directory: ${COLORS.dim}${imagePath}${COLORS.reset}` + ); + + const markdocPath = path.join(blogPath, '+page.markdoc'); + const content = generateMarkdocContent(blogInfo); + fs.writeFileSync(markdocPath, content); + console.log( + `${COLORS.pink}✓${COLORS.reset} Created file: ${COLORS.dim}${markdocPath}${COLORS.reset}` + ); + + // Copy cover image if provided + let coverCopied = false; + if (blogInfo.coverSourcePath && blogInfo.coverTargetPath) { + coverCopied = await copyImage(blogInfo.coverSourcePath, blogInfo.coverTargetPath); + } + + return { success: true, blogPath, imagePath, markdocPath, coverCopied }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +async function main() { + printHeader(); + + console.log(`${COLORS.dim}Loading authors and categories...${COLORS.reset}\n`); + + // Pre-fetch authors and categories + const authors = getAuthors(); + const categories = getCategories(); + + if (authors.length === 0) { + console.log(`${COLORS.yellow}⚠ No authors found in the system${COLORS.reset}`); + } else { + console.log(`${COLORS.pink}✓${COLORS.reset} Found ${authors.length} authors`); + } + + if (categories.length === 0) { + console.log(`${COLORS.yellow}⚠ No categories found in the system${COLORS.reset}`); + } else { + console.log(`${COLORS.pink}✓${COLORS.reset} Found ${categories.length} categories`); + } + + try { + const blogInfo = await collectBlogInfo(authors, categories); + + console.log(`\n${COLORS.bright}📋 Review Blog Information${COLORS.reset}\n`); + console.log(`${COLORS.pink}Title:${COLORS.reset} ${blogInfo.title}`); + console.log(`${COLORS.pink}Slug:${COLORS.reset} ${blogInfo.slug}`); + console.log(`${COLORS.pink}Description:${COLORS.reset} ${blogInfo.description}`); + console.log(`${COLORS.pink}Date:${COLORS.reset} ${blogInfo.date}`); + console.log(`${COLORS.pink}Author:${COLORS.reset} ${blogInfo.author}`); + console.log(`${COLORS.pink}Category:${COLORS.reset} ${blogInfo.category}`); + console.log(`${COLORS.pink}Featured:${COLORS.reset} ${blogInfo.featured}`); + console.log(`${COLORS.pink}Cover:${COLORS.reset} ${blogInfo.cover}`); + console.log(`${COLORS.pink}Time to Read:${COLORS.reset} ${blogInfo.timeToRead} minutes`); + + const confirm = await selectFromList('\nCreate this blog post?', [ + { label: 'Yes, create it!', value: true }, + { label: 'No, cancel', value: false } + ]); + + if (!confirm) { + console.log(`\n${COLORS.yellow}Blog creation cancelled.${COLORS.reset}`); + closeReadline(); + return; + } + + const result = await createBlogFiles(blogInfo); + + if (result.success) { + console.log( + `\n${COLORS.pink}${COLORS.bright}✨ Success! Blog post created successfully!${COLORS.reset}\n` + ); + console.log(`${COLORS.bright}Created directories:${COLORS.reset}`); + console.log(` • Blog post: ${COLORS.pink}${result.blogPath}${COLORS.reset}`); + console.log(` • Images: ${COLORS.pink}${result.imagePath}${COLORS.reset}`); + + console.log(`\n${COLORS.bright}Next steps:${COLORS.reset}`); + console.log( + `1. Edit your blog post: ${COLORS.pink}${result.markdocPath}${COLORS.reset}` + ); + + if (!result.coverCopied) { + console.log( + `2. Add your cover image as: ${COLORS.pink}${result.imagePath}/cover.png${COLORS.reset}` + ); + console.log( + `3. Add any other images to: ${COLORS.pink}${result.imagePath}${COLORS.reset}` + ); + } else { + console.log( + `2. Add any other images to: ${COLORS.pink}${result.imagePath}${COLORS.reset}` + ); + } + + console.log( + `\n${COLORS.dim}Run 'pnpm dev' to see your blog post in action!${COLORS.reset}` + ); + } else { + console.log(`\n${COLORS.red}Error creating blog files: ${result.error}${COLORS.reset}`); + } + } catch (error) { + console.log(`\n${COLORS.red}Error: ${error.message}${COLORS.reset}`); + } + + closeReadline(); +} + +process.on('SIGINT', () => { + console.log(`\n${COLORS.yellow}Process interrupted. Exiting...${COLORS.reset}`); + closeReadline(); + process.exit(0); +}); + +main(); diff --git a/scripts/blog/utils.js b/scripts/blog/utils.js new file mode 100644 index 0000000000..b9d92470c9 --- /dev/null +++ b/scripts/blog/utils.js @@ -0,0 +1,302 @@ +import readline from 'readline'; +import process from 'process'; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +export const COLORS = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + pink: '\x1b[38;2;253;54;110m' // #fd366e +}; + +export function printHeader() { + console.clear(); + console.log(`${COLORS.pink}${COLORS.bright}`); + console.log(' ___ _ __ '); + console.log(' / _ | ___ ___ _ ___ ____(_) /____ '); + console.log(' / __ |/ _ \\/ _ \\| |/|/ / __/ / / __/ -_)'); + console.log(' /_/ |_/ .__/ .__/|__,__/_/ /_/_/\\__/\\__/ '); + console.log(' /_/ /_/ '); + console.log(''); + console.log(' BLOG CREATOR'); + console.log(`${COLORS.reset}\n`); + console.log(`${COLORS.dim}Create a new blog post for the Appwrite website${COLORS.reset}\n`); +} + +export function question(prompt) { + return new Promise((resolve) => { + rl.question(`${COLORS.yellow}${prompt}${COLORS.reset}`, (answer) => { + resolve(answer.trim()); + }); + }); +} + +export function questionWithDefault(prompt, defaultValue) { + return new Promise((resolve) => { + rl.question( + `${COLORS.yellow}${prompt} ${COLORS.dim}(default: ${defaultValue})${COLORS.reset}${COLORS.yellow}: ${COLORS.reset}`, + (answer) => { + resolve(answer.trim() || defaultValue); + } + ); + }); +} + +export async function selectFromList(prompt, options) { + return new Promise((resolve) => { + let selectedIndex = 0; + const maxVisible = 10; + let scrollOffset = 0; + + const render = () => { + // Clear screen and redraw + console.log('\x1b[2J\x1b[H'); + console.log(`\n${COLORS.pink}${prompt}${COLORS.reset}`); + console.log(`${COLORS.dim}(Use ↑↓ to navigate, Enter to select)${COLORS.reset}\n`); + + // Calculate visible range + const startIdx = scrollOffset; + const endIdx = Math.min(startIdx + maxVisible, options.length); + + // Show scroll indicator if needed + if (startIdx > 0) { + console.log(` ${COLORS.dim}▲ ${startIdx} more above${COLORS.reset}`); + } + + // Render visible options + for (let i = startIdx; i < endIdx; i++) { + const option = options[i]; + if (i === selectedIndex) { + console.log(`${COLORS.pink}❯ ${option.label || option}${COLORS.reset}`); + } else { + console.log(` ${COLORS.dim}${option.label || option}${COLORS.reset}`); + } + } + + // Show scroll indicator if needed + if (endIdx < options.length) { + console.log(` ${COLORS.dim}▼ ${options.length - endIdx} more below${COLORS.reset}`); + } + }; + + const updateScroll = () => { + if (selectedIndex < scrollOffset) { + scrollOffset = selectedIndex; + } else if (selectedIndex >= scrollOffset + maxVisible) { + scrollOffset = selectedIndex - maxVisible + 1; + } + }; + + render(); + + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + const keypress = (key) => { + if (key === '\u0003') { // Ctrl+C + process.stdin.setRawMode(false); + process.stdin.pause(); + console.log('\n'); + process.exit(); + } + + if (key === '\r' || key === '\n') { // Enter + process.stdin.removeListener('data', keypress); + process.stdin.setRawMode(false); + process.stdin.resume(); + console.log(''); + resolve(options[selectedIndex].value !== undefined ? options[selectedIndex].value : options[selectedIndex]); + return; + } + + if (key === '\u001b[A') { // Up arrow + selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1; + updateScroll(); + render(); + } + + if (key === '\u001b[B') { // Down arrow + selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0; + updateScroll(); + render(); + } + }; + + process.stdin.on('data', keypress); + }); +} + +export async function multiSelectFromList(prompt, options) { + return new Promise((resolve) => { + let selectedIndex = 0; + let selectedItems = new Set(); + const maxVisible = 10; + let scrollOffset = 0; + + const render = () => { + console.log('\x1b[2J\x1b[H'); + console.log(`\n${COLORS.pink}${prompt}${COLORS.reset}`); + console.log(`${COLORS.dim}(Use ↑↓ to navigate, Space to select/deselect, Enter to confirm)${COLORS.reset}\n`); + + // Calculate visible range + const startIdx = scrollOffset; + const endIdx = Math.min(startIdx + maxVisible, options.length); + + // Show scroll indicator if needed + if (startIdx > 0) { + console.log(` ${COLORS.dim}▲ ${startIdx} more above${COLORS.reset}`); + } + + // Render visible options + for (let i = startIdx; i < endIdx; i++) { + const option = options[i]; + const isSelected = selectedItems.has(i); + const checkbox = isSelected ? `${COLORS.pink}[✓]${COLORS.reset}` : '[ ]'; + const label = option.label || option; + + if (i === selectedIndex) { + console.log(`${COLORS.pink}❯${COLORS.reset} ${checkbox} ${label}`); + } else { + console.log(` ${checkbox} ${COLORS.dim}${label}${COLORS.reset}`); + } + } + + // Show scroll indicator if needed + if (endIdx < options.length) { + console.log(` ${COLORS.dim}▼ ${options.length - endIdx} more below${COLORS.reset}`); + } + + if (selectedItems.size > 0) { + console.log(`\n${COLORS.dim}Selected: ${selectedItems.size} item(s)${COLORS.reset}`); + } + }; + + const updateScroll = () => { + if (selectedIndex < scrollOffset) { + scrollOffset = selectedIndex; + } else if (selectedIndex >= scrollOffset + maxVisible) { + scrollOffset = selectedIndex - maxVisible + 1; + } + }; + + render(); + + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + const keypress = (key) => { + if (key === '\u0003') { // Ctrl+C + process.stdin.setRawMode(false); + process.stdin.pause(); + console.log('\n'); + process.exit(); + } + + if (key === '\r' || key === '\n') { // Enter + process.stdin.removeListener('data', keypress); + process.stdin.setRawMode(false); + process.stdin.pause(); + console.log(''); + const selected = Array.from(selectedItems).map(i => options[i].value || options[i]); + resolve(selected); + return; + } + + if (key === ' ') { // Space + if (selectedItems.has(selectedIndex)) { + selectedItems.delete(selectedIndex); + } else { + selectedItems.add(selectedIndex); + } + render(); + } + + if (key === '\u001b[A') { // Up arrow + selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1; + updateScroll(); + render(); + } + + if (key === '\u001b[B') { // Down arrow + selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0; + updateScroll(); + render(); + } + }; + + process.stdin.on('data', keypress); + }); +} + +export async function imagePathInput(prompt, targetPath) { + console.log(`\n${COLORS.pink}${prompt}${COLORS.reset}`); + console.log(`${COLORS.dim}Drag & drop an image file here, or press Enter to add later${COLORS.reset}`); + console.log(`${COLORS.dim}Image will be saved to: ${targetPath}${COLORS.reset}\n`); + + const input = await question('Image path: '); + + if (!input || !input.trim()) { + return { sourcePath: null, targetPath }; + } + + // Clean up dragged file path: + // 1. Remove surrounding quotes + // 2. Replace escaped spaces (\ ) with regular spaces + // 3. Handle escaped quotes + let cleanPath = input.trim() + .replace(/^['"]|['"]$/g, '') // Remove surrounding quotes + .replace(/\\ /g, ' ') // Replace escaped spaces + .replace(/\\"/g, '"') // Replace escaped quotes + .replace(/\\'/g, "'"); // Replace escaped single quotes + + // Check if file exists + const fs = await import('fs'); + + if (fs.existsSync(cleanPath)) { + console.log(`${COLORS.pink}✓${COLORS.reset} Image selected: ${COLORS.dim}${cleanPath}${COLORS.reset}`); + return { sourcePath: cleanPath, targetPath }; + } else { + console.log(`${COLORS.red}File not found: ${cleanPath}${COLORS.reset}`); + return { sourcePath: null, targetPath }; + } +} + +export async function copyImage(sourcePath, targetPath) { + if (!sourcePath) return false; + + const fs = await import('fs'); + const path = await import('path'); + + try { + // Create directory if it doesn't exist + const targetDir = path.dirname(targetPath); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Copy the file + fs.copyFileSync(sourcePath, targetPath); + console.log(`${COLORS.pink}✓${COLORS.reset} Image copied to: ${COLORS.dim}${targetPath}${COLORS.reset}`); + return true; + } catch (error) { + console.log(`${COLORS.red}Failed to copy image: ${error.message}${COLORS.reset}`); + return false; + } +} + +export function closeReadline() { + rl.close(); +} \ No newline at end of file From ae7eab00c17c3270e248adb301bfb94d5f6395e5 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Wed, 17 Sep 2025 18:09:49 +0530 Subject: [PATCH 2/3] reuse header --- scripts/blog/create-author.js | 15 +-------------- scripts/blog/utils.js | 6 +++--- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/scripts/blog/create-author.js b/scripts/blog/create-author.js index 894df5e281..fb2f08700f 100644 --- a/scripts/blog/create-author.js +++ b/scripts/blog/create-author.js @@ -16,19 +16,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ROOT_DIR = path.join(__dirname, '..', '..'); -function printAuthorHeader() { - console.clear(); - console.log(`${COLORS.pink}${COLORS.bright}`); - console.log(' ___ _ __ '); - console.log(' / _ | ___ ___ _ ___ ____(_) /____ '); - console.log(' / __ |/ _ \\/ _ \\| |/|/ / __/ / / __/ -_)'); - console.log(' /_/ |_/ .__/ .__/|__,__/_/ /_/_/\\__/\\__/ '); - console.log(' /_/ /_/ '); - console.log(''); - console.log(' AUTHOR CREATOR'); - console.log(`${COLORS.reset}\n`); - console.log(`${COLORS.dim}Add a new author to the Appwrite blog${COLORS.reset}\n`); -} function slugify(text) { return text @@ -216,7 +203,7 @@ async function createAuthorFiles(authorInfo) { } async function main() { - printAuthorHeader(); + printHeader('AUTHOR CREATOR', 'Add a new author to the Appwrite blog'); try { const authorInfo = await collectAuthorInfo(); diff --git a/scripts/blog/utils.js b/scripts/blog/utils.js index b9d92470c9..b05ca19e7c 100644 --- a/scripts/blog/utils.js +++ b/scripts/blog/utils.js @@ -20,7 +20,7 @@ export const COLORS = { pink: '\x1b[38;2;253;54;110m' // #fd366e }; -export function printHeader() { +export function printHeader(title = 'BLOG CREATOR', subtitle = 'Create a new blog post for the Appwrite website') { console.clear(); console.log(`${COLORS.pink}${COLORS.bright}`); console.log(' ___ _ __ '); @@ -29,9 +29,9 @@ export function printHeader() { console.log(' /_/ |_/ .__/ .__/|__,__/_/ /_/_/\\__/\\__/ '); console.log(' /_/ /_/ '); console.log(''); - console.log(' BLOG CREATOR'); + console.log(` ${title}`); console.log(`${COLORS.reset}\n`); - console.log(`${COLORS.dim}Create a new blog post for the Appwrite website${COLORS.reset}\n`); + console.log(`${COLORS.dim}${subtitle}${COLORS.reset}\n`); } export function question(prompt) { From 668151ecacf3be88c8bcec168433fb42e98c5c24 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Wed, 17 Sep 2025 18:10:15 +0530 Subject: [PATCH 3/3] format --- scripts/blog/create-author.js | 31 ++++++++------- scripts/blog/utils.js | 75 +++++++++++++++++++++++++---------- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/scripts/blog/create-author.js b/scripts/blog/create-author.js index fb2f08700f..297bb2d9ef 100644 --- a/scripts/blog/create-author.js +++ b/scripts/blog/create-author.js @@ -16,7 +16,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ROOT_DIR = path.join(__dirname, '..', '..'); - function slugify(text) { return text .toLowerCase() @@ -193,7 +192,10 @@ async function createAuthorFiles(authorInfo) { // Copy avatar image if provided let avatarCopied = false; if (authorInfo.avatarSourcePath && authorInfo.avatarTargetPath) { - avatarCopied = await copyImage(authorInfo.avatarSourcePath, authorInfo.avatarTargetPath); + avatarCopied = await copyImage( + authorInfo.avatarSourcePath, + authorInfo.avatarTargetPath + ); } return { success: true, authorPath, markdocPath, avatarCopied }; @@ -248,25 +250,26 @@ async function main() { console.log(`\n${COLORS.bright}Next steps:${COLORS.reset}`); if (!result.avatarCopied) { - const avatarPath = path.join(ROOT_DIR, 'static', 'images', 'avatars', `${authorInfo.slug}.png`); - console.log( - `1. Add avatar image to: ${COLORS.pink}${avatarPath}${COLORS.reset}` - ); - console.log( - `2. The author "${authorInfo.name}" is now available for blog posts` + const avatarPath = path.join( + ROOT_DIR, + 'static', + 'images', + 'avatars', + `${authorInfo.slug}.png` ); + console.log(`1. Add avatar image to: ${COLORS.pink}${avatarPath}${COLORS.reset}`); + console.log(`2. The author "${authorInfo.name}" is now available for blog posts`); } else { - console.log( - `The author "${authorInfo.name}" is now available for blog posts` - ); + console.log(`The author "${authorInfo.name}" is now available for blog posts`); } console.log( `\n${COLORS.dim}Run 'pnpm create-blog' to create a blog post with this author!${COLORS.reset}` ); } else { - console.log(`\n${COLORS.red}Error creating author files: ${result.error}${COLORS.reset}`); + console.log( + `\n${COLORS.red}Error creating author files: ${result.error}${COLORS.reset}` + ); } - } catch (error) { console.log(`\n${COLORS.red}Error: ${error.message}${COLORS.reset}`); } @@ -280,4 +283,4 @@ process.on('SIGINT', () => { process.exit(0); }); -main(); \ No newline at end of file +main(); diff --git a/scripts/blog/utils.js b/scripts/blog/utils.js index b05ca19e7c..1578f1d7cc 100644 --- a/scripts/blog/utils.js +++ b/scripts/blog/utils.js @@ -20,7 +20,10 @@ export const COLORS = { pink: '\x1b[38;2;253;54;110m' // #fd366e }; -export function printHeader(title = 'BLOG CREATOR', subtitle = 'Create a new blog post for the Appwrite website') { +export function printHeader( + title = 'BLOG CREATOR', + subtitle = 'Create a new blog post for the Appwrite website' +) { console.clear(); console.log(`${COLORS.pink}${COLORS.bright}`); console.log(' ___ _ __ '); @@ -86,7 +89,9 @@ export async function selectFromList(prompt, options) { // Show scroll indicator if needed if (endIdx < options.length) { - console.log(` ${COLORS.dim}▼ ${options.length - endIdx} more below${COLORS.reset}`); + console.log( + ` ${COLORS.dim}▼ ${options.length - endIdx} more below${COLORS.reset}` + ); } }; @@ -105,29 +110,37 @@ export async function selectFromList(prompt, options) { process.stdin.setEncoding('utf8'); const keypress = (key) => { - if (key === '\u0003') { // Ctrl+C + if (key === '\u0003') { + // Ctrl+C process.stdin.setRawMode(false); process.stdin.pause(); console.log('\n'); process.exit(); } - if (key === '\r' || key === '\n') { // Enter + if (key === '\r' || key === '\n') { + // Enter process.stdin.removeListener('data', keypress); process.stdin.setRawMode(false); process.stdin.resume(); console.log(''); - resolve(options[selectedIndex].value !== undefined ? options[selectedIndex].value : options[selectedIndex]); + resolve( + options[selectedIndex].value !== undefined + ? options[selectedIndex].value + : options[selectedIndex] + ); return; } - if (key === '\u001b[A') { // Up arrow + if (key === '\u001b[A') { + // Up arrow selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1; updateScroll(); render(); } - if (key === '\u001b[B') { // Down arrow + if (key === '\u001b[B') { + // Down arrow selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0; updateScroll(); render(); @@ -148,7 +161,9 @@ export async function multiSelectFromList(prompt, options) { const render = () => { console.log('\x1b[2J\x1b[H'); console.log(`\n${COLORS.pink}${prompt}${COLORS.reset}`); - console.log(`${COLORS.dim}(Use ↑↓ to navigate, Space to select/deselect, Enter to confirm)${COLORS.reset}\n`); + console.log( + `${COLORS.dim}(Use ↑↓ to navigate, Space to select/deselect, Enter to confirm)${COLORS.reset}\n` + ); // Calculate visible range const startIdx = scrollOffset; @@ -175,11 +190,15 @@ export async function multiSelectFromList(prompt, options) { // Show scroll indicator if needed if (endIdx < options.length) { - console.log(` ${COLORS.dim}▼ ${options.length - endIdx} more below${COLORS.reset}`); + console.log( + ` ${COLORS.dim}▼ ${options.length - endIdx} more below${COLORS.reset}` + ); } if (selectedItems.size > 0) { - console.log(`\n${COLORS.dim}Selected: ${selectedItems.size} item(s)${COLORS.reset}`); + console.log( + `\n${COLORS.dim}Selected: ${selectedItems.size} item(s)${COLORS.reset}` + ); } }; @@ -198,24 +217,29 @@ export async function multiSelectFromList(prompt, options) { process.stdin.setEncoding('utf8'); const keypress = (key) => { - if (key === '\u0003') { // Ctrl+C + if (key === '\u0003') { + // Ctrl+C process.stdin.setRawMode(false); process.stdin.pause(); console.log('\n'); process.exit(); } - if (key === '\r' || key === '\n') { // Enter + if (key === '\r' || key === '\n') { + // Enter process.stdin.removeListener('data', keypress); process.stdin.setRawMode(false); process.stdin.pause(); console.log(''); - const selected = Array.from(selectedItems).map(i => options[i].value || options[i]); + const selected = Array.from(selectedItems).map( + (i) => options[i].value || options[i] + ); resolve(selected); return; } - if (key === ' ') { // Space + if (key === ' ') { + // Space if (selectedItems.has(selectedIndex)) { selectedItems.delete(selectedIndex); } else { @@ -224,13 +248,15 @@ export async function multiSelectFromList(prompt, options) { render(); } - if (key === '\u001b[A') { // Up arrow + if (key === '\u001b[A') { + // Up arrow selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1; updateScroll(); render(); } - if (key === '\u001b[B') { // Down arrow + if (key === '\u001b[B') { + // Down arrow selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0; updateScroll(); render(); @@ -243,7 +269,9 @@ export async function multiSelectFromList(prompt, options) { export async function imagePathInput(prompt, targetPath) { console.log(`\n${COLORS.pink}${prompt}${COLORS.reset}`); - console.log(`${COLORS.dim}Drag & drop an image file here, or press Enter to add later${COLORS.reset}`); + console.log( + `${COLORS.dim}Drag & drop an image file here, or press Enter to add later${COLORS.reset}` + ); console.log(`${COLORS.dim}Image will be saved to: ${targetPath}${COLORS.reset}\n`); const input = await question('Image path: '); @@ -256,7 +284,8 @@ export async function imagePathInput(prompt, targetPath) { // 1. Remove surrounding quotes // 2. Replace escaped spaces (\ ) with regular spaces // 3. Handle escaped quotes - let cleanPath = input.trim() + let cleanPath = input + .trim() .replace(/^['"]|['"]$/g, '') // Remove surrounding quotes .replace(/\\ /g, ' ') // Replace escaped spaces .replace(/\\"/g, '"') // Replace escaped quotes @@ -266,7 +295,9 @@ export async function imagePathInput(prompt, targetPath) { const fs = await import('fs'); if (fs.existsSync(cleanPath)) { - console.log(`${COLORS.pink}✓${COLORS.reset} Image selected: ${COLORS.dim}${cleanPath}${COLORS.reset}`); + console.log( + `${COLORS.pink}✓${COLORS.reset} Image selected: ${COLORS.dim}${cleanPath}${COLORS.reset}` + ); return { sourcePath: cleanPath, targetPath }; } else { console.log(`${COLORS.red}File not found: ${cleanPath}${COLORS.reset}`); @@ -289,7 +320,9 @@ export async function copyImage(sourcePath, targetPath) { // Copy the file fs.copyFileSync(sourcePath, targetPath); - console.log(`${COLORS.pink}✓${COLORS.reset} Image copied to: ${COLORS.dim}${targetPath}${COLORS.reset}`); + console.log( + `${COLORS.pink}✓${COLORS.reset} Image copied to: ${COLORS.dim}${targetPath}${COLORS.reset}` + ); return true; } catch (error) { console.log(`${COLORS.red}Failed to copy image: ${error.message}${COLORS.reset}`); @@ -299,4 +332,4 @@ export async function copyImage(sourcePath, targetPath) { export function closeReadline() { rl.close(); -} \ No newline at end of file +}