feat: an editor for creating posts quickly

This commit is contained in:
草师傅 2025-08-26 21:47:19 +08:00
parent e96c7d89db
commit d1ae051cae
Signed by: gb
GPG key ID: 43330A030E2D6478
4 changed files with 1275 additions and 0 deletions

View file

@ -35,6 +35,7 @@
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808", "packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808",
"devDependencies": { "devDependencies": {
"@azure/static-web-apps-cli": "^2.0.6", "@azure/static-web-apps-cli": "^2.0.6",
"@milkdown/crepe": "^7.15.5",
"@types/node": "^22.15.3", "@types/node": "^22.15.3",
"@waline/client": "^3.6.0" "@waline/client": "^3.6.0"
} }

1093
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

98
src/pages/editor.astro Normal file
View file

@ -0,0 +1,98 @@
---
import "@milkdown/crepe/theme/common/style.css";
import "/src/styles/editor.css";
import Layout from "../layouts/Layout.astro";
import {getCollection} from "astro:content";
const allPosts = await getCollection('posts', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
const uniqueCategories = [...new Set(allPosts.map((post: any) => post.data.categories ? post.data.categories : []).flat())];
---
<Layout title="Create a new post">
{/* in memory of https://github.com/KeJunMao/jekyll-theme-mdui */}
<h1 class="title">Create a new post</h1>
<div class="content">
<form class="form">
<label for="title-input">Title</label>
<input type="text" placeholder="What is your post title?" id="title-input" required />
<label for="slug-input">Slug</label>
<input type="text" placeholder="The parmallink will be /blog/:slug" id="slug-input" required />
<label for="description-input">Description</label>
<textarea placeholder="What is your description?" id="description-input" />
<label for="category-input">Category</label>
<input list="categories" id="category-input" name="category-input" placeholder="Select or type a category" />
<datalist id="categories">
{uniqueCategories.map((category) => (
<option value={category} />
))}
</datalist>
<label for="tags-input">Tags (comma separated)</label>
<input type="text" placeholder="e.g. tag1, tag2, tag3" id="tags-input" />
<div class="content-field">
<div id="editor"></div>
<p><small>powered by <a href="https://milkdown.dev">milkdown</a></small></p>
</div>
<button type="button" id="download-btn">Download Markdown</button>
</form>
</div>
<script>
import { Crepe } from "@milkdown/crepe";
const crepe = new Crepe({
root: "#editor",
defaultValue: "",
features: {
[Crepe.Feature.Toolbar]: true,
[Crepe.Feature.Latex]: true,
},
featureConfigs: {
[Crepe.Feature.Placeholder]: {
text: 'Start writing...',
mode: 'block',
},
},
});
// Get markdown content
let markdown = ""
crepe.create();
crepe.on((listener) => {
listener.markdownUpdated((ctx, md) => {
console.log("Content updated:", markdown);
});
})
// Download functionality
document.getElementById('download-btn').addEventListener('click',() => {
const title = document.getElementById('title-input').value || 'untitled';
const description = document.getElementById('description-input').value
const slug = document.getElementById('slug-input').value
const content = crepe.getMarkdown();
const category = document.getElementById('category-input').value || 'uncategorized';
const tags = [document.getElementById('tags-input').value.split(',').map((tag: string)=> tag.trim()).filter((tag: string) => tag)];
const date = new Date();
// Create markdown content with title
const markdownContent = `---\ntitle: ${title}\ndescription: ${description}\ncategories: ${category}\ntags: ${tags}\nslug: ${slug}\ndate: ${date}\ndraft: true\n---\n\n${content}`;
// Create blob and download
const blob = new Blob([markdownContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${slug.toLowerCase()}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
</script>
</Layout>

83
src/styles/editor.css Normal file
View file

@ -0,0 +1,83 @@
form label {
display: block;
margin: 1rem auto;
}
form input {
flex: 1;
padding: 0.5rem;
width: 100%;
border: unset;
border-bottom: 1px solid var(--border-color);
font-size: .8rem;
background-color: var(--bg-color);
color: var(--text-color);
}
form textarea {
flex: 1;
padding: 0.5rem;
border: 1px solid var(--border-color);
font-size: .8rem;
background-color: var(--bg-color);
color: var(--text-color);
width: 100%;
min-height: 100px;
resize: vertical;
}
form input:focus,textarea:focus {
border-radius: 0;
outline: unset;
border: 2px solid var(--accent-color);
}
form div.content-field {
margin: 1rem auto;
}
.milkdown {
--crepe-color-background: var(--bg-color);
--crepe-color-on-background: var(--text-color);
--crepe-color-surface: var(--text-color);
--crepe-color-surface-low: var(--secondary-color);
--crepe-color-on-surface: var(--secondary-text-color);
--crepe-color-on-surface-variant: var(--secondary-text-color);
--crepe-color-outline: var(--border-color);
--crepe-color-primary: var(--accent-color);
--crepe-color-secondary: var(--secondary-color);
--crepe-color-on-secondary: var(--secondary-text-color);
--crepe-color-inverse: var(--secondary-text-color);
--crepe-color-on-inverse: var(--bg-color);
--crepe-color-inline-code: var(--terminal-green);
--crepe-color-error: var(--terminal-red);
--crepe-color-hover: var(--secondary-color);
--crepe-color-selected: var(--secondary-color);
--crepe-color-inline-area: var(--text-color);
--crepe-font-title: Rubik, Cambria, 'Times New Roman', Times, serif;
--crepe-font-default: Inter, Arial, Helvetica, sans-serif;
--crepe-font-code:
'JetBrains Mono', Menlo, Monaco, 'Courier New', Courier, monospace;
--crepe-shadow-1:
0px 1px 2px 0px rgba(255, 255, 255, 0.3),
0px 1px 3px 1px rgba(255, 255, 255, 0.15);
--crepe-shadow-2:
0px 1px 2px 0px rgba(255, 255, 255, 0.3),
0px 2px 6px 2px rgba(255, 255, 255, 0.15);
border: .1rem solid var(--border-color);
}
form button#download-btn {
background-color: var(--accent-color);
color: var(--bg-color);
border: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
margin-top: 1rem;
}