feat: Rename image and data files
- Add PostCard component - Add WysiwygEditor component - Use 'front-matter' to parse front matter in markdown files for metadata instead of using redundant data objects
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 810 KiB After Width: | Height: | Size: 810 KiB |
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
80
src/components/Cards/PostCard.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
// PostCard.jsx
|
||||
import React from 'react';
|
||||
import { Button } from '@progress/kendo-react-buttons';
|
||||
import { Card, CardImage, CardBody } from '@progress/kendo-react-layout';
|
||||
|
||||
const PostCard = ({ post, onEdit }) => {
|
||||
const dataPath = post.header.image;
|
||||
const relativePath = `../../${dataPath}`;
|
||||
const imageUrl = new URL(relativePath, import.meta.url).href;
|
||||
|
||||
const formatDate = (utcString) => {
|
||||
if (!utcString) return '';
|
||||
|
||||
// 1. Create Date object. It parses the UTC string automatically.
|
||||
const date = new Date(utcString);
|
||||
|
||||
// 2. Define options for the desired output format (local time zone)
|
||||
const dateOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short', // e.g., 'Sep'
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true, // e.g., '9:51 PM'
|
||||
timeZoneName: undefined // Removes the timezone name from the output
|
||||
};
|
||||
|
||||
// 3. Convert to local, human-readable string and clean up formatting
|
||||
// The space between date and time may vary by locale, so we use string replacement for consistency.
|
||||
const formattedDate = date.toLocaleDateString(undefined, dateOptions);
|
||||
|
||||
// This attempts to convert the format '9/20/2025, 9:51 PM' to 'Sep 20, 2025, 9:51 PM'
|
||||
const parts = formattedDate.split(',');
|
||||
if (parts.length > 1) {
|
||||
const timePart = date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const datePart = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
return `${datePart}, ${timePart}`;
|
||||
}
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={post.slug} style={{ width: '250px', textAlign: 'center', display: 'flex' }}>
|
||||
|
||||
{/* 1. Thumbnail Image */}
|
||||
<CardImage
|
||||
src={imageUrl}
|
||||
alt={post.title}
|
||||
style={{ width: '100%', height: '100px', objectFit: 'cover' }}
|
||||
/>
|
||||
|
||||
{/* 2. Title, Date, and Button Container */}
|
||||
<CardBody style={{ flexGrow: 1, padding: '10px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
|
||||
{/* Title Link */}
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<a className="k-link" href={post.canonical_url} target="_blank" rel="noreferrer" style={{ fontWeight: 'bold' }}>
|
||||
{post.title}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Date and Edit Button */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.85em' }}>
|
||||
<span>{formatDate(post.date)}</span>
|
||||
<Button
|
||||
look="flat"
|
||||
themeColor="primary"
|
||||
onClick={() => onEdit(post.slug)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostCard;
|
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
RichTextEditorComponent,
|
||||
Inject,
|
||||
Toolbar,
|
||||
HtmlEditor,
|
||||
MarkdownEditor,
|
||||
SourceCode,
|
||||
Link, Image
|
||||
} from '@syncfusion/ej2-react-richtexteditor';
|
||||
import * as React from 'react';
|
||||
|
||||
function CampfireToggleEditor(props) {
|
||||
const toolbarOptions = {
|
||||
items: [
|
||||
'Bold', 'Italic', 'Underline', '|',
|
||||
'Formats', 'Alignments', 'OrderedList', 'UnorderedList', '|',
|
||||
'CreateLink', 'Image', '|',
|
||||
'SourceCode',
|
||||
'Undo', 'Redo'
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<RichTextEditorComponent
|
||||
value={props.value}
|
||||
change={handleEditorChange}
|
||||
editorMode={'Html'}
|
||||
toolbarSettings={toolbarOptions}
|
||||
>
|
||||
<Inject
|
||||
services={[
|
||||
Toolbar,
|
||||
Link,
|
||||
Image,
|
||||
HtmlEditor,
|
||||
MarkdownEditor,
|
||||
SourceCode
|
||||
]}
|
||||
/>
|
||||
</RichTextEditorComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default CampfireToggleEditor;
|
@@ -11,7 +11,7 @@ export default function Copyright() {
|
||||
>
|
||||
<p>
|
||||
© 2025{" | "}Derek L. Seitz{" | "}
|
||||
<a href="https://dlseitz.dev" target="_blank" rel="noopener noreferrer" class="k-link">
|
||||
<a href="https://dlseitz.dev" target="_blank" rel="noopener noreferrer" className="k-link">
|
||||
dlseitz.dev
|
||||
</a>
|
||||
</p>
|
||||
|
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: #0 - Setting Up Camp
|
||||
slug: 0-setting-up-camp
|
||||
title: "#0 - Setting Up Camp"
|
||||
slug: "0-setting-up-camp"
|
||||
fileName:
|
||||
published: true
|
||||
date: 2025-08-24 05:00:00 UTC
|
||||
tags: fullstack,developerjourney,BuildInPublic,introduction
|
||||
canonical_url: https://campfire.dlseitz.dev/0-setting-up-camp
|
||||
date: "2025-08-24 05:00:00 UTC"
|
||||
tags: ["fullstack", "developerjourney", "BuildInPublic", "introduction", "Campfire-Logs"]
|
||||
canonical_url: "https://campfire.dlseitz.dev/0-setting-up-camp"
|
||||
header:
|
||||
image: /assets/0-setting-up-camp.jpg
|
||||
image: "assets/header/0-setting-up-camp.jpg"
|
||||
attribution: 'Photo by <a href="https://unsplash.com/@kemaldgn?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Kemal Berkay Dogan</a> on <a href="https://unsplash.com/photos/a-campfire-with-a-cup-of-coffee-sitting-in-front-of-it-TcUN5sDZPZ8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>'
|
||||
---
|
||||
|
||||
|
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: #1 - The Great Gitea Migration
|
||||
title: "#1 - The Great Gitea Migration"
|
||||
slug: "1-the-great-gitea-migration"
|
||||
published: true
|
||||
date: 2025-08-27 20:19:51 UTC
|
||||
tags: CampfireLogs,SelfHosting,Devops,gitea
|
||||
canonical_url: https://campfire.dlseitz.dev/1-the-great-gitea-migration
|
||||
date: "2025-08-27 20:19:51 UTC"
|
||||
tags: ["CampfireLogs", "SelfHosting", "Devops", "gitea", "Campfire-Logs"]
|
||||
canonical_url: "https://campfire.dlseitz.dev/1-the-great-gitea-migration"
|
||||
header:
|
||||
image: /assets/header/#1-the-great-gitea-migration.jpg
|
||||
image: "assets/header/1-the-great-gitea-migration.jpg"
|
||||
attribution: 'Photo by <a href="https://unsplash.com/@lc_photography?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Leon Contreras</a> on <a href="https://unsplash.com/photos/selective-focus-photography-of-marshmallows-on-fire-pit-YndHL7gQIJE?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>'
|
||||
---
|
||||
|
||||
|
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: #2 - Retrofitting the Privacy Policy
|
||||
title: "#2 - Retrofitting the Privacy Policy"
|
||||
slug: "2-refactoring-the-privacy-policy"
|
||||
published: true
|
||||
date: 2025-09-02 20:41:21 UTC
|
||||
tags: WebDevelopment,FullStackDevelopment,Nodejs,DevLife
|
||||
canonical_url: https://campfire.dlseitz.dev/2-retrofitting-the-privacy-policy
|
||||
date: "2025-09-02 20:41:21 UTC"
|
||||
tags: ["WebDevelopment", "FullStackDevelopment", "Nodejs", "DevLife", "Campfire-Logs"]
|
||||
canonical_url: "https://campfire.dlseitz.dev/2-retrofitting-the-privacy-policy"
|
||||
header:
|
||||
image: /assets/header/#2-retrofitting-the-privacy-policy.png
|
||||
image: "assets/header/2-retrofitting-the-privacy-policy.png"
|
||||
attribution: 'Image generated with Sora. | © 2025 Derek L. Seitz'
|
||||
---
|
||||
|
||||
|
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: #3 - Data Privacy: Things to Consider
|
||||
title: "#3 - Data Privacy: Things to Consider"
|
||||
slug: "3-data-privacy-things-to-consider"
|
||||
published: true
|
||||
date: 2025-09-03 03:49:36 UTC
|
||||
tags: dataprivacy,WebDevelopment,Freelancing,PrivacyPolicy
|
||||
canonical_url: https://campfire.dlseitz.dev/3-data-privacy-things-to-consider
|
||||
date: "2025-09-03 03:49:36 UTC"
|
||||
tags: ["dataprivacy", "WebDevelopment", "Freelancing", "PrivacyPolicy", "Campfire-Logs"]
|
||||
canonical_url: "https://campfire.dlseitz.dev/3-data-privacy-things-to-consider"
|
||||
header:
|
||||
image: /assets/header/#3-data-privacy-things-to-consider.jpg
|
||||
image: "assets/header/3-data-privacy-things-to-consider.jpg"
|
||||
attribution: 'Photo by <a href="https://unsplash.com/@heftiba?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Toa Heftiba</a> on <a href="https://unsplash.com/photos/group-of-people-sitting-on-front-firepit-x9I-6yoXrXE?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>'
|
||||
|
||||
---
|
||||
|
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: #4 - Refactoring A False Sense of Simplicity
|
||||
title: "#4 - Refactoring A False Sense of Simplicity"
|
||||
slug: "4-refactoring-a-false-sense-of-simplicity"
|
||||
published: true
|
||||
date: 2025-09-12 01:08:54 UTC
|
||||
tags: refactoring,WebDevelopment,FrontendDevelopment,CSS
|
||||
canonical_url: https://campfire.dlseitz.dev/4-refactoring-a-false-sense-of-simplicity
|
||||
date: "2025-09-12 01:08:54 UTC"
|
||||
tags: ["refactoring", "WebDevelopment", "FrontendDevelopment", "CSS", "Campfire-Logs"]
|
||||
canonical_url: "https://campfire.dlseitz.dev/4-refactoring-a-false-sense-of-simplicity"
|
||||
header:
|
||||
image: '/assets/header/#4-refactoring-a-false-sense-of-simplicity.png'
|
||||
image: "assets/header/4-refactoring-a-false-sense-of-simplicity.png"
|
||||
attribution: 'Image generated with Sora. | © 2025 Derek L. Seitz'
|
||||
---
|
||||
|
||||
|
@@ -1,13 +1,14 @@
|
||||
---
|
||||
title: #5 - 'The Power of [Separation] Compels You!'
|
||||
title: "#5 - 'The Power of [Separation] Compels You!'"
|
||||
slug: "5-the-power-of-separation-compels-you"
|
||||
published: true
|
||||
date: 2025-09-20 21:51:29 UTC
|
||||
tags: WebDevelopment,refactoring,JavaScript,apiintegration
|
||||
canonical_url: https://campfire.dlseitz.dev/5-the-power-of-separation-compels-you
|
||||
date: "2025-09-20 21:51:29 UTC"
|
||||
tags: ["WebDevelopment", "refactoring", "JavaScript", "apiintegration", "Campfire-Logs"]
|
||||
canonical_url: "https://campfire.dlseitz.dev/5-the-power-of-separation-compels-you"
|
||||
header:
|
||||
image: /assets/header/#5-the-power-of-separation-compels-you.webp
|
||||
image: "assets/header/5-the-power-of-separation-compels-you.webp"
|
||||
attribution: 'Image generated with Sora. | © 2025 Derek L. Seitz'
|
||||
contentImagePath: /assets/#5/
|
||||
contentImagePath: "./assets/#5/"
|
||||
---
|
||||
|
||||
Hey there, and welcome back to **_Campfire Logs: The Art of Trial & Error_**. In my last log, [_#4 - Refactoring a False Sense of Simplicity_](https://hashnode.com/post/cmfg52ttw000002jufwdvdxkj), I introduced you to the [front end](https://www.computerscience.org/bootcamps/resources/frontend-vs-backend/#:~:text=Front%2Dend%20development%20focuses%20on%20the%20user%2Dfacing%20side%20of%20a%20website.%20Front%2Dend%20developers%20ensure%20that%20visitors%20can%20easily%20interact%20with%20and%20navigate%20sites%20by%20using%20programming%20languages%2C%20design%20skills%2C%20and%20other%20tools.%20They%20produce%20the%20drop%2Ddown%20menus%2C%20layouts%2C%20and%20designs%20for%20websites.) demo I recently [refactored](https://daedtech.com/refactoring-development-technique-not-project/#:~:text=Code%20refactoring%20is%20the%20process%20of%20restructuring%20existing%20computer%20code%20%E2%80%93%20changing%20the%20factoring%20%E2%80%93%20without%20changing%20its%20external%20behavior.) to be more modular and accessibility-friendly. Today, we are going to talk more about that same refactor, but we are going to focus on some of the enhancements I made to the existing features and the new features added for improved interactivity. We are also going to discuss how separating the data, presentation, and logic layers to the demo improved maintainability of the codebase by [decoupling](https://blog.covibe.us/the-pitfalls-of-excessive-decoupling-in-software-development-striking-the-right-balance/#:~:text=Decoupling%2C%20in%20software%20development%2C%20refers%20to%20the%20practice%20of%20breaking%20down%20a%20software%20system%20into%20smaller%2C%20independent%20components%20or%20modules.) its components.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import matter from 'gray-matter';
|
||||
import fm from 'front-matter';
|
||||
|
||||
const modules = import.meta.glob('./BlogPosts/*.md', { query: '?raw', import: 'default' });
|
||||
|
||||
@@ -7,10 +7,10 @@ export async function loadPosts() {
|
||||
|
||||
for (const path in modules) {
|
||||
const fileContent = await modules[path]();
|
||||
const { data } = matter(fileContent);
|
||||
const { attributes } = fm(fileContent);
|
||||
|
||||
posts.push({
|
||||
...data,
|
||||
...attributes,
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
// DashboardPage.jsx
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { loadPosts } from '../data/blog-post-data';
|
||||
import { Button } from '@progress/kendo-react-buttons';
|
||||
import { Card, CardImage, CardBody, GridLayout } from '@progress/kendo-react-layout';
|
||||
import PostCard from '../components/Cards/PostCard';
|
||||
|
||||
const Dashboard = React.forwardRef((props, ref) => {
|
||||
// State to hold posts loaded asynchronously
|
||||
const [blogPosts, setBlogPosts] = useState([]);
|
||||
|
||||
// Load posts on component mount
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
const postsData = await loadPosts();
|
||||
@@ -16,7 +16,6 @@ const Dashboard = React.forwardRef((props, ref) => {
|
||||
fetchPosts();
|
||||
}, []);
|
||||
|
||||
// Separate published and draft posts (no changes here)
|
||||
const publishedPosts = blogPosts.filter(post => post.published);
|
||||
const draftPosts = blogPosts.filter(post => !post.published);
|
||||
|
||||
@@ -28,20 +27,23 @@ const Dashboard = React.forwardRef((props, ref) => {
|
||||
<div style={{ textAlign: 'center' }} ref={ref}>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<section style={{ textAlign: 'center' }}>
|
||||
<section style={{ textAlign: 'center', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h2>Published Posts</h2>
|
||||
{publishedPosts.length ? (
|
||||
<ul>
|
||||
<GridLayout
|
||||
cols={[{ width: "1fr" }, { width: "1fr" }, { width: "1fr" }]}
|
||||
gap={{ rows: 30, cols: 10 }}
|
||||
style={{ width: '900px' }}
|
||||
>
|
||||
{publishedPosts.map(post => (
|
||||
<li key={post.slug}>
|
||||
<a className="k-link" href={post.canonical_url} target="_blank" rel="noreferrer">
|
||||
{post.title}
|
||||
</a>{' '}
|
||||
<span style={{ marginLeft: '30px', padding: '0 20px' }}>{post.date}</span>{' '}
|
||||
<Button style={{ marginLeft: '30px', padding: '0 20px' }} onClick={() => handleEdit(post.slug)}>Edit</Button>
|
||||
</li>
|
||||
// The PostCard automatically becomes a grid item (cell)
|
||||
<PostCard
|
||||
key={post.slug}
|
||||
post={post}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</GridLayout>
|
||||
) : (
|
||||
<p>No published posts.</p>
|
||||
)}
|
||||
|