Added tag filtering and dropdown

This commit is contained in:
2025-11-22 18:13:21 +01:00
parent 314372c57f
commit cd43a09781
9 changed files with 331 additions and 62 deletions

View File

@@ -18,8 +18,8 @@ import nodeAdapter from '@astrojs/node'
export default defineConfig({ export default defineConfig({
adapter: nodeAdapter({ adapter: nodeAdapter({
mode: 'standalone', mode: 'standalone'
}),// Set adapter for deployment }), // Set adapter for deployment
site: themeConfig.site.website, site: themeConfig.site.website,
image: { image: {
service: { service: {

View File

@@ -4,13 +4,128 @@ import type { PostListProps } from '@/types'
import { themeConfig } from '@/config' import { themeConfig } from '@/config'
const { posts } = Astro.props as PostListProps const { posts } = Astro.props as PostListProps
// Read predefined tags and fallback from config
const predefinedTags: string[] = themeConfig.tags?.predefined ?? ['General']
const fallbackTag: string = themeConfig.tags?.default ?? themeConfig.tags?.fallback ?? 'General'
/**
* Normalize and pick a canonical tag for a post.
* - Supports `tags`, `tag`, `topic` frontmatter shapes.
* - Accepts arrays, CSV strings, and nested/plugin-provided locations.
* - Matches case-insensitively against predefined tags; falls back to `fallbackTag`.
*/
function canonicalTag(post: any): string {
const fm = post?.data ?? {}
const candidates: string[] = []
const addCandidate = (v: unknown) => {
if (v == null) return
if (Array.isArray(v)) {
for (const x of v) addCandidate(x)
return
}
if (typeof v === 'string') {
for (const part of v
.split(',')
.map((s) => s.trim())
.filter(Boolean)) {
candidates.push(part)
}
return
}
if (typeof v === 'object') {
try {
for (const val of Object.values(v as Record<string, unknown>)) addCandidate(val)
} catch {
// ignore
}
return
}
// fallback: stringify anything else
candidates.push(String(v))
}
// Common frontmatter keys
addCandidate(fm.tags)
addCandidate(fm.tag)
addCandidate(fm.topic)
// Some plugins may attach frontmatter into nested fields
addCandidate(fm.remarkPluginFrontmatter?.tags ?? fm.remarkPluginFrontmatter?.tag)
addCandidate(fm.frontmatter?.tags ?? fm.frontmatter?.tag)
// Also accept top-level collection shapes such as post.tags
addCandidate(post?.tags)
addCandidate(post?.tag)
addCandidate(post?.topic)
const normalized = candidates.map((s) => String(s).trim()).filter(Boolean)
if (normalized.length === 0) return fallbackTag
// Exact case-insensitive match
for (const c of normalized) {
const match = predefinedTags.find((p) => p.toLowerCase() === c.toLowerCase())
if (match) return match
}
// Loose substring match (helpful for hyphenated or compound tags)
for (const c of normalized) {
const match = predefinedTags.find(
(p) => c.toLowerCase().includes(p.toLowerCase()) || p.toLowerCase().includes(c.toLowerCase())
)
if (match) return match
}
// If nothing matches, return fallback
return fallbackTag
}
// Build posts with canonical tags and compute counts
const postsWithTag = posts.map((post) => ({ ...post, canonicalTag: canonicalTag(post) }))
const counts: Record<string, number> = {}
for (const t of predefinedTags) counts[t] = 0
// Ensure fallback counted even if not in predefined tags
if (!(fallbackTag in counts)) counts[fallbackTag] = 0
for (const p of postsWithTag) {
counts[p.canonicalTag] = (counts[p.canonicalTag] || 0) + 1
}
--- ---
<ul> <div class="post-list-widget">
<div
class="post-list-controls"
style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.5rem;"
>
<label for="post-tag-filter" class="sr-only">Filter posts by tag</label>
<select
id="post-tag-filter"
aria-label="Filter posts by tag"
class="tag-select"
data-predefined={JSON.stringify(predefinedTags)}
data-fallback={fallbackTag}
>
<option value="">All ({postsWithTag.length})</option>
{ {
posts.map((post) => ( // Render all predefined tags in order
<li> predefinedTags.map((t) => <option value={t}>{`${t} (${counts[t] ?? 0})`}</option>)
<a href={`/${post.id}/`}> }
{
// If fallback is not already in predefined and has posts, include it
!predefinedTags.includes(fallbackTag) && counts[fallbackTag] > 0 && (
<option value={fallbackTag}>{`${fallbackTag} (${counts[fallbackTag]})`}</option>
)
}
</select>
</div>
<ul class="post-list" data-post-list>
{
postsWithTag.map((post) => (
<li class="post-list-item" data-tag={post.canonicalTag}>
<a href={`/${post.id}/`} class="post-link">
<div class={`post-item ${!themeConfig.date.dateOnRight ? 'date-left' : ''}`}> <div class={`post-item ${!themeConfig.date.dateOnRight ? 'date-left' : ''}`}>
{!themeConfig.date.dateOnRight && ( {!themeConfig.date.dateOnRight && (
<p class="date font-features"> <p class="date font-features">
@@ -33,43 +148,92 @@ const { posts } = Astro.props as PostListProps
</li> </li>
)) ))
} }
</ul> </ul>
<div class="placeholder"></div> </div>
<style> <style>
ul { .tag-select {
padding: 0.35rem 0.75rem 0.35rem 0.5rem;
border-radius: 8px;
border: 1px solid var(--border);
/* Use theme card/background variables rather than forcing white */
background-color: var(--card-background, var(--background, #0b0b0b));
color: var(--text-primary);
font-size: 0.95rem;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding-right: 2rem; /* space for custom arrow */
/* Ensure text color applies on webkit mobile */
-webkit-text-fill-color: var(--text-primary);
/* keep a simple caret SVG; color fallback chosen but field background now uses theme */
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23BBBBBB' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 0.6rem center;
background-size: 0.9rem;
transition:
box-shadow 0.12s ease,
border-color 0.12s ease,
background-color 0.12s ease;
}
/* Try to style option backgrounds where the browser allows it */
.tag-select option {
background-color: var(--card-background, var(--background, #0b0b0b));
color: var(--text-primary);
}
/* Hide native arrow in IE/Edge */
.tag-select::-ms-expand {
display: none;
}
.tag-select:focus {
outline: 2px solid var(--focus-outline, rgba(50, 150, 250, 0.16));
outline-offset: 2px;
}
ul.post-list {
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style-type: none; list-style: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
} }
a { .post-list-item {
/* no default hiding; JS toggles inline style */
}
a.post-link {
color: var(--text-primary); color: var(--text-primary);
display: block; display: block;
text-decoration: none; text-decoration: none;
transition: opacity 0.15s ease-out; transition: opacity 0.15s ease-out;
} }
/* Restore hover interactions for pointer devices */
@media (hover: hover) and (pointer: fine) { @media (hover: hover) and (pointer: fine) {
ul:hover a { /* When the list is hovered, de-emphasize non-hovered links */
ul.post-list:hover a.post-link {
opacity: 0.4; opacity: 0.4;
} }
ul:hover a:hover { /* Fully reveal hovered item */
ul.post-list:hover a.post-link:hover {
opacity: 1; opacity: 1;
} }
ul:hover a:hover .divider { /* Improve divider and dotted-divider appearance on hovered item */
ul.post-list:hover a.post-link:hover .divider {
background-color: var(--text-tertiary); background-color: var(--text-tertiary);
opacity: 0.75; opacity: 0.85;
} }
ul:hover a:hover .dotted-divider { ul.post-list:hover a.post-link:hover .dotted-divider {
color: var(--text-secondary); color: var(--text-secondary);
opacity: 1;
} }
ul:hover a:hover .date {
/* Ensure date color increases contrast on hover */
ul.post-list:hover a.post-link:hover .date {
color: var(--text-secondary); color: var(--text-secondary);
opacity: 1; opacity: 1;
} }
@@ -87,15 +251,6 @@ const { posts } = Astro.props as PostListProps
justify-content: flex-start; justify-content: flex-start;
} }
.post-item.date-left .title {
flex: 1 1 auto;
min-width: 0;
}
.post-item.date-left .date {
margin-right: 0.75rem;
}
.title { .title {
margin: 0; margin: 0;
flex-shrink: 1; flex-shrink: 1;
@@ -134,12 +289,44 @@ const { posts } = Astro.props as PostListProps
opacity: 0.75; opacity: 0.75;
} }
.dotted-divider::after { .sr-only {
content: '·····························································································································································'; position: absolute !important;
pointer-events: none; height: 1px;
} width: 1px;
overflow: hidden;
.placeholder { clip: rect(1px, 1px, 1px, 1px);
height: 3rem; white-space: nowrap;
} }
</style> </style>
<script>
;(function () {
const selectEl = document.getElementById('post-tag-filter')
const list = document.querySelector('[data-post-list]')
if (!selectEl || !list) return
if (!(selectEl instanceof HTMLSelectElement)) return
const select = selectEl
/**
* Apply a tag filter to the list.
* @param {string} val - Selected tag value (empty string means show all)
*/
const applyFilter = (val) => {
const items = list.querySelectorAll('li[data-tag]')
items.forEach((li) => {
if (!(li instanceof HTMLElement)) return
const tag = li.getAttribute('data-tag') || ''
if (!val) li.style.display = ''
else li.style.display = tag === val ? '' : 'none'
})
}
// apply initial filter (no-op by default)
applyFilter(select.value || '')
select.addEventListener('change', function () {
const v = this instanceof HTMLSelectElement ? this.value : ''
applyFilter(v)
})
})()
</script>

View File

@@ -12,10 +12,10 @@ export const themeConfig: ThemeConfig = {
// GENERAL SETTINGS //////////////////////////////////////////////////////////////////////////////////// // GENERAL SETTINGS ////////////////////////////////////////////////////////////////////////////////////
general: { general: {
contentWidth: '40rem', // Content area width contentWidth: '42rem', // Content area width
centeredLayout: true, // Use centered layout (false for left-aligned) centeredLayout: true, // Use centered layout (false for left-aligned)
themeToggle: false, // Show theme toggle button (uses system theme by default) themeToggle: false, // Show theme toggle button (uses system theme by default)
postListDottedDivider: true, // Show dotted divider in post list postListDottedDivider: false, // Show dotted divider in post list
footer: true, // Show footer footer: true, // Show footer
fadeAnimation: true // Enable fade animations fadeAnimation: true // Enable fade animations
}, },
@@ -33,5 +33,15 @@ export const themeConfig: ThemeConfig = {
toc: true, // Show the table of contents (when there is enough page width) toc: true, // Show the table of contents (when there is enough page width)
imageViewer: true, // Enable image viewer imageViewer: true, // Enable image viewer
copyCode: true // Enable copy button in code blocks copyCode: true // Enable copy button in code blocks
},
// TAG SETTINGS //////////////////////////////////////////////////////////////////////////////////////
// Predefined canonical tags for the site. This list is used by components
// (e.g., PostList) to render a consistent dropdown and to compute counts.
// Add or remove tags here to control what appears in the UI.
tags: {
predefined: ['General', 'Game Dev', 'Reverse Engineering', 'Study'],
// Fallback/default tag to use when a post does not have a recognized tag
default: 'General'
} }
} }

View File

@@ -10,7 +10,11 @@ const posts = defineCollection({
title: z.string(), title: z.string(),
// Transform string to Date object // Transform string to Date object
pubDate: z.coerce.date(), pubDate: z.coerce.date(),
image: z.string().optional() image: z.string().optional(),
// Preserve tag fields so components can read them. Support string or array formats.
tags: z.union([z.string(), z.array(z.string())]).optional(),
tag: z.union([z.string(), z.array(z.string())]).optional(),
topic: z.string().optional()
}) })
}) })

View File

@@ -0,0 +1,58 @@
---
title: 'Relearning Programming - Variables and Initialization'
pubDate: '2025-11-22'
tags: 'Study'
---
![](./_assets/relearn-programming.png)
# Foreword
Hey,
I suck at coding - and that's why I decided to relearn programming. My language of choice is C++. I've been working with this language for a long time now, and it just grew on me like no other language did.
# What Led Me Here
I've been struggling with my problem solving + the core concepts of C++ itself. Even though I am aware of the fact that it's totally common to make mistakes, I've had the overwhelming feeling of frustration and "imposter syndrome". Every piece of information was
floating around in my head with no structure. Toss in ADHD and boom. That's how you replicate the feeling.
I tolerated it for too long.
# My Plan And Goals
My goals with relearning programming and C++ is to start with the bare basics and thoroughly study and apply my knowledge with a modern approach to C++. During this journey I will make sure to deep dive into every concept where my skills are lacking. I will make sure to
fully understand each assignment, each function call, each class and each line of code.
I will make sure to create plenty of small exercises & projects. The exercises give me the freedom to experiment with certain features and
to puzzle together solutions, which then can be finally implemented into the projects. These projects can be small games, utility tools or
simply just code snippets. Old C++ code repels me, so Im sticking to C++20 for now and moving to C++23 soon.
Nearly everything I learn and code will be talked about in this series of blog posts. This will make sure that I summarize and explain
the material in my own words. The length of the posts might vary. Sometimes I will talk about more than one concept in one post. Some
concepts are too short to write a 500 word post about.
# What This Series Will Look Like
- Notes from studying C++ / programming concepts in my own words
- Small exercises to experiment
- Projects to apply what I've learned (Games, utilities, code snippets)
- Mistakes I make along the way + solutions to them
- Occasional rants, schizophrenia and dog pictures
# Starting Point
Right now, I know a lot, but at the same time, I don't know anything. I struggle with the understanding of how everything is running
under the hood. This is a crucial point to be aware of, because each decision you take has influence on the logic and performance of
your program. Not to mention the cruciality of the architecture.
# Closing Thoughts
I feel motivated and nervous at the same time. This will be a long, but quick journey. Beside me mentioning that I suck at programming
and that "I don't know anything", I'm pretty sure that I'll profit of my past experience and knowledge.
I'm so hyped to show off my future projects and I'll try my best to entertain you via some text and images. It's quite hard to pull
off, if I am being honest.
To my future me ever reading this: Do not fucking skip anything.
---
*"Using no way as a way, having no limitation as limitation"</br>
~ Bruce Lee*

View File

@@ -1,7 +1,7 @@
--- ---
title: 'Relearning Programming - Introduction' title: 'Relearning Programming - Introduction'
pubDate: '2025-11-21' pubDate: '2025-11-21'
tag: 'study' tag: 'Study'
--- ---
![](./_assets/relearn-programming.png) ![](./_assets/relearn-programming.png)

View File

@@ -1,5 +1,5 @@
--- ---
title: 'IT WORKED' title: 'Welcome!!!'
pubDate: '2025-07-18' pubDate: '2025-07-18'
topic: 'general' topic: 'general'
--- ---

View File

@@ -74,7 +74,7 @@ html.light {
/* Dark Mode (Explicit) */ /* Dark Mode (Explicit) */
html.dark { html.dark {
--bg: #151414; --bg: #0a0a0a;
--text-primary: rgba(197, 201, 197, 1); --text-primary: rgba(197, 201, 197, 1);
--text-secondary: rgba(197, 201, 197, 0.4); --text-secondary: rgba(197, 201, 197, 0.4);
--text-tertiary: rgba(197, 201, 197, 0.24); --text-tertiary: rgba(197, 201, 197, 0.24);

View File

@@ -40,10 +40,20 @@ export interface PostSettings {
copyCode: boolean copyCode: boolean
} }
// Tag settings for predefined tags
export interface TagSettings {
predefined: string[]
// optional default/fallback keys allow flexibility in config naming
default?: string
fallback?: string
}
// Theme configuration type // Theme configuration type
export interface ThemeConfig { export interface ThemeConfig {
site: SiteInfo site: SiteInfo
general: GeneralSettings general: GeneralSettings
date: DateSettings date: DateSettings
post: PostSettings post: PostSettings
// optional tag settings
tags?: TagSettings
} }