Adding blog post capability
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
apatil 2025-05-05 22:04:26 +01:00
parent 6a5438ed00
commit 0a78ae9deb
14 changed files with 1426 additions and 71 deletions

1120
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,12 @@
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/material-nextjs": "^7.0.2",
"gray-matter": "^4.0.3",
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

View File

@ -0,0 +1,18 @@
import { getPostBySlug, getAllSlugs } from '../../../lib/posts';
import { notFound } from 'next/navigation';
type Params = { slug: string };
export async function generateStaticParams() {
const slugs = getAllSlugs();
return slugs.map(slug => ({ slug }));
}
export default async function BlogPost({ params }: { params: Params }) {
const post = await getPostBySlug(params.slug);
if (!post) return notFound();
return (
<article>
<h1>{post.title}</h1>
<p >{post.date}</p>
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
</article>
);
}

26
src/app/blog/page.tsx Normal file
View File

@ -0,0 +1,26 @@
import { getAllPosts } from '../../lib/posts';
import ArticleIcon from '@mui/icons-material/Article';
import { Avatar, Box, Link, List, ListItem, ListItemAvatar, ListItemButton, ListItemText, Typography } from '@mui/material';
export default function Home() {
const posts = getAllPosts();
return (
<Box>
<Typography variant='h1' >My Blog</Typography>
<List>
{posts.map(post => (
<ListItem key={post.slug}>
<ListItemAvatar>
<Avatar>
<ArticleIcon />
</Avatar>
</ListItemAvatar>
<ListItemButton component={Link} href={`/blog/${post.slug}`}>
<ListItemText primary={post.title} secondary={<><span>{post.date}</span><br /><span>{post.excerpt}</span></>} />
</ListItemButton>
</ListItem>
))}
</List>
</Box >
);
}

View File

@ -1,12 +1,8 @@
import type { Metadata } from "next";
import "./globals.css";
import theme from "@/theme/theme";
import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline } from "@mui/material";
import InitColorSchemeScript from "@mui/material/InitColorSchemeScript";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v13-appRouter";
import ModeSwitch from "@/components/ModeSwitch/ModeSwitch";
import Header from "@/components/Header/Header";
import { ThemeContext } from "@/theme/ThemeContext";
import { Box } from "@mui/material";
export const metadata: Metadata = {
@ -24,15 +20,13 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body>
<InitColorSchemeScript attribute="class" />
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider theme={theme}>
<CssBaseline />
<ModeSwitch />
<Header />
<ThemeContext>
<Header />
<Box sx={{ mt: '6rem' }}>
{children}
</ThemeProvider>
</AppRouterCacheProvider>
</Box>
</ThemeContext>
</body>
</html >
);

View File

@ -1,8 +1,11 @@
import { Box, Typography } from "@mui/material";
export default function Home() {
return (
<div>
<h1>Welcome to the den</h1>
<p>Here lives the geek bear who is hungry for bits and bytes. Ravaging through the internet dumpster finding the finest 0s and 1s. He looks for speed and performance</p>
</div>
<Box>
<Typography variant={'h1'}>Welcome to the den</Typography>
<Typography variant="body1">Here lives the geek bear who is hungry for bits and bytes. Ravaging through the internet dumpster finding the finest 0s and 1s. He looks for speed and performance</Typography>
</Box>
);
}

View File

@ -15,6 +15,7 @@ import {
Toolbar,
Typography
} from '@mui/material'
import ModeSwitch from '../ModeSwitch/ModeSwitch'
const navItems = [
{ label: 'Home', href: '/' },
{ label: 'Blog', href: '/blog' },
@ -28,37 +29,35 @@ export default function Header() {
}
const drawer = (
<Box onClick={handleDrawerToggle}>
<Typography variant="h6" >Menu</Typography>
<List>
{navItems.map((item) => (
<ListItem key={item.label} disablePadding>
<ListItemButton sx={{ textAlign: 'center' }} component={Link} href={item.href}>
<ListItem key={item.label}>
<ListItemButton sx={{ textAlign: 'center', px: '5rem' }} component={Link} href={item.href}>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
)
return (
<>
<AppBar position="static" color='transparent' >
<Box sx={{ flexGrow: 1 }}>
<AppBar position="fixed" sx={{ backgroundColor: 'primary.main', opacity: '80%' }}>
<Toolbar >
<IconButton
color="inherit"
size='large'
sx={{ color: 'background.paper' }}
aria-label="open drawer"
edge="end"
edge="start"
onClick={handleDrawerToggle}
>
<MenuIcon />
</IconButton>
<Box >
{navItems.map((item) => (
<Link key={item.label} href={item.href} >
{item.label}
</Link>
))}
</Box>
<Typography variant="h6" padding={2} color='background.paper'>
TCG
</Typography>
<ModeSwitch />
</Toolbar>
</AppBar>
<Drawer
@ -69,6 +68,6 @@ export default function Header() {
>
{drawer}
</Drawer>
</>
</Box>
)
}

View File

@ -1,40 +1,71 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import { useColorScheme } from '@mui/material/styles';
import { useColorMode } from '@/theme/ThemeContext';
import { styled, Switch } from '@mui/material';
const MaterialUISwitch = styled(Switch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
`${theme.palette.background.paper}`,
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: '#aab4be',
...theme.applyStyles('dark', {
backgroundColor: '#8796A5',
}),
},
},
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.secondary.main,
width: 32,
height: 32,
'&::before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
`${theme.palette.background.paper}`,
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
},
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: '#aab4be',
borderRadius: 20 / 2,
...theme.applyStyles('dark', {
backgroundColor: '#8796A5',
}),
},
}));
export default function ModeSwitch() {
const { mode, setMode } = useColorScheme();
const { toggleColorMode, mode } = useColorMode()
if (!mode) {
return null;
}
return (
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
mt: 1,
p: 1,
}}
>
<FormControl>
<InputLabel id="mode-select-label">Theme</InputLabel>
<Select
labelId="mode-select-label"
id="mode-select"
value={mode}
onChange={(event) => setMode(event.target.value as typeof mode)}
label="Theme"
>
<MenuItem value="system">System</MenuItem>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
</Select>
</FormControl>
</Box>
<FormControl fullWidth>
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
<MaterialUISwitch aria-labelledby='mode-select-label' value={mode} onChange={(_e) => toggleColorMode()} />
</FormControl>
);
}

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './posts';

