initial commit

This commit is contained in:
grassblock 2025-05-01 16:53:18 +08:00
commit 61511ed28c
28 changed files with 5210 additions and 0 deletions

52
src/pages/blog.astro Normal file
View file

@ -0,0 +1,52 @@
---
import Layout from '../layouts/Layout.astro';
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
posts.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime());
---
<Layout title="Blog | Terminal Blog" path="~/grassblock/micr0blog/blog">
<h1 class="post-title">~/blog</h1>
<div class="post-content">
<p class="typewriter">Posts from the terminal.</p>
<div style="margin-top: 2rem;">
<span class="command">ls -la posts/</span>
<div style="margin-top: 1rem; margin-left: 1rem;">
{posts.map((post) => (
<p>
<span style="color: var(--terminal-yellow);">{new Date(post.data.pubDate).toISOString().split('T')[0]}</span>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</p>
))}
{posts.length === 0 && (
<>
<p>
<span style="color: var(--terminal-yellow);">2025-06-08</span>
<a href="/blog/terminal-setup">My Terminal Setup</a>
</p>
<p>
<span style="color: var(--terminal-yellow);">2025-06-05</span>
<a href="/blog/minimalism">The Art of Minimalism</a>
</p>
<p>
<span style="color: var(--terminal-yellow);">2025-06-01</span>
<a href="/blog/first-post">First Post</a>
</p>
</>
)}
</div>
</div>
<div style="margin-top: 2rem;">
<p>
<span class="command">cat rss.txt</span>
<br />
<a href="/rss.xml" style="margin-left: 1rem;">Subscribe to RSS feed</a>
</p>
</div>
</div>
</Layout>

View file

@ -0,0 +1,77 @@
---
import Layout from '../../layouts/Layout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
// Sample content for demo purposes if no actual content collection is set up
const samplePosts = {
'terminal-setup': {
title: 'My Terminal Setup',
date: '2025-06-08',
content: `
<p>Here's my current terminal setup:</p>
<ul>
<li>Shell: ZSH with Oh My Zsh</li>
<li>Terminal: Alacritty</li>
<li>Color Scheme: Nord</li>
<li>Font: JetBrains Mono</li>
</ul>
<p>I've been using this setup for about a year now and it's been working great for me.</p>
`
},
'minimalism': {
title: 'The Art of Minimalism',
date: '2025-06-05',
content: `
<p>Minimalism isn't just about having less, it's about making room for what matters.</p>
<p>In code, this means writing clean, maintainable code that does exactly what it needs to do, nothing more, nothing less.</p>
<p>This terminal blog is an exercise in digital minimalism - stripping away the unnecessary to focus on what's important: the content.</p>
`
},
'first-post': {
title: 'First Post',
date: '2025-06-01',
content: `
<p>Hello world! This is the first post on my new terminal blog.</p>
<p>I built this blog using Astro and vanilla CSS/JS to create a terminal-like experience.</p>
<p>More posts coming soon...</p>
`
}
};
const slug = Astro.params.slug;
---
<Layout
title={entry ? entry.data.title : samplePosts[slug]?.title}
path={`~/grassblock/micr0blog/blog/${slug}`}
>
{entry ? (
<>
<h1 class="post-title">{entry.data.title}</h1>
<span class="post-date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
<div class="post-content">
<Content />
</div>
</>
) : (
<>
<h1 class="post-title">{samplePosts[slug]?.title}</h1>
<span class="post-date">{samplePosts[slug]?.date}</span>
<div class="post-content" set:html={samplePosts[slug]?.content}></div>
</>
)}
<div style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
<a href="/blog">&larr; Back to posts</a>
</div>
</Layout>

33
src/pages/index.astro Normal file
View file

@ -0,0 +1,33 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Home | Terminal Blog">
<h1 class="post-title">~/home</h1>
<div class="post-content">
<p class="typewriter">A random grassblock do some some writing work.</p>
<p class="typewriter" style="margin-top: 1.5rem;">Welcome to my terminal blog. Navigate using the links above.</p>
<div style="margin-top: 2rem;">
<span class="command">ls -la</span>
<div style="margin-top: 0.5rem; margin-left: 1rem;">
<p>drwxr-xr-x 3 user group 96 Jun 8 15:42 .</p>
<p>drwxr-xr-x 15 user group 480 Jun 8 14:22 ..</p>
<p>-rw-r--r-- 1 user group 283 Jun 8 15:42 about.md</p>
<p>-rw-r--r-- 1 user group 148 Jun 8 15:40 projects.md</p>
<p>-rw-r--r-- 1 user group 892 Jun 8 15:35 notes.md</p>
</div>
</div>
<div style="margin-top: 2rem;">
<span class="command">cat about.md</span>
<div style="margin-top: 0.5rem; margin-left: 1rem;">
<p>
I'm a developer who enjoys minimalist design and terminal aesthetics.
This blog is a collection of my thoughts, projects, and experiments.
</p>
</div>
</div>
</div>
</Layout>

