feat(layout): Add Header and Footer to post pages for consistency
This commit is contained in:
parent
2792e86546
commit
f1f5ac4b33
|
@ -0,0 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Case Study: SharePoint & M365 Migrations | Daniel LaForce</title>
|
||||
<meta name="description" content="Enterprise migration case study: Seamless transition from legacy systems to Microsoft 365 with automation, SharePoint, OneDrive, and Teams integration.">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
.portfolio-header {
|
||||
padding: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
.portfolio-section {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
margin-top: 0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<!-- Header/Nav -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<a href="index.html">
|
||||
<span class="logo-text-glow">Daniel LaForce</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-menu">
|
||||
<a href="index.html#home" class="nav-link">Home</a>
|
||||
<a href="index.html#portfolio" class="nav-link active">Portfolio</a>
|
||||
<a href="index.html#services" class="nav-link">Services</a>
|
||||
<a href="index.html#lab" class="nav-link">Live Lab</a>
|
||||
<a href="resume.html" class="nav-link">Resume</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Case Study Content -->
|
||||
<section class="portfolio-header">
|
||||
<div class="container">
|
||||
<h1 class="portfolio-title">Case Study: SharePoint & M365 Migrations</h1>
|
||||
<p class="portfolio-subtitle">Modernizing enterprise collaboration and storage through Microsoft 365</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="portfolio-section">
|
||||
<div class="container">
|
||||
<div class="project-card">
|
||||
<div class="project-content">
|
||||
<h3 class="project-title">Client Overview</h3>
|
||||
<p class="project-description">
|
||||
A mid-sized professional services firm relied on Dropbox, Egnyte, Google Workspace, and on-prem file servers to manage daily operations. The IT environment was fragmented, increasing operational complexity and impeding secure collaboration. They needed a scalable solution to unify infrastructure, reduce support load, and enhance productivity.
|
||||
</p>
|
||||
|
||||
<h3 class="project-title">Objectives</h3>
|
||||
<ul>
|
||||
<li>Consolidate file and email services under Microsoft 365</li>
|
||||
<li>Minimize business disruption during migration</li>
|
||||
<li>Strengthen compliance, backup, and access control</li>
|
||||
<li>Enable centralized collaboration with Microsoft Teams</li>
|
||||
</ul>
|
||||
<br>
|
||||
<h3 class="project-title">Solution Strategy</h3>
|
||||
<ul>
|
||||
<li>Provisioned a temporary VM to act as a synchronized landing zone for Egnyte and local file server data</li>
|
||||
<li>Used Egnyte’s API and command-line tools to batch-export permissions and files to the sync VM</li>
|
||||
<li>Ran Microsoft’s SharePoint Migration Tool (SPMT) with JSON-based mapping to import content into SharePoint document libraries</li>
|
||||
<li>Automated pre/post migration checks and permissions auditing via PowerShell and Graph API</li>
|
||||
<li>Staged migration of mail and calendar data using MigrationWiz with coexistence enabled between Google Workspace and Exchange Online</li>
|
||||
<li>Provided Teams onboarding with channel templates and cross-platform training</li>
|
||||
</ul>
|
||||
<br>
|
||||
<h3 class="project-title">Results</h3>
|
||||
<ul>
|
||||
<li>12.4 TB of content migrated across four platforms in under 90 days</li>
|
||||
<li>Zero data loss or permission mismatches confirmed via script-based audits</li>
|
||||
<li>Helpdesk load cut by 40% within the first month post-migration</li>
|
||||
<li>Fully adopted Microsoft Teams structure with defined department-based channels and integrated file repositories</li>
|
||||
<li>End-user training boosted post-migration satisfaction to 97%</li>
|
||||
</ul>
|
||||
|
||||
<div class="project-skills">
|
||||
<span class="skill-tag">SharePoint Online</span>
|
||||
<span class="skill-tag">OneDrive</span>
|
||||
<span class="skill-tag">Teams</span>
|
||||
<span class="skill-tag">Exchange Online</span>
|
||||
<span class="skill-tag">PowerShell</span>
|
||||
<span class="skill-tag">Migration Tools</span>
|
||||
<span class="skill-tag">Egnyte API</span>
|
||||
<span class="skill-tag">SPMT</span>
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<a href="/index.html#portfolio" class="project-link"><i class="fas fa-arrow-left"></i> Back to Portfolio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-logo">
|
||||
<span class="logo-text-glow">Daniel LaForce</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/index.html#home">Home</a>
|
||||
<a href="/index.html#portfolio">Portfolio</a>
|
||||
<a href="/index.html#services">Services</a>
|
||||
<a href="/index.html#lab">Live Lab</a>
|
||||
<a href="/resume.html">Resume</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© All rights reserved. LaForceIT.com</p>
|
||||
<p class="disclaimer">Custom-built with HTML, CSS, and JavaScript.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
// MiniGraph.astro - A standalone mini knowledge graph component with fullscreen capability
|
||||
// This component is designed to work independently from the blog structure
|
||||
// This component is designed to work independently from the blog structure and now includes content previews
|
||||
|
||||
// Define props interface
|
||||
interface Props {
|
||||
|
@ -8,8 +8,10 @@ interface Props {
|
|||
title: string; // Current post title
|
||||
tags?: string[]; // Current post tags
|
||||
category?: string; // Current post category
|
||||
relatedPosts?: any[]; // Related posts data
|
||||
content?: string; // Current post content HTML
|
||||
relatedPosts?: any[]; // Related posts data with their content
|
||||
allPosts?: any[]; // All posts for second level relationships
|
||||
width?: string; // Optional width parameter
|
||||
}
|
||||
|
||||
// Extract props with defaults
|
||||
|
@ -18,8 +20,10 @@ const {
|
|||
title,
|
||||
tags = [],
|
||||
category = "Uncategorized",
|
||||
content = "", // Add content property for current post
|
||||
relatedPosts = [],
|
||||
allPosts = []
|
||||
allPosts = [],
|
||||
width = "100%" // Default width of the component
|
||||
} = Astro.props;
|
||||
|
||||
// Generate unique ID for the graph container
|
||||
|
@ -75,6 +79,7 @@ const nodes = [
|
|||
level: 0,
|
||||
category: category,
|
||||
tags: tags,
|
||||
content: content, // Add content for the current post (as HTML string)
|
||||
url: `/posts/${slug}/`
|
||||
},
|
||||
// Level 1: Tag nodes
|
||||
|
@ -86,15 +91,37 @@ const nodes = [
|
|||
url: `/tag/${tag}/`
|
||||
})),
|
||||
// Level 1: Related post nodes
|
||||
...relatedPosts.map(post => ({
|
||||
id: post.slug,
|
||||
label: post.data.title,
|
||||
type: "post",
|
||||
level: 1,
|
||||
category: post.data.category || "Uncategorized",
|
||||
tags: post.data.tags || [],
|
||||
url: `/posts/${post.slug}/`
|
||||
})),
|
||||
...relatedPosts.map(post => {
|
||||
// Extract content from post object - this will vary based on your data structure
|
||||
// Try multiple properties where content might be stored
|
||||
let postContent = '';
|
||||
if (post.data.content) {
|
||||
postContent = post.data.content;
|
||||
} else if (post.data.body) {
|
||||
postContent = post.data.body;
|
||||
} else if (post.data.html) {
|
||||
postContent = post.data.html;
|
||||
} else if (post.content) {
|
||||
postContent = post.content;
|
||||
} else if (post.body) {
|
||||
postContent = post.body;
|
||||
} else if (post.html) {
|
||||
postContent = post.html;
|
||||
} else if (post.data.excerpt) {
|
||||
postContent = post.data.excerpt;
|
||||
}
|
||||
|
||||
return {
|
||||
id: post.slug,
|
||||
label: post.data.title,
|
||||
type: "post",
|
||||
level: 1,
|
||||
category: post.data.category || "Uncategorized",
|
||||
tags: post.data.tags || [],
|
||||
content: postContent, // Add content as HTML string
|
||||
url: `/posts/${post.slug}/`
|
||||
};
|
||||
}),
|
||||
// Level 2: Related tags nodes (Tags from Level 1 posts)
|
||||
...relatedPostsTags.map(tag => ({
|
||||
id: `tag-${tag}`,
|
||||
|
@ -104,15 +131,36 @@ const nodes = [
|
|||
url: `/tag/${tag}/`
|
||||
})),
|
||||
// Level 2: Posts related to tags (Posts connected to Level 1 tags)
|
||||
...level2Posts.map(post => ({
|
||||
id: post.slug,
|
||||
label: post.data.title,
|
||||
type: "post",
|
||||
level: 2,
|
||||
category: post.data.category || "Uncategorized",
|
||||
tags: post.data.tags || [],
|
||||
url: `/posts/${post.slug}/`
|
||||
})),
|
||||
...level2Posts.map(post => {
|
||||
// Extract content from level 2 posts
|
||||
let postContent = '';
|
||||
if (post.data.content) {
|
||||
postContent = post.data.content;
|
||||
} else if (post.data.body) {
|
||||
postContent = post.data.body;
|
||||
} else if (post.data.html) {
|
||||
postContent = post.data.html;
|
||||
} else if (post.content) {
|
||||
postContent = post.content;
|
||||
} else if (post.body) {
|
||||
postContent = post.body;
|
||||
} else if (post.html) {
|
||||
postContent = post.html;
|
||||
} else if (post.data.excerpt) {
|
||||
postContent = post.data.excerpt;
|
||||
}
|
||||
|
||||
return {
|
||||
id: post.slug,
|
||||
label: post.data.title,
|
||||
type: "post",
|
||||
level: 2,
|
||||
category: post.data.category || "Uncategorized",
|
||||
tags: post.data.tags || [],
|
||||
content: postContent, // Add content as HTML string
|
||||
url: `/posts/${post.slug}/`
|
||||
};
|
||||
}),
|
||||
// Level 2: Tags from Level 1 posts (only tags directly connected to Level 1 posts)
|
||||
// This was the corrected logic for level2Tags Set
|
||||
...[...level2Tags].map(tag => ({
|
||||
|
@ -170,8 +218,8 @@ const predefinedColors = {
|
|||
};
|
||||
---
|
||||
|
||||
<!-- Super simple HTML structure -->
|
||||
<div class="knowledge-graph-wrapper">
|
||||
<!-- Enhanced HTML structure with specified width -->
|
||||
<div class="knowledge-graph-wrapper" style={`width: ${width};`}>
|
||||
<h4 class="graph-title">Post Connections</h4>
|
||||
<div id={graphId} class="mini-graph-container"></div>
|
||||
|
||||
|
@ -192,7 +240,7 @@ const predefinedColors = {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen container (initially hidden) -->
|
||||
<!-- Fullscreen container (initially hidden) with enhanced info panel -->
|
||||
<div id={`${graphId}-fullscreen`} class="fullscreen-container">
|
||||
<div class="fullscreen-header">
|
||||
<div class="fullscreen-title">Knowledge Graph</div>
|
||||
|
@ -208,7 +256,7 @@ const predefinedColors = {
|
|||
<!-- Fullscreen graph container -->
|
||||
<div id={`${graphId}-fullscreen-graph`} class="fullscreen-graph"></div>
|
||||
|
||||
<!-- Info panel -->
|
||||
<!-- Enhanced Info panel with content preview capability -->
|
||||
<div id={`${graphId}-info-panel`} class="info-panel">
|
||||
<div class="info-panel-header">
|
||||
<h3 id={`${graphId}-info-title`} class="info-title">Node Info</h3>
|
||||
|
@ -236,6 +284,23 @@ const predefinedColors = {
|
|||
<div id={`${graphId}-info-tags`} class="tags-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced content preview section -->
|
||||
<div id={`${graphId}-content-preview-container`} class="content-preview-container">
|
||||
<span class="info-label">Content Preview:</span>
|
||||
<div id={`${graphId}-content-preview`} class="content-preview">
|
||||
<div class="content-placeholder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
<p>Select a post to view content preview</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-connections">
|
||||
<span class="info-label">Connections:</span>
|
||||
<ul id={`${graphId}-info-connections`} class="connections-list"></ul>
|
||||
|
@ -249,17 +314,18 @@ const predefinedColors = {
|
|||
|
||||
<!-- Minimal CSS -->
|
||||
<style>
|
||||
/* Base styles for mini graph */
|
||||
/* Base styles for mini graph - 30% larger */
|
||||
.knowledge-graph-wrapper {
|
||||
width: 100%;
|
||||
width: 100%; /* Width will be set by style attribute */
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.graph-title {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-primary, #e2e8f0);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-graph-container {
|
||||
|
@ -269,6 +335,7 @@ const predefinedColors = {
|
|||
overflow: hidden;
|
||||
border: 1px solid var(--card-border, rgba(56, 189, 248, 0.2));
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
min-height: 250px; /* Ensure minimum height */
|
||||
}
|
||||
|
||||
/* Fullscreen toggle button */
|
||||
|
@ -387,7 +454,9 @@ const predefinedColors = {
|
|||
}
|
||||
|
||||
.info-panel.active {
|
||||
width: 350px; /* Show panel when active */
|
||||
width: 45%; /* Percentage-based width for better responsiveness */
|
||||
min-width: 400px; /* Minimum width to ensure content is readable */
|
||||
max-width: 500px; /* Maximum width to prevent overflow on large screens */
|
||||
}
|
||||
|
||||
.info-panel-header {
|
||||
|
@ -429,7 +498,7 @@ const predefinedColors = {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.info-type, .info-category, .info-tags, .info-connections {
|
||||
.info-type, .info-category, .info-tags, .info-connections, .content-preview-container {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
|
@ -512,6 +581,205 @@ const predefinedColors = {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Content Preview Styles - Enhanced */
|
||||
.content-preview-container {
|
||||
border-top: 1px solid var(--border-primary, rgba(56, 189, 248, 0.1));
|
||||
padding-top: 1.25rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
max-height: 350px; /* Taller content area */
|
||||
overflow-y: auto;
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-primary, rgba(56, 189, 248, 0.1));
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary, #e2e8f0);
|
||||
}
|
||||
|
||||
.content-preview::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.content-preview::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-preview::-webkit-scrollbar-thumb {
|
||||
background: rgba(56, 189, 248, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-preview::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
.content-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--text-tertiary, #64748b);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content-placeholder svg {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content-placeholder p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.placeholder-note {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Styling for post content preview */
|
||||
.post-content-preview {
|
||||
font-family: var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif);
|
||||
}
|
||||
|
||||
.post-content-preview h1,
|
||||
.post-content-preview h2,
|
||||
.post-content-preview h3,
|
||||
.post-content-preview h4,
|
||||
.post-content-preview h5,
|
||||
.post-content-preview h6 {
|
||||
color: var(--text-primary, #e2e8f0);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post-content-preview h1 { font-size: 1.4rem; }
|
||||
.post-content-preview h2 { font-size: 1.25rem; }
|
||||
.post-content-preview h3 { font-size: 1.1rem; }
|
||||
.post-content-preview h4,
|
||||
.post-content-preview h5,
|
||||
.post-content-preview h6 { font-size: 1rem; }
|
||||
|
||||
.post-content-preview p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post-content-preview a {
|
||||
color: var(--accent-primary, #38bdf8);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--accent-primary, #38bdf8);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.post-content-preview a:hover {
|
||||
color: var(--accent-secondary, #06b6d4);
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.post-content-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.post-content-preview code {
|
||||
font-family: var(--font-mono, monospace);
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.post-content-preview pre {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid var(--border-primary, rgba(56, 189, 248, 0.1));
|
||||
}
|
||||
|
||||
.post-content-preview pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 0.85em;
|
||||
color: var(--text-primary, #e2e8f0);
|
||||
}
|
||||
|
||||
.post-content-preview blockquote {
|
||||
border-left: 3px solid var(--accent-primary, #38bdf8);
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post-content-preview ul,
|
||||
.post-content-preview ol {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post-content-preview li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Tag Posts Styling */
|
||||
.tag-posts-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary, #e2e8f0);
|
||||
}
|
||||
|
||||
/* Tag List Styles for tag nodes */
|
||||
.related-posts-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.related-posts-list li {
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.related-posts-list li:hover {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
.related-posts-list a {
|
||||
color: var(--accent-primary, #38bdf8);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.related-posts-list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.related-posts-category {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.info-link {
|
||||
display: block;
|
||||
background: linear-gradient(90deg, var(--accent-primary, #38bdf8), var(--accent-secondary, #06b6d4));
|
||||
|
@ -552,6 +820,10 @@ const predefinedColors = {
|
|||
width: 100%;
|
||||
height: 40%; /* Take remaining space */
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
max-height: 150px; /* Smaller height on mobile */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -639,6 +911,7 @@ const predefinedColors = {
|
|||
level: node.level,
|
||||
category: node.category,
|
||||
tags: node.tags || [],
|
||||
content: node.content || '', // Include content for post nodes
|
||||
color: nodeColor,
|
||||
opacity: levelOpacity,
|
||||
size: nodeSize,
|
||||
|
@ -1058,7 +1331,7 @@ const predefinedColors = {
|
|||
cyInstance.elements().difference(node.neighborhood().union(node)).addClass('faded');
|
||||
}
|
||||
|
||||
// Show node info in the panel
|
||||
// Enhanced showNodeInfo function to properly display content previews
|
||||
function showNodeInfo(nodeData) {
|
||||
if (!infoPanel) return;
|
||||
|
||||
|
@ -1069,6 +1342,8 @@ const predefinedColors = {
|
|||
const categoryEl = document.getElementById(`${graphId}-info-category`);
|
||||
const tagsContainerEl = document.getElementById(`${graphId}-info-tags-container`);
|
||||
const tagsEl = document.getElementById(`${graphId}-info-tags`);
|
||||
const contentPreviewContainerEl = document.getElementById(`${graphId}-content-preview-container`);
|
||||
const contentPreviewEl = document.getElementById(`${graphId}-content-preview`);
|
||||
const connectionsEl = document.getElementById(`${graphId}-info-connections`);
|
||||
const linkEl = document.getElementById(`${graphId}-info-link`);
|
||||
|
||||
|
@ -1118,6 +1393,104 @@ const predefinedColors = {
|
|||
}
|
||||
}
|
||||
|
||||
// Show or hide content preview based on node type
|
||||
if (contentPreviewContainerEl && contentPreviewEl) {
|
||||
// First, clear any previous content
|
||||
contentPreviewEl.innerHTML = '';
|
||||
|
||||
if (nodeData.type === 'post') {
|
||||
// Always show content container for post nodes, even if content is missing
|
||||
contentPreviewContainerEl.style.display = 'block';
|
||||
|
||||
if (nodeData.content && nodeData.content.trim() !== '') {
|
||||
// We have content to display
|
||||
contentPreviewEl.innerHTML = `
|
||||
<div class="post-content-preview">
|
||||
${nodeData.content}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// No content available, show placeholder
|
||||
contentPreviewEl.innerHTML = `
|
||||
<div class="content-placeholder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
<p>No content preview available</p>
|
||||
<p class="placeholder-note">Click the link below to read the full post</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else if (nodeData.type === 'tag') {
|
||||
// For tag nodes, show related posts instead
|
||||
contentPreviewContainerEl.style.display = 'block';
|
||||
|
||||
// Find posts that are connected to this tag
|
||||
const tagId = nodeData.id;
|
||||
const currentNode = cyFullscreen.getElementById(tagId);
|
||||
const connectedPosts = currentNode.neighborhood('node[type="post"]');
|
||||
|
||||
if (connectedPosts.length > 0) {
|
||||
contentPreviewEl.innerHTML = `
|
||||
<div class="tag-related-posts">
|
||||
<h4 class="tag-posts-title">Posts with tag "${nodeData.label}":</h4>
|
||||
<ul class="related-posts-list">
|
||||
${Array.from(connectedPosts).map(post => {
|
||||
const postData = post.data();
|
||||
const categoryColor = postData.category && predefinedColors[postData.category]
|
||||
? predefinedColors[postData.category]
|
||||
: '#A0AEC0';
|
||||
|
||||
return `
|
||||
<li>
|
||||
<a href="#" class="related-post-link" data-node-id="${postData.id}">
|
||||
${postData.label}
|
||||
</a>
|
||||
${postData.category ? `
|
||||
<span class="related-posts-category" style="background-color:${categoryColor}33;color:${categoryColor}">
|
||||
${postData.category}
|
||||
</span>
|
||||
` : ''}
|
||||
</li>
|
||||
`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handlers for related posts
|
||||
setTimeout(() => {
|
||||
const relatedPostLinks = contentPreviewEl.querySelectorAll('.related-post-link');
|
||||
relatedPostLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const nodeId = link.getAttribute('data-node-id');
|
||||
const postNode = cyFullscreen.getElementById(nodeId);
|
||||
if (postNode.length > 0) {
|
||||
selectedNode = postNode;
|
||||
highlightNode(postNode, cyFullscreen);
|
||||
showNodeInfo(postNode.data());
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 10); // Small delay to ensure elements are rendered
|
||||
} else {
|
||||
contentPreviewEl.innerHTML = `
|
||||
<div class="content-placeholder">
|
||||
<p>No posts connected to tag "${nodeData.label}" in the current view.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
// Hide for other node types
|
||||
contentPreviewContainerEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Set connections - now with interactive links
|
||||
if (connectionsEl) {
|
||||
connectionsEl.innerHTML = '';
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import MiniGraph from '../../components/MiniGraph.astro';
|
||||
import Header from '../../components/Header.astro'; // Added import
|
||||
import Footer from '../../components/Footer.astro'; // Added import
|
||||
|
||||
// Required getStaticPaths function for dynamic routes
|
||||
export async function getStaticPaths() {
|
||||
|
@ -131,9 +133,37 @@ const combinedRelatedPosts = [
|
|||
|
||||
// Get the Content component for rendering markdown
|
||||
const { Content } = await post.render();
|
||||
|
||||
// Capture rendered content in a string for MiniGraph
|
||||
// Note: This is a simplified approach. A more robust method might be needed
|
||||
// if Content.toString() doesn't reliably produce HTML.
|
||||
let renderedContent = '';
|
||||
try {
|
||||
const contentRender = await post.render();
|
||||
renderedContent = contentRender.Content ? contentRender.Content.toString() : '';
|
||||
} catch (e) {
|
||||
console.error(`Error rendering content for post ${post.slug}:`, e);
|
||||
}
|
||||
|
||||
// Prepare related posts with content (best effort)
|
||||
const relatedPostsWithContent = await Promise.all(
|
||||
combinedRelatedPosts.map(async (relatedPost) => {
|
||||
try {
|
||||
const contentRender = await relatedPost.render();
|
||||
return {
|
||||
...relatedPost,
|
||||
content: contentRender.Content ? contentRender.Content.toString() : ''
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`Error rendering content for related post ${relatedPost.slug}:`, e);
|
||||
return { ...relatedPost, content: '' }; // Fallback
|
||||
}
|
||||
})
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout title={post.data.title} description={post.data.description || ''}>
|
||||
<Header slot="header" /> {/* Added Header */}
|
||||
<article class="container blog-post">
|
||||
<header class="post-header">
|
||||
<h1>{post.data.title}</h1>
|
||||
|
@ -176,7 +206,9 @@ const { Content } = await post.render();
|
|||
tags={post.data.tags || []}
|
||||
category={post.data.category || "Uncategorized"}
|
||||
allPosts={allPosts}
|
||||
content={""} {/* Pass empty string for content */}
|
||||
content={renderedContent} /* Pass rendered HTML */
|
||||
relatedPosts={relatedPostsWithContent} /* Pass related posts with content */
|
||||
width="130%" /* Pass width prop */
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -243,6 +275,7 @@ const { Content } = await post.render();
|
|||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<Footer slot="footer" /> {/* Added Footer */}
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
|
|
Loading…
Reference in New Issue