feat: add /write page with three-column layout, font switcher, frontmatter reference, and localStorage persistence

This commit is contained in:
Wayne Sutton
2025-12-20 18:58:19 -08:00
parent 4b187cff53
commit 9bdfe10d61
15 changed files with 1650 additions and 87 deletions

View File

@@ -29,6 +29,7 @@ npm run sync:prod # production
- Logo gallery with continuous marquee scroll
- Static raw markdown files at `/raw/{slug}.md`
- Dedicated blog page with configurable navigation order
- Markdown writing page at `/write` with frontmatter reference
### SEO and Discovery
@@ -571,6 +572,45 @@ body {
Replace the `font-family` property with your preferred font stack.
### Font Sizes
All font sizes use CSS variables defined in `:root`. Customize sizes by editing the variables:
```css
:root {
/* Base size scale */
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-xl: 18px;
--font-size-2xl: 20px;
--font-size-3xl: 24px;
/* Component-specific (examples) */
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
--font-size-nav-link: 14px;
}
```
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block with smaller values.
## Write Page
A public markdown writing page at `/write` (not linked in navigation). Features:
- Three-column Cursor docs-style layout
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
- Frontmatter reference panel with copy buttons for each field
- Font switcher (Serif/Sans-serif) with localStorage persistence
- Theme toggle matching the site themes (Moon, Sun, Half2Icon, Cloud)
- Word, line, and character counts
- localStorage persistence for content, content type, and font preference
- Works with Grammarly and browser spellcheck
- Warning message about refresh losing content
Access directly at `yourdomain.com/write`. Content is stored in localStorage only (not synced to database). Use it to draft posts, then copy the content to a markdown file in `content/blog/` or `content/pages/` and run `npm run sync`.
## Source
Fork this project: [github.com/waynesutton/markdown-site](https://github.com/waynesutton/markdown-site)

32
TASK.md
View File

@@ -2,17 +2,43 @@
## To Do
- [ ] Add markdown write page with copy option
- [ ] add github code block
- [ ] create a ui site config page
- [ ] create a prompt formator or skill or agent to change everything at once after forking
- [ ] create a prompt formator or checklidst or skill or agent to change everything at once after forking
## Current Status
v1.12.1 deployed. OG images now use post/page image from frontmatter instead of always defaulting.
v1.16.0 deployed. Added public /write page with three-column Cursor docs-style layout, font switcher, theme toggle, and localStorage persistence for markdown writing.
## Completed
- [x] Public /write page with three-column layout (not linked in nav)
- [x] Left sidebar: Home link, content type selector, actions (Clear, Theme, Font)
- [x] Center: Writing area with Copy All button and borderless textarea
- [x] Right sidebar: Frontmatter reference with per-field copy buttons
- [x] Font switcher to toggle between Serif and Sans-serif fonts
- [x] Font preference persistence in localStorage
- [x] Theme toggle icons matching ThemeToggle.tsx (Moon, Sun, Half2Icon, Cloud)
- [x] Content type switching (Blog Post/Page) updates writing area template
- [x] Word, line, and character counts in status bar
- [x] Warning banner about refresh losing content
- [x] localStorage persistence for content, type, and font
- [x] Redesign /write page with three-column Cursor docs-style layout
- [x] Add per-field copy icons to frontmatter reference panel
- [x] Add refresh warning message in left sidebar
- [x] Left sidebar with home link, content type selector, and actions
- [x] Right sidebar with frontmatter fields and copy buttons
- [x] Center area with title, Copy All button, and borderless textarea
- [x] Theme toggle with matching icons for all four themes
- [x] Redesign /write page with wider layout and modern Notion-like UI
- [x] Remove header from /write page (standalone writing experience)
- [x] Add inline theme toggle and home link to Write page toolbar
- [x] Collapsible frontmatter fields panel
- [x] Add markdown write page with copy option at /write
- [x] Centralized font-size CSS variables in global.css
- [x] Base size scale with semantic naming (3xs to hero)
- [x] Component-specific font-size variables
- [x] Mobile responsive font-size overrides
- [x] Open Graph image fix for posts and pages with frontmatter images
- [x] Dedicated blog page with configurable display options
- [x] Blog page navigation order via siteConfig.blogPage.order

View File

@@ -4,6 +4,152 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [1.16.0] - 2025-12-21
### Added
- Public markdown writing page at `/write` (not linked in navigation)
- Three-column Cursor docs-style layout
- Left sidebar: Home link, content type selector (Blog Post/Page), actions (Clear, Theme, Font)
- Center: Full-height writing area with title, Copy All button, and borderless textarea
- Right sidebar: Frontmatter reference with copy icon for each field
- Font switcher in Actions section
- Toggle between Serif and Sans-serif fonts
- Font preference saved to localStorage
- Theme toggle matching the rest of the app (Moon, Sun, Half2Icon, Cloud)
- localStorage persistence for content, type, and font preference
- Word, line, and character counts in status bar
- Warning banner: "Refresh loses content"
- Grammarly and browser spellcheck compatible
- Works with all four themes (dark, light, tan, cloud)
### Technical
- New component: `src/pages/Write.tsx`
- Route: `/write` (added to `src/App.tsx`)
- Three localStorage keys: `markdown_write_content`, `markdown_write_type`, `markdown_write_font`
- CSS Grid layout (220px | 1fr | 280px)
- Uses Phosphor icons: House, Article, File, Trash, CopySimple, Warning, Check
- Uses lucide-react and radix-ui icons for theme toggle (consistent with ThemeToggle.tsx)
## [1.15.1] - 2025-12-21
### Fixed
- Theme toggle icons on `/write` page now match `ThemeToggle.tsx` component
- dark: Moon icon (lucide-react)
- light: Sun icon (lucide-react)
- tan: Half2Icon (radix-ui) - consistent with rest of app
- cloud: Cloud icon (lucide-react)
- Content type switching (Blog Post/Page) now always updates writing area template
### Technical
- Replaced Phosphor icons (Moon, Sun, Leaf, CloudSun) with lucide-react and radix-ui icons
- `handleTypeChange` now always regenerates template when switching types
## [1.15.0] - 2025-12-21
### Changed
- Redesigned `/write` page with three-column Cursor docs-style layout
- Left sidebar: Home link, content type selector (Blog Post/Page), actions (Clear, Theme)
- Center: Full-height writing area with title, Copy All button, and borderless textarea
- Right sidebar: Frontmatter reference with copy icon for each field
- Frontmatter fields panel with per-field copy buttons
- Each frontmatter field shows name, example value, and copy icon
- Click to copy individual field syntax to clipboard
- Required fields marked with red asterisk
- Fields update dynamically when switching between Blog Post and Page
- Warning banner for unsaved content
- "Refresh loses content" warning in left sidebar with warning icon
- Helps users remember localStorage persistence limitations
- Enhanced status bar
- Word, line, and character counts in sticky footer
- Save hint with content directory path
### Technical
- Three-column CSS Grid layout (220px sidebar | 1fr main | 280px right sidebar)
- Theme toggle cycles through dark, light, tan, cloud with matching icons
- Collapsible sidebars on mobile (stacked layout)
- Uses Phosphor icons: House, Article, File, Trash, CopySimple, Warning, Check
## [1.14.0] - 2025-12-20
### Changed
- Redesigned `/write` page with Notion-like minimal UI
- Full-screen distraction-free writing experience
- Removed site header for focused writing environment
- Wider writing area (900px max-width centered)
- Borderless textarea with transparent background
- Own minimal header with home link, type selector, and icon buttons
- Improved toolbar design
- Home icon link to return to main site
- Clean dropdown for content type selection (no borders)
- Collapsible frontmatter fields panel (hidden by default)
- Theme toggle in toolbar (cycles through dark, light, tan, cloud)
- Icon buttons with subtle hover states
- Copy button with inverted theme colors
- Enhanced status bar
- Sticky footer with word/line/character counts
- Save hint with content directory path
- Dot separators between stats
### Technical
- Write page now renders without Layout component wrapper
- Added Phosphor icons: House, Sun, Moon, CloudSun, Leaf, Info, X
- CSS restructured for minimal aesthetic (`.write-wrapper`, `.write-header`, etc.)
- Mobile responsive with hidden copy text and save hint on small screens
## [1.13.0] - 2025-12-20
### Added
- Public markdown writing page at `/write` (not linked in navigation)
- Dropdown to select between "Blog Post" and "Page" content types
- Frontmatter fields reference panel with required/optional indicators
- Copy button using Phosphor CopySimple icon
- Clear button to reset content to template
- Status bar showing lines, words, and characters count
- Usage hint with instructions for saving content
- localStorage persistence for writing session
- Content persists across page refreshes within same browser
- Each browser has isolated content (session privacy)
- Content type selection saved separately
- Auto-generated frontmatter templates
- Blog post template with all common fields
- Page template with navigation fields
- Current date auto-populated in templates
### Technical
- New component: `src/pages/Write.tsx`
- Route: `/write` (added to `src/App.tsx`)
- CSS styles added to `src/styles/global.css`
- Works with all four themes (dark, light, tan, cloud)
- Plain textarea for Grammarly and browser spellcheck compatibility
- Mobile responsive design with adjusted layout for smaller screens
- No Convex backend required (localStorage only)
## [1.12.2] - 2025-12-20
### Added
- Centralized font-size configuration using CSS variables in `global.css`
- Base size scale from 10px to 64px with semantic names
- Component-specific variables for consistent sizing
- Mobile responsive overrides at 768px breakpoint
- All hardcoded font sizes converted to CSS variables for easier customization
### Technical
- Font sizes defined in `:root` selector with `--font-size-*` naming convention
- Mobile breakpoint uses same variables with smaller values
- Base scale: 3xs (10px), 2xs (11px), xs (12px), sm (13px), md (14px), base (16px), lg (17px), xl (18px), 2xl (20px), 3xl (24px), 4xl (28px), 5xl (32px), 6xl (36px), hero (64px)
## [1.12.1] - 2025-12-20
### Fixed

View File

@@ -703,6 +703,29 @@ body {
}
```
### Change Font Sizes
All font sizes use CSS variables defined in `:root`. Customize sizes by editing these variables in `src/styles/global.css`:
```css
:root {
/* Base size scale */
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-xl: 18px;
--font-size-2xl: 20px;
--font-size-3xl: 24px;
/* Component-specific (examples) */
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
--font-size-nav-link: 14px;
}
```
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block.
### Add Static Pages (Optional)
Create optional pages like About, Projects, or Contact. These appear as navigation links in the top right corner.
@@ -944,6 +967,32 @@ markdown-site/
└── package.json # Dependencies
```
## Write Page
A markdown writing page is available at `/write` (not linked in navigation). Use it to draft content before saving to your markdown files.
**Features:**
- Three-column Cursor docs-style layout
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
- Frontmatter field reference with individual copy buttons
- Font switcher (Serif/Sans-serif)
- Theme toggle matching site themes
- Word, line, and character counts
- localStorage persistence for content, type, and font preference
- Works with Grammarly and browser spellcheck
**Workflow:**
1. Go to `yourdomain.com/write`
2. Select content type (Blog Post or Page)
3. Write your content using the frontmatter reference
4. Click "Copy All" to copy the markdown
5. Save to `content/blog/` or `content/pages/`
6. Run `npm run sync` or `npm run sync:prod`
Content is stored in localStorage only and not synced to the database. Refreshing the page preserves your content, but clearing browser data will lose it.
## Next Steps
After deploying:

View File

@@ -63,6 +63,7 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
- Copy to ChatGPT, Claude, and Perplexity sharing
- Generate Skill option for AI agent training
- View as Markdown option in share dropdown
- Markdown writing page at `/write` with frontmatter reference
## Who this is for

View File

@@ -7,6 +7,82 @@ order: 5
All notable changes to this project.
## v1.15.2
Released December 20, 2025
**Write page font switcher**
- Font switcher in `/write` page Actions section
- Toggle between Serif and Sans-serif fonts in the writing area
- Font preference saved to localStorage and persists across sessions
- Uses same font families defined in global.css
## v1.15.1
Released December 20, 2025
**Write page theme and content fixes**
- Fixed theme toggle icons on `/write` page to match `ThemeToggle.tsx` (Moon, Sun, Half2Icon, Cloud)
- Content type switching now always updates the template in the writing area
## v1.15.0
Released December 20, 2025
**Write page three-column layout**
- Redesigned `/write` page with Cursor docs-style three-column layout
- Left sidebar: content type selector (Blog Post/Page) and action buttons (Clear, Theme)
- Center: full-screen writing area with Copy All button
- Right sidebar: frontmatter field reference with individual copy buttons for each field
- Warning message about refresh losing content
- Stats bar showing words, lines, and characters
## v1.14.0
Released December 20, 2025
**Write page Notion-like UI**
- Redesigned `/write` page with full-screen, distraction-free writing experience
- Floating header with home link, type selector, and action buttons
- Collapsible frontmatter panel on the right
- Removed borders from writing area for cleaner look
- Improved typography and spacing
## v1.13.0
Released December 20, 2025
**Markdown write page**
- New `/write` page for drafting markdown content (not linked in navigation)
- Content type selector for Blog Post or Page with appropriate frontmatter templates
- Frontmatter reference with copy buttons for each field
- Theme toggle matching site themes
- Word, line, and character counts
- localStorage persistence for content, type, and font preference
- Works with Grammarly and browser spellcheck
- Copy all button for easy content transfer
- Clear button to reset content
Access at `yourdomain.com/write`. Content stored in localStorage only.
## v1.12.2
Released December 20, 2025
**Centralized font-size CSS variables**
- All font sizes now use CSS variables for easier customization
- Base scale from `--font-size-3xs` (10px) to `--font-size-hero` (64px)
- Component-specific variables for blog headings, navigation, search, stats, and more
- Mobile responsive overrides at 768px breakpoint
Edit `src/styles/global.css` to customize font sizes across the entire site by changing the `:root` variables.
## v1.12.1
Released December 20, 2025

View File

@@ -394,6 +394,22 @@ body {
}
```
### Font Sizes
All font sizes use CSS variables in `:root`. Customize by editing:
```css
:root {
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
}
```
Mobile sizes defined in `@media (max-width: 768px)` block.
### Images
| Image | Location | Size |

View File

@@ -41,6 +41,7 @@ A brief description of each file in the codebase.
| `Blog.tsx` | Dedicated blog page with post list (configurable via siteConfig.blogPage) |
| `Post.tsx` | Individual blog post view (update SITE_URL/SITE_NAME when forking) |
| `Stats.tsx` | Real-time analytics dashboard with visitor stats |
| `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans-serif), and localStorage persistence (not linked in nav) |
### Components (`src/components/`)
@@ -71,9 +72,9 @@ A brief description of each file in the codebase.
### Styles (`src/styles/`)
| File | Description |
| ------------ | ---------------------------------------------------------------- |
| `global.css` | Global CSS with theme variables, font config for all four themes |
| File | Description |
| ------------ | ------------------------------------------------------------------------------------ |
| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes |
## Convex Backend (`convex/`)

View File

@@ -62,6 +62,7 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
- Copy to ChatGPT, Claude, and Perplexity sharing
- Generate Skill option for AI agent training
- View as Markdown option in share dropdown
- Markdown writing page at `/write` with frontmatter reference
## Who this is for

View File

@@ -7,6 +7,82 @@ Date: 2025-12-21
All notable changes to this project.
## v1.15.2
Released December 20, 2025
**Write page font switcher**
- Font switcher in `/write` page Actions section
- Toggle between Serif and Sans-serif fonts in the writing area
- Font preference saved to localStorage and persists across sessions
- Uses same font families defined in global.css
## v1.15.1
Released December 20, 2025
**Write page theme and content fixes**
- Fixed theme toggle icons on `/write` page to match `ThemeToggle.tsx` (Moon, Sun, Half2Icon, Cloud)
- Content type switching now always updates the template in the writing area
## v1.15.0
Released December 20, 2025
**Write page three-column layout**
- Redesigned `/write` page with Cursor docs-style three-column layout
- Left sidebar: content type selector (Blog Post/Page) and action buttons (Clear, Theme)
- Center: full-screen writing area with Copy All button
- Right sidebar: frontmatter field reference with individual copy buttons for each field
- Warning message about refresh losing content
- Stats bar showing words, lines, and characters
## v1.14.0
Released December 20, 2025
**Write page Notion-like UI**
- Redesigned `/write` page with full-screen, distraction-free writing experience
- Floating header with home link, type selector, and action buttons
- Collapsible frontmatter panel on the right
- Removed borders from writing area for cleaner look
- Improved typography and spacing
## v1.13.0
Released December 20, 2025
**Markdown write page**
- New `/write` page for drafting markdown content (not linked in navigation)
- Content type selector for Blog Post or Page with appropriate frontmatter templates
- Frontmatter reference with copy buttons for each field
- Theme toggle matching site themes
- Word, line, and character counts
- localStorage persistence for content, type, and font preference
- Works with Grammarly and browser spellcheck
- Copy all button for easy content transfer
- Clear button to reset content
Access at `yourdomain.com/write`. Content stored in localStorage only.
## v1.12.2
Released December 20, 2025
**Centralized font-size CSS variables**
- All font sizes now use CSS variables for easier customization
- Base scale from `--font-size-3xs` (10px) to `--font-size-hero` (64px)
- Component-specific variables for blog headings, navigation, search, stats, and more
- Mobile responsive overrides at 768px breakpoint
Edit `src/styles/global.css` to customize font sizes across the entire site by changing the `:root` variables.
## v1.12.1
Released December 20, 2025

View File

@@ -394,6 +394,22 @@ body {
}
```
### Font Sizes
All font sizes use CSS variables in `:root`. Customize by editing:
```css
:root {
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
}
```
Mobile sizes defined in `@media (max-width: 768px)` block.
### Images
| Image | Location | Size |

View File

@@ -700,6 +700,29 @@ body {
}
```
### Change Font Sizes
All font sizes use CSS variables defined in `:root`. Customize sizes by editing these variables in `src/styles/global.css`:
```css
:root {
/* Base size scale */
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-xl: 18px;
--font-size-2xl: 20px;
--font-size-3xl: 24px;
/* Component-specific (examples) */
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
--font-size-nav-link: 14px;
}
```
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block.
### Add Static Pages (Optional)
Create optional pages like About, Projects, or Contact. These appear as navigation links in the top right corner.
@@ -941,6 +964,32 @@ markdown-site/
└── package.json # Dependencies
```
## Write Page
A markdown writing page is available at `/write` (not linked in navigation). Use it to draft content before saving to your markdown files.
**Features:**
- Three-column Cursor docs-style layout
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
- Frontmatter field reference with individual copy buttons
- Font switcher (Serif/Sans-serif)
- Theme toggle matching site themes
- Word, line, and character counts
- localStorage persistence for content, type, and font preference
- Works with Grammarly and browser spellcheck
**Workflow:**
1. Go to `yourdomain.com/write`
2. Select content type (Blog Post or Page)
3. Write your content using the frontmatter reference
4. Click "Copy All" to copy the markdown
5. Save to `content/blog/` or `content/pages/`
6. Run `npm run sync` or `npm run sync:prod`
Content is stored in localStorage only and not synced to the database. Refreshing the page preserves your content, but clearing browser data will lose it.
## Next Steps
After deploying:

View File

@@ -1,8 +1,9 @@
import { Routes, Route } from "react-router-dom";
import { Routes, Route, useLocation } from "react-router-dom";
import Home from "./pages/Home";
import Post from "./pages/Post";
import Stats from "./pages/Stats";
import Blog from "./pages/Blog";
import Write from "./pages/Write";
import Layout from "./components/Layout";
import { usePageTracking } from "./hooks/usePageTracking";
import siteConfig from "./config/siteConfig";
@@ -10,6 +11,12 @@ import siteConfig from "./config/siteConfig";
function App() {
// Track page views and active sessions
usePageTracking();
const location = useLocation();
// Write page renders without Layout (no header, full-screen writing)
if (location.pathname === "/write") {
return <Write />;
}
return (
<Layout>

411
src/pages/Write.tsx Normal file
View File

@@ -0,0 +1,411 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { Link } from "react-router-dom";
import {
CopySimple,
Check,
Trash,
House,
Article,
File,
Warning,
TextAa,
} from "@phosphor-icons/react";
import { Moon, Sun, Cloud } from "lucide-react";
import { Half2Icon } from "@radix-ui/react-icons";
import { useTheme } from "../context/ThemeContext";
// Frontmatter field definitions for blog posts
const POST_FIELDS = [
{ name: "title", required: true, example: '"Your Post Title"' },
{
name: "description",
required: true,
example: '"A brief description for SEO"',
},
{ name: "date", required: true, example: '"2025-01-20"' },
{ name: "slug", required: true, example: '"your-post-url"' },
{ name: "published", required: true, example: "true" },
{ name: "tags", required: true, example: '["tag1", "tag2"]' },
{ name: "readTime", required: false, example: '"5 min read"' },
{ name: "image", required: false, example: '"/images/my-image.png"' },
{
name: "excerpt",
required: false,
example: '"Short description for cards"',
},
{ name: "featured", required: false, example: "true" },
{ name: "featuredOrder", required: false, example: "1" },
];
// Frontmatter field definitions for pages
const PAGE_FIELDS = [
{ name: "title", required: true, example: '"Page Title"' },
{ name: "slug", required: true, example: '"page-url"' },
{ name: "published", required: true, example: "true" },
{ name: "order", required: false, example: "1" },
{ name: "excerpt", required: false, example: '"Short description"' },
{ name: "image", required: false, example: '"/images/thumbnail.png"' },
{ name: "featured", required: false, example: "true" },
{ name: "featuredOrder", required: false, example: "1" },
];
// Generate frontmatter template based on content type
function generateTemplate(type: "post" | "page"): string {
if (type === "post") {
return `---
title: "Your Post Title"
description: "A brief description for SEO and social sharing"
date: "${new Date().toISOString().split("T")[0]}"
slug: "your-post-url"
published: true
tags: ["tag1", "tag2"]
readTime: "5 min read"
---
# Your Post Title
Start writing your content here...
## Section Heading
Add your markdown content. You can use:
- **Bold text** and *italic text*
- [Links](https://example.com)
- Code blocks with syntax highlighting
\`\`\`typescript
const greeting = "Hello, world";
console.log(greeting);
\`\`\`
## Conclusion
Wrap up your thoughts here.
`;
}
return `---
title: "Page Title"
slug: "page-url"
published: true
order: 1
---
# Page Title
Your page content goes here...
## Section
Add your markdown content.
`;
}
// localStorage keys
const STORAGE_KEY_CONTENT = "markdown_write_content";
const STORAGE_KEY_TYPE = "markdown_write_type";
const STORAGE_KEY_FONT = "markdown_write_font";
// Font family definitions (matches global.css options)
type FontType = "serif" | "sans";
const FONTS: Record<FontType, string> = {
serif:
'"New York", -apple-system-ui-serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif',
};
// Get the appropriate icon for current theme (matches ThemeToggle.tsx)
function getThemeIcon(theme: string) {
switch (theme) {
case "dark":
return <Moon size={18} />;
case "light":
return <Sun size={18} />;
case "tan":
return <Half2Icon style={{ width: 18, height: 18 }} />;
case "cloud":
return <Cloud size={18} />;
default:
return <Sun size={18} />;
}
}
export default function Write() {
const { theme, toggleTheme } = useTheme();
const [contentType, setContentType] = useState<"post" | "page">("post");
const [content, setContent] = useState("");
const [copied, setCopied] = useState(false);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [font, setFont] = useState<FontType>("serif");
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Load from localStorage on mount
useEffect(() => {
const savedContent = localStorage.getItem(STORAGE_KEY_CONTENT);
const savedType = localStorage.getItem(STORAGE_KEY_TYPE) as
| "post"
| "page"
| null;
const savedFont = localStorage.getItem(STORAGE_KEY_FONT) as FontType | null;
if (savedContent) {
setContent(savedContent);
} else {
setContent(generateTemplate("post"));
}
if (savedType) {
setContentType(savedType);
}
if (savedFont && (savedFont === "serif" || savedFont === "sans")) {
setFont(savedFont);
}
}, []);
// Save to localStorage on content change
useEffect(() => {
localStorage.setItem(STORAGE_KEY_CONTENT, content);
}, [content]);
// Save type to localStorage
useEffect(() => {
localStorage.setItem(STORAGE_KEY_TYPE, contentType);
}, [contentType]);
// Save font preference to localStorage
useEffect(() => {
localStorage.setItem(STORAGE_KEY_FONT, font);
}, [font]);
// Toggle font between serif and sans-serif
const toggleFont = useCallback(() => {
setFont((prev) => (prev === "serif" ? "sans" : "serif"));
}, []);
// Handle type change and update content template
const handleTypeChange = (newType: "post" | "page") => {
if (newType === contentType) return;
setContentType(newType);
// Always update to the new template when switching types
setContent(generateTemplate(newType));
};
// Copy content to clipboard
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for older browsers
const textarea = document.createElement("textarea");
textarea.value = content;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}, [content]);
// Copy a single frontmatter field to clipboard
const handleCopyField = useCallback(
async (fieldName: string, example: string) => {
const fieldText = `${fieldName}: ${example}`;
try {
await navigator.clipboard.writeText(fieldText);
setCopiedField(fieldName);
setTimeout(() => setCopiedField(null), 1500);
} catch {
// Fallback
const textarea = document.createElement("textarea");
textarea.value = fieldText;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
setCopiedField(fieldName);
setTimeout(() => setCopiedField(null), 1500);
}
},
[],
);
// Clear content and reset to template
const handleClear = useCallback(() => {
setContent(generateTemplate(contentType));
if (textareaRef.current) {
textareaRef.current.focus();
}
}, [contentType]);
// Calculate stats
const lines = content.split("\n").length;
const characters = content.length;
const words = content.trim() ? content.trim().split(/\s+/).length : 0;
const fields = contentType === "post" ? POST_FIELDS : PAGE_FIELDS;
return (
<div className="write-layout">
{/* Left Sidebar: Type selector */}
<aside className="write-sidebar-left">
<div className="write-sidebar-header">
<Link to="/" className="write-logo-link" title="Back to home">
<House size={20} weight="regular" />
<span>Home</span>
</Link>
</div>
<nav className="write-nav">
<div className="write-nav-section">
<span className="write-nav-label">Content Type</span>
<button
onClick={() => handleTypeChange("post")}
className={`write-nav-item ${contentType === "post" ? "active" : ""}`}
>
<Article
size={18}
weight={contentType === "post" ? "fill" : "regular"}
/>
<span>Blog Post</span>
</button>
<button
onClick={() => handleTypeChange("page")}
className={`write-nav-item ${contentType === "page" ? "active" : ""}`}
>
<File
size={18}
weight={contentType === "page" ? "fill" : "regular"}
/>
<span>Page</span>
</button>
</div>
<div className="write-nav-section">
<span className="write-nav-label">Actions</span>
<button onClick={handleClear} className="write-nav-item">
<Trash size={18} />
<span>Clear</span>
</button>
<button onClick={toggleTheme} className="write-nav-item">
{getThemeIcon(theme)}
<span>Theme</span>
</button>
<button onClick={toggleFont} className="write-nav-item">
<TextAa size={18} />
<span>{font === "serif" ? "Serif" : "Sans"}</span>
</button>
</div>
</nav>
{/* Warning about refresh */}
<div className="write-warning">
<Warning size={14} />
<span>Refresh loses content</span>
</div>
</aside>
{/* Main writing area */}
<main className="write-main">
<div className="write-main-header">
<h1 className="write-main-title">
{contentType === "post" ? "Blog Post" : "Page"}
</h1>
<button
onClick={handleCopy}
className={`write-copy-btn ${copied ? "copied" : ""}`}
>
{copied ? (
<>
<Check size={16} weight="bold" />
<span>Copied</span>
</>
) : (
<>
<CopySimple size={16} />
<span>Copy All</span>
</>
)}
</button>
</div>
<textarea
ref={textareaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
className="write-textarea"
placeholder="Start writing your markdown..."
spellCheck={true}
autoComplete="off"
autoCapitalize="sentences"
autoFocus
style={{ fontFamily: FONTS[font] }}
/>
{/* Footer with stats */}
<div className="write-main-footer">
<div className="write-stats">
<span>{words} words</span>
<span className="write-stats-divider" />
<span>{lines} lines</span>
<span className="write-stats-divider" />
<span>{characters} chars</span>
</div>
<div className="write-save-hint">
Save to{" "}
<code>content/{contentType === "post" ? "blog" : "pages"}/</code>{" "}
then <code>npm run sync</code>
</div>
</div>
</main>
{/* Right Sidebar: Frontmatter fields */}
<aside className="write-sidebar-right">
<div className="write-sidebar-header">
<span className="write-sidebar-title">Frontmatter</span>
</div>
<div className="write-fields">
<div className="write-fields-section">
<span className="write-fields-label">
{contentType === "post" ? "Blog Post" : "Page"} Fields
</span>
{fields.map((field) => (
<div key={field.name} className="write-field-row">
<div className="write-field-info">
<code className="write-field-name">
{field.name}
{field.required && (
<span className="write-field-required">*</span>
)}
</code>
<span className="write-field-example">{field.example}</span>
</div>
<button
onClick={() => handleCopyField(field.name, field.example)}
className={`write-field-copy ${copiedField === field.name ? "copied" : ""}`}
title={`Copy ${field.name}`}
>
{copiedField === field.name ? (
<Check size={14} weight="bold" />
) : (
<CopySimple size={14} />
)}
</button>
</div>
))}
</div>
<div className="write-fields-note">
<span className="write-field-required">*</span> Required fields
</div>
</div>
</aside>
</div>
);
}

File diff suppressed because it is too large Load Diff