30
src/pages/lab.astro Normal file
View file

@ -0,0 +1,30 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Lab | Terminal Blog" path="~/grassblock/micr0blog/lab">
<h1 class="post-title">~/lab</h1>
<div class="post-content">
<p class="typewriter">This is where experiments happen.</p>
<div style="margin-top: 2rem;">
<span class="command">ls -la experiments/</span>
<div style="margin-top: 1rem; margin-left: 1rem;">
<p><a href="/lab/experiment-1">Terminal Text Effects</a></p>
<p><a href="/lab/experiment-2">ASCII Art Generator</a></p>
<p><a href="/lab/experiment-3">Command Line Games</a></p>
</div>
</div>
<div style="margin-top: 2rem;">
<span class="command">cat README.md</span>
<div style="margin-top: 0.5rem; margin-left: 1rem;">
<p>
The lab is a space for experimental projects and ideas.
Feel free to explore, but be cautious as things might break.
</p>
</div>
</div>
</div>
</Layout>

410
src/pages/lab/[slug].astro Normal file
View file

@ -0,0 +1,410 @@
---
import Layout from '../../layouts/Layout.astro';
export function getStaticPaths() {
return [
{ params: { slug: 'experiment-1' } },
{ params: { slug: 'experiment-2' } },
{ params: { slug: 'experiment-3' } }
];
}
const { slug } = Astro.params;
const experiments = {
'experiment-1': {
title: 'Terminal Text Effects',
content: `
<p class="typewriter">This is a demonstration of terminal-like text effects.</p>
<div style="margin-top: 2rem;">
<p id="rainbow-text">This text will change colors like a rainbow.</p>
</div>
<div style="margin-top: 2rem;">
<p id="glitch-text">This text will occasionally glitch.</p>
</div>
`,
script: `
// Rainbow text effect
const rainbowText = document.getElementById('rainbow-text');
if (rainbowText) {
const colors = ['#ff5555', '#ffb86c', '#f1fa8c', '#50fa7b', '#8be9fd', '#bd93f9', '#ff79c6'];
let colorIndex = 0;
setInterval(() => {
rainbowText.style.color = colors[colorIndex];
colorIndex = (colorIndex + 1) % colors.length;
}, 1000);
}
// Glitch text effect
const glitchText = document.getElementById('glitch-text');
if (glitchText) {
const originalText = glitchText.textContent;
const glitchChars = '!@#$%^&*()_+-=[]{}|;:,.<>?/\\\\';
setInterval(() => {
if (Math.random() > 0.9) {
let glitchedText = '';
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > 0.9) {
glitchedText += glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitchedText += originalText[i];
}
}
glitchText.textContent = glitchedText;
setTimeout(() => {
glitchText.textContent = originalText;
}, 100);
}
}, 2000);
}
`
},
'experiment-2': {
title: 'ASCII Art Generator',
content: `
<p class="typewriter">Generate ASCII art from text.</p>
<div style="margin-top: 2rem;">
<input type="text" id="ascii-input" placeholder="Enter text" class="terminal-input" />
<button id="generate-btn" class="terminal-btn">Generate</button>
</div>
<div style="margin-top: 1rem; white-space: pre; font-family: monospace; overflow-x: auto;" id="ascii-output">
</div>
`,
script: `
const generateBtn = document.getElementById('generate-btn');
const asciiInput = document.getElementById('ascii-input');
const asciiOutput = document.getElementById('ascii-output');
if (generateBtn && asciiInput && asciiOutput) {
generateBtn.addEventListener('click', () => {
const text = asciiInput.value.trim();
if (!text) return;
const fontStyles = [
generateSimpleAscii,
generateBlockAscii,
generateShadowAscii
];
const style = fontStyles[Math.floor(Math.random() * fontStyles.length)];
asciiOutput.textContent = style(text);
});
}
function generateSimpleAscii(text) {
return \`
_____ _____ _____ _____ _____ _____
|_____||_____||_____||_____||_____||_____|
| ${text.split('').join(' | ')} |
|_____|_____|_____|_____|_____|_____|
\`;
}
function generateBlockAscii(text) {
return \`
██████╗ ${text.split('').map(() => '██████╗ ').join('')}
██╔════╝ ${text.split('').map(() => '██╔══██╗').join('')}
██║ ███╗${text.split('').map(() => '██████╔╝').join('')}
██║ ██║${text.split('').map(() => '██╔══██╗').join('')}
╚██████╔╝${text.split('').map(() => '██████╔╝').join('')}
╚═════╝ ${text.split('').map(() => '╚═════╝ ').join('')}
\`;
}
function generateShadowAscii(text) {
return \`
░░░░░░${text.split('').map(() => '░░░░░░').join('')}
▒░ ▒░${text.split('').map(() => '▒░ ▒░').join('')}
▒▒ ▒▒${text.split('').map(() => '▒▒ ▒▒').join('')}
▓▒ ▒▓${text.split('').map(() => '▓▒ ▒▓').join('')}
▓▓▓▓▓▓ ${text.split('').map(() => '▓▓▓▓▓▓ ').join('')}
\`;
}
`,
style: `
.terminal-input {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.5rem;
font-family: var(--font-mono);
margin-right: 0.5rem;
}
.terminal-btn {
background-color: var(--border-color);
border: none;
color: var(--text-color);
padding: 0.5rem 1rem;
font-family: var(--font-mono);
cursor: pointer;
transition: background-color 0.2s;
}
.terminal-btn:hover {
background-color: var(--accent-color);
color: var(--bg-color);
}
`
},
'experiment-3': {
title: 'Command Line Games',
content: `
<p class="typewriter">Try a simple command line game.</p>
<div style="margin-top: 2rem;">
<p>Enter a command:</p>
<div style="display: flex; margin-top: 0.5rem;">
<span class="command" style="margin-right: 0;">play</span>
<input type="text" id="game-input" class="terminal-input" style="flex-grow: 1;" />
</div>
<div id="game-output" style="margin-top: 1rem; white-space: pre-wrap; font-family: monospace;">
Available games: number-guess, rock-paper-scissors, hangman
</div>
</div>
`,
script: `
const gameInput = document.getElementById('game-input');
const gameOutput = document.getElementById('game-output');
let currentGame = null;
let gameState = {};
if (gameInput && gameOutput) {
gameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const command = gameInput.value.trim().toLowerCase();
gameInput.value = '';
processCommand(command);
}
});
}
function processCommand(command) {
if (!currentGame) {
// Starting a new game
if (command === 'number-guess') {
currentGame = 'number-guess';
gameState = {
target: Math.floor(Math.random() * 100) + 1,
attempts: 0
};
appendOutput('I\'m thinking of a number between 1 and 100.');
appendOutput('Enter your guess:');
} else if (command === 'rock-paper-scissors') {
currentGame = 'rock-paper-scissors';
appendOutput('Let\'s play Rock, Paper, Scissors!');
appendOutput('Enter rock, paper, or scissors:');
} else if (command === 'hangman') {
currentGame = 'hangman';
const words = ['javascript', 'terminal', 'computer', 'keyboard', 'program'];
gameState = {
word: words[Math.floor(Math.random() * words.length)],
guessed: [],
attempts: 0
};
appendOutput('Let\'s play Hangman!');
appendOutput(\`The word has \${gameState.word.length} letters.\`);
appendOutput(getHangmanDisplay());
} else {
appendOutput(\`Unknown game: \${command}\`);
appendOutput('Available games: number-guess, rock-paper-scissors, hangman');
}
} else if (currentGame === 'number-guess') {
const guess = parseInt(command);
if (isNaN(guess)) {
appendOutput('Please enter a valid number.');
return;
}
gameState.attempts++;
if (guess === gameState.target) {
appendOutput(\`Congratulations! You guessed it in \${gameState.attempts} attempts.\`);
currentGame = null;
appendOutput('\\nAvailable games: number-guess, rock-paper-scissors, hangman');
} else if (guess < gameState.target) {
appendOutput('Too low! Try again:');
} else {
appendOutput('Too high! Try again:');
}
} else if (currentGame === 'rock-paper-scissors') {
const choices = ['rock', 'paper', 'scissors'];
if (!choices.includes(command)) {
appendOutput('Please enter rock, paper, or scissors.');
return;
}
const computerChoice = choices[Math.floor(Math.random() * choices.length)];
appendOutput(\`You chose \${command}. Computer chose \${computerChoice}.\`);
if (command === computerChoice) {
appendOutput('It\'s a tie!');
} else if (
(command === 'rock' && computerChoice === 'scissors') ||
(command === 'paper' && computerChoice === 'rock') ||
(command === 'scissors' && computerChoice === 'paper')
) {
appendOutput('You win!');
} else {
appendOutput('Computer wins!');
}
appendOutput('Play again? Enter rock, paper, or scissors, or type "exit" to quit:');
if (command === 'exit') {
currentGame = null;
appendOutput('\\nAvailable games: number-guess, rock-paper-scissors, hangman');
}
} else if (currentGame === 'hangman') {
if (command === 'exit') {
currentGame = null;
appendOutput(\`The word was: \${gameState.word}\`);
appendOutput('\\nAvailable games: number-guess, rock-paper-scissors, hangman');
return;
}
if (command.length !== 1) {
appendOutput('Please enter a single letter.');
return;
}
const letter = command.toLowerCase();
if (gameState.guessed.includes(letter)) {
appendOutput('You already guessed that letter!');
return;
}
gameState.guessed.push(letter);
if (!gameState.word.includes(letter)) {
gameState.attempts++;
}
appendOutput(getHangmanDisplay());
const wordDisplay = gameState.word
.split('')
.map(char => gameState.guessed.includes(char) ? char : '_')
.join(' ');
appendOutput(\`Word: \${wordDisplay}\`);
appendOutput(\`Guessed: \${gameState.guessed.join(', ')}\`);
if (!wordDisplay.includes('_')) {
appendOutput('Congratulations! You guessed the word!');
currentGame = null;
appendOutput('\\nAvailable games: number-guess, rock-paper-scissors, hangman');
} else if (gameState.attempts >= 6) {
appendOutput(\`Game over! The word was: \${gameState.word}\`);
currentGame = null;
appendOutput('\\nAvailable games: number-guess, rock-paper-scissors, hangman');
}
}
}
function getHangmanDisplay() {
const hangmanStages = [
\`
+---+
| |
|
|
|
|
=========\`,
\`
+---+
| |
O |
|
|
|
=========\`,
\`
+---+
| |
O |
| |
|
|
=========\`,
\`
+---+
| |
O |
/| |
|
|
=========\`,
\`
+---+
| |
O |
/|\\ |
|
|
=========\`,
\`
+---+
| |
O |
/|\\ |
/ |
|
=========\`,
\`
+---+
| |
O |
/|\\ |
/ \\ |
|
=========\`
];
return hangmanStages[Math.min(gameState.attempts, 6)];
}
function appendOutput(text) {
gameOutput.textContent += '\\n' + text;
gameOutput.scrollTop = gameOutput.scrollHeight;
}
`,
style: `
.terminal-input {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.5rem;
font-family: var(--font-mono);
}
`
}
};
---
<Layout title={`Lab | ${experiments[slug]?.title}`} path={`~/grassblock/micr0blog/lab/${slug}`}>
<h1 class="post-title">{experiments[slug]?.title}</h1>
<div class="post-content">
<div set:html={experiments[slug]?.content}></div>
</div>
<div style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
<a href="/lab">&larr; Back to lab</a>
</div>
{experiments[slug]?.style && (
<style set:html={experiments[slug]?.style}></style>
)}
{experiments[slug]?.script && (
<script set:html={experiments[slug]?.script}></script>
)}
</Layout>

18
src/pages/rss.xml.js Normal file
View file

@ -0,0 +1,18 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'Terminal Blog',
description: 'A random grassblock do some some writing work.',
site: context.site,
items: posts.map((post) => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.pubDate,
link: `/blog/${post.slug}/`,
content: post.body,
})),
});
}

View file

@ -0,0 +1,18 @@
import { getCollection } from 'astro:content';
export async function GET() {
const posts = await getCollection('blog');
const searchIndex = posts.map(post => ({
title: post.data.title,
description: post.data.description,
content: post.body,
pubDate: post.data.pubDate,
slug: post.slug
}));
return new Response(JSON.stringify(searchIndex), {
headers: {
'Content-Type': 'application/json'
}
});
}