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

1
src/assets/astro.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

121
src/components/Search.astro Normal file
View file

@ -0,0 +1,121 @@
---
---
<div class="search-container">
<div class="command-prompt">
<span class="command">search</span>
<input
type="text"
id="search-input"
class="search-input"
placeholder="Type to search..."
autocomplete="off"
/>
</div>
<div id="search-results" class="search-results"></div>
</div>
<script>
import Fuse from 'fuse.js';
let fuse;
let posts = [];
async function initializeSearch() {
const response = await fetch('/search-index.json');
posts = await response.json();
fuse = new Fuse(posts, {
keys: ['title', 'description', 'content'],
threshold: 0.3,
includeMatches: true
});
}
function performSearch(query) {
if (!query) {
document.getElementById('search-results').innerHTML = '';
return;
}
const results = fuse.search(query);
const resultsElement = document.getElementById('search-results');
if (results.length === 0) {
resultsElement.innerHTML = '<p>No results found.</p>';
return;
}
const html = results
.map(result => `
<div class="search-result">
<a href="/blog/${result.item.slug}">
<span class="result-title">${result.item.title}</span>
<span class="result-date">${new Date(result.item.pubDate).toISOString().split('T')[0]}</span>
</a>
</div>
`)
.join('');
resultsElement.innerHTML = html;
}
// Initialize search when the component mounts
initializeSearch();
// Add event listener for search input
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
</script>
<style>
.search-container {
margin: 2rem 0;
}
.command-prompt {
display: flex;
align-items: center;
gap: 0.5rem;
}
.search-input {
background: transparent;
border: none;
color: var(--text-color);
font-family: var(--font-mono);
font-size: 1rem;
padding: 0.5rem;
width: 100%;
outline: none;
}
.search-results {
margin-top: 1rem;
margin-left: 1rem;
}
.search-result {
margin: 0.5rem 0;
}
.search-result a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
text-decoration: none;
color: var(--text-color);
border-radius: 4px;
}
.search-result a:hover {
background: var(--border-color);
}
.result-date {
color: var(--terminal-yellow);
font-size: 0.9rem;
}
</style>

View file

@ -0,0 +1,22 @@
---
---
<button class="theme-switcher" id="theme-switcher">
Toggle Theme
</button>
<script>
const themeSwitcher = document.getElementById('theme-switcher');
const root = document.documentElement;
// Check for saved theme preference or default to dark
const savedTheme = localStorage.getItem('theme') || 'dark';
root.setAttribute('data-theme', savedTheme);
themeSwitcher?.addEventListener('click', () => {
const currentTheme = root.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
</script>

View file

@ -0,0 +1,209 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
'DejaVu Sans Mono', monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>

View file

@ -0,0 +1,9 @@
import { z } from 'astro:content';
export const blogs = z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional()
});

View file

@ -0,0 +1,15 @@
---
title: 'First Post'
description: 'Welcome to my terminal blog'
pubDate: '2025-06-01'
---
Hello world! This is the first post on my new terminal blog.
I built this blog using Astro and vanilla CSS/JS to create a terminal-like experience.
```bash
echo "Hello, terminal world!"
```
More posts coming soon...

View file

@ -0,0 +1,24 @@
---
title: 'The Art of Minimalism'
description: 'Thoughts on minimalism in design and code'
pubDate: '2025-06-05'
---
Minimalism isn't just about having less, it's about making room for what matters.
In code, this means writing clean, maintainable code that does exactly what it needs to do, nothing more, nothing less.
This terminal blog is an exercise in digital minimalism - stripping away the unnecessary to focus on what's important: the content.
## Minimalist Principles in Code
1. **Do one thing well** - Functions and components should have a single responsibility
2. **Eliminate unnecessary state** - Less state means fewer bugs and easier reasoning
3. **Prefer immutability** - Immutable data structures lead to more predictable code
4. **Embrace constraints** - Limitations often lead to more creative solutions
## Terminal Aesthetics
There's something beautifully minimalist about terminal interfaces. They strip away the graphical excess and focus on pure functionality. Yet, within these constraints, there's a unique aesthetic that many find appealing.
The monospace fonts, the cursor blinking in the void, the clean, structured output - all of these elements combine to create an experience that's both functional and visually satisfying.

View file

