Added tag filtering and dropdown
This commit is contained in:
@@ -18,7 +18,7 @@ import nodeAdapter from '@astrojs/node'
|
||||
|
||||
export default defineConfig({
|
||||
adapter: nodeAdapter({
|
||||
mode: 'standalone',
|
||||
mode: 'standalone'
|
||||
}), // Set adapter for deployment
|
||||
site: themeConfig.site.website,
|
||||
image: {
|
||||
|
||||
@@ -4,13 +4,128 @@ import type { PostListProps } from '@/types'
|
||||
import { themeConfig } from '@/config'
|
||||
|
||||
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) => (
|
||||
<li>
|
||||
<a href={`/${post.id}/`}>
|
||||
// Render all predefined tags in order
|
||||
predefinedTags.map((t) => <option value={t}>{`${t} (${counts[t] ?? 0})`}</option>)
|
||||
}
|
||||
{
|
||||
// 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' : ''}`}>
|
||||
{!themeConfig.date.dateOnRight && (
|
||||
<p class="date font-features">
|
||||
@@ -34,42 +149,91 @@ const { posts } = Astro.props as PostListProps
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div class="placeholder"></div>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
.post-list-item {
|
||||
/* no default hiding; JS toggles inline style */
|
||||
}
|
||||
|
||||
a.post-link {
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* Restore hover interactions for pointer devices */
|
||||
@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;
|
||||
}
|
||||
|
||||
ul:hover a:hover {
|
||||
/* Fully reveal hovered item */
|
||||
ul.post-list:hover a.post-link:hover {
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -87,15 +251,6 @@ const { posts } = Astro.props as PostListProps
|
||||
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 {
|
||||
margin: 0;
|
||||
flex-shrink: 1;
|
||||
@@ -134,12 +289,44 @@ const { posts } = Astro.props as PostListProps
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.dotted-divider::after {
|
||||
content: '·····························································································································································';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
height: 3rem;
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -12,10 +12,10 @@ export const themeConfig: ThemeConfig = {
|
||||
|
||||
// GENERAL SETTINGS ////////////////////////////////////////////////////////////////////////////////////
|
||||
general: {
|
||||
contentWidth: '40rem', // Content area width
|
||||
contentWidth: '42rem', // Content area width
|
||||
centeredLayout: true, // Use centered layout (false for left-aligned)
|
||||
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
|
||||
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)
|
||||
imageViewer: true, // Enable image viewer
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ const posts = defineCollection({
|
||||
title: z.string(),
|
||||
// Transform string to Date object
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
58
src/content/posts/_relearning-programming-part1.md
Normal file
58
src/content/posts/_relearning-programming-part1.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: 'Relearning Programming - Variables and Initialization'
|
||||
pubDate: '2025-11-22'
|
||||
tags: 'Study'
|
||||
---
|
||||
|
||||

|
||||
|
||||
# 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 I’m 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*
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: 'Relearning Programming - Introduction'
|
||||
pubDate: '2025-11-21'
|
||||
tag: 'study'
|
||||
tag: 'Study'
|
||||
---
|
||||
|
||||

|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'IT WORKED'
|
||||
title: 'Welcome!!!'
|
||||
pubDate: '2025-07-18'
|
||||
topic: 'general'
|
||||
---
|
||||
@@ -74,7 +74,7 @@ html.light {
|
||||
|
||||
/* Dark Mode (Explicit) */
|
||||
html.dark {
|
||||
--bg: #151414;
|
||||
--bg: #0a0a0a;
|
||||
--text-primary: rgba(197, 201, 197, 1);
|
||||
--text-secondary: rgba(197, 201, 197, 0.4);
|
||||
--text-tertiary: rgba(197, 201, 197, 0.24);
|
||||
|
||||
@@ -40,10 +40,20 @@ export interface PostSettings {
|
||||
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
|
||||
export interface ThemeConfig {
|
||||
site: SiteInfo
|
||||
general: GeneralSettings
|
||||
date: DateSettings
|
||||
post: PostSettings
|
||||
// optional tag settings
|
||||
tags?: TagSettings
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user