52
src/lib/posts.ts Normal file
View File

@ -0,0 +1,52 @@
// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'src/posts');
export function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames
.map((fileName) => {
const slug = fileName.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
return {
slug,
title: data.title,
date: data.date,
excerpt: data.excerpt,
};
})
.sort((a, b) => (a.date < b.date ? 1 : -1));
}
export async function getPostBySlug(slug: string) {
const fullPath = path.join(postsDirectory, `${slug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const processedContent = await remark().use(html).process(content);
const contentHtml = processedContent.toString();
return {
slug,
contentHtml,
title: data.title,
date: data.date,
};
}
export const getAllSlugs = (): Array<{ params: { slug: string } }> => {
return fs.readdirSync(postsDirectory).map((fileName) => ({
params: { slug: fileName.replace(/\.md$/, '') },
}));
};

10
src/posts/first-post.md Normal file
View File

@ -0,0 +1,10 @@
---
title: 'My First Post'
date: '2025-05-01'
slug: 'first-post'
excerpt: 'This is an excerpt from the first post.'
---
# My First Post
Welcome to my blog post written in **Markdown**.

View File

@ -0,0 +1,43 @@
'use client'
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { getTheme } from "./theme";
import { CssBaseline, ThemeProvider } from "@mui/material";
type Mode = 'light' | 'dark';
const ColorModeContext = createContext({
toggleColorMode: () => { },
mode: 'light' as Mode
})
export const useColorMode = () => useContext(ColorModeContext);
export const ThemeContext = ({ children }: { children: React.ReactNode }) => {
const [mode, setMode] = useState<Mode>('light');
useEffect(() => {
const stored = localStorage.getItem('mui-theme');
if (stored === 'dark' || stored === 'light')
setMode(stored)
}, [])
const toggleColorMode = () => {
setMode((prev) => {
const next = prev === 'light' ? 'dark' : 'light';
localStorage.setItem('mui-theme', next);
return next;
})
}
const theme = useMemo(() => getTheme(mode), [mode])
return (
<ColorModeContext.Provider value={{ toggleColorMode, mode }}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</ColorModeContext.Provider>
)
}

View File

@ -1,7 +1,47 @@
'use client';
import { createTheme } from '@mui/material/styles';
import { ColorSystemOptions, createTheme } from '@mui/material/styles';
import { Roboto } from 'next/font/google';
const lightColorScheme: ColorSystemOptions = {
palette: {
mode: 'light',
primary: {
main: '#A3C4F3',
},
secondary: {
main: '#F6C6EA',
},
background: {
default: '#FFF9F0',
paper: '#FFFFFF',
},
text: {
primary: '#333333',
secondary: '#5A5A5A',
},
},
};
const darkColorScheme: ColorSystemOptions = {
palette: {
mode: 'dark',
primary: {
main: '#B8E0D2',
},
secondary: {
main: '#EFBBCF',
},
background: {
default: '#1E1E2F',
paper: '#2A2A3B',
},
text: {
primary: '#F0F0F0',
secondary: '#B0B0B0',
},
},
};
const roboto = Roboto({
weight: ['300', '400', '500', '700'],
subsets: ['latin'],
@ -9,7 +49,7 @@ const roboto = Roboto({
});
const theme = createTheme({
colorSchemes: { light: true, dark: true },
colorSchemes: { light: lightColorScheme, dark: darkColorScheme },
cssVariables: {
colorSchemeSelector: 'class',
},
@ -35,3 +75,6 @@ const theme = createTheme({
});
export default theme;
export const getTheme = (mode: 'light' | 'dark') =>
createTheme(mode === 'dark' ? darkColorScheme : lightColorScheme);

View File

@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/app/blog/[slug]"
],
"exclude": [
"node_modules"
]
}