@ -0,0 +1,37 @@
---
title: 'My Terminal Setup'
description: 'A walkthrough of my current terminal configuration'
pubDate: '2025-06-08'
---
Here's my current terminal setup:
- Shell: ZSH with Oh My Zsh
- Terminal: Alacritty
- Color Scheme: Nord
- Font: JetBrains Mono
I've been using this setup for about a year now and it's been working great for me.
## Configuration
My `.zshrc` has the following key customizations:
```bash
# Enable Powerlevel10k theme
ZSH_THEME="powerlevel10k/powerlevel10k"
# Enable useful plugins
plugins=(git npm node zsh-autosuggestions zsh-syntax-highlighting)
# Custom aliases
alias gs="git status"
alias gc="git commit -m"
alias gl="git log --oneline"
```
## Why I Love This Setup
The combination of ZSH, Oh My Zsh, and Powerlevel10k provides a powerful and visually appealing terminal experience. The autosuggestions and syntax highlighting plugins make command entry much more efficient.
Alacritty is fast and lightweight, and the Nord color scheme is easy on the eyes for long coding sessions.

11
src/content/config.ts Normal file
View file

@ -0,0 +1,11 @@
import { defineCollection } from 'astro:content';
import { blogs } from './blog/_schemas';
const blogCollection = defineCollection({
type: 'content',
schema: blogs,
});
export const collections = {
'blog': blogCollection,
};

51
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,51 @@
---
import '../styles/global.css';
import Search from '../components/Search.astro';
import ThemeSwitcher from '../components/ThemeSwitcher.astro';
interface Props {
title: string;
path?: string;
}
const { title, path = "~/grassblock/micr0blog" } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<ThemeSwitcher />
<main>
<div class="container">
<div class="terminal-path">
{path}
</div>
<nav class="nav">
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/lab">Lab</a>
</nav>
<Search />
<div class="content-box">
<slot />
</div>
</div>
</main>
<footer class="footer">
<div class="container">
Powered by <a href="#">Bear ƒ••?</a>
</div>
</footer>
</body>
</html>

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'
}
});
}

191
src/styles/global.css Normal file
View file

@ -0,0 +1,191 @@
/* Global Styles for Terminal Blog */
:root {
/* Dark theme (default) */
--bg-color: #1f2937;
--text-color: #a5b4cf;
--accent-color: #64a0ff;
--border-color: #3b4351;
--header-color: #83a2ce;
--terminal-green: #4ade80;
--terminal-yellow: #fbbf24;
--terminal-red: #ef4444;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
:root[data-theme="light"] {
--bg-color: #f3f4f6;
--text-color: #374151;
--accent-color: #3b82f6;
--border-color: #d1d5db;
--header-color: #1f2937;
--terminal-green: #059669;
--terminal-yellow: #d97706;
--terminal-red: #dc2626;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
font-family: var(--font-mono);
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
height: 100%;
width: 100%;
transition: background-color 0.3s ease, color 0.3s ease;
}
a {
color: var(--accent-color);
text-decoration: none;
transition: opacity 0.2s ease;
}
a:hover {
opacity: 0.8;
}
main {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
min-height: calc(100vh - 120px);
}
.container {
max-width: 900px;
margin: 0 auto;
}
.terminal-path {
color: var(--terminal-green);
padding: 1rem;
font-size: 1.2rem;
font-weight: 500;
border-radius: 4px;
margin-bottom: 1rem;
display: inline-block;
}
.content-box {
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 2rem;
margin-bottom: 2rem;
transition: border-color 0.3s ease;
}
.nav {
display: flex;
gap: 2rem;
margin: 1.5rem 0;
}
.nav a {
font-size: 1.1rem;
}
.cursor {
display: inline-block;
width: 0.6em;
height: 1em;
background-color: var(--text-color);
margin-left: 0.2em;
animation: blink 1s step-end infinite;
}
@keyframes blink {
from, to { opacity: 1; }
50% { opacity: 0; }
}
.footer {
text-align: center;
padding: 2rem 0;
font-size: 0.9rem;
color: var(--text-color);
opacity: 0.7;
}
/* Post styles */
.post-title {
color: var(--header-color);
margin-bottom: 1rem;
font-size: 1.5rem;
font-weight: normal;
}
.post-date {
color: var(--terminal-yellow);
font-size: 0.9rem;
margin-bottom: 1.5rem;
display: block;
}
.post-content {
margin-bottom: 2rem;
}
/* Terminal Commands */
.command {
color: var(--terminal-green);
margin-right: 0.5rem;
}
.command::before {
content: "$ ";
opacity: 0.7;
}
/* Theme Switcher */
.theme-switcher {
position: fixed;
top: 1rem;
right: 1rem;
background: var(--border-color);
border: none;
color: var(--text-color);
padding: 0.5rem 1rem;
border-radius: 4px;
font-family: var(--font-mono);
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.theme-switcher:hover {
background: var(--accent-color);
color: var(--bg-color);
}
/* Media Queries */
@media (max-width: 768px) {
.terminal-path {
font-size: 1rem;
}
.nav {
gap: 1rem;
}
.content-box {
padding: 1.5rem;
}
}
@media (max-width: 480px) {
.terminal-path {
font-size: 0.9rem;
}
.nav {
gap: 0.8rem;
}
.content-box {
padding: 1rem;
}
}