1550 lines
49 KiB
Plaintext
1550 lines
49 KiB
Plaintext
---
|
|
// MiniGraph.astro - A standalone mini knowledge graph component with fullscreen capability
|
|
// This component is designed to work independently from the blog structure and now includes content previews
|
|
|
|
// Define props interface
|
|
interface Props {
|
|
slug: string; // Current post slug
|
|
title: string; // Current post title
|
|
tags?: string[]; // Current post tags
|
|
category?: string; // Current post category
|
|
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
|
|
const {
|
|
slug,
|
|
title,
|
|
tags = [],
|
|
category = "Uncategorized",
|
|
content = "", // Add content property for current post
|
|
relatedPosts = [],
|
|
allPosts = [],
|
|
width = "100%" // Default width of the component
|
|
} = Astro.props;
|
|
|
|
// Generate unique ID for the graph container
|
|
const graphId = `graph-${Math.random().toString(36).substring(2, 8)}`;
|
|
|
|
// Get all unique tags from related posts (Level 1 tags)
|
|
const relatedPostsTags = relatedPosts
|
|
.flatMap(post => post.data.tags || [])
|
|
.filter(tag => !tags.includes(tag)); // Exclude current post tags to avoid duplicates
|
|
|
|
// Create a set of all Level 1 nodes' tags for filtering Level 2 tags
|
|
const level1TagsSet = new Set([...tags, ...relatedPostsTags]);
|
|
|
|
// Get Level 2 posts: posts related to Level 1 tags (excluding current post and Level 1 posts)
|
|
const level2PostIds = new Set();
|
|
const level2Posts = [];
|
|
|
|
// For each Level 1 tag, find related posts
|
|
tags.forEach(tag => {
|
|
allPosts.forEach(post => {
|
|
// Skip if post is current post or already in related posts
|
|
if (post.slug === slug || relatedPosts.some(rp => rp.slug === post.slug) || level2PostIds.has(post.slug)) {
|
|
return;
|
|
}
|
|
|
|
// If post has the tag, add it to Level 2
|
|
if (post.data.tags?.includes(tag)) {
|
|
level2PostIds.add(post.slug);
|
|
level2Posts.push(post);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Only collect Level 2 tags that are directly linked to Level 1 posts
|
|
// This fixes the issue where unrelated Level 2 tags were being included
|
|
const level2Tags = new Set();
|
|
relatedPosts.forEach(post => {
|
|
(post.data.tags || []).forEach(tag => {
|
|
// Only add if not already in Level 0 or Level 1 tags
|
|
if (!tags.includes(tag) && !relatedPostsTags.includes(tag)) {
|
|
level2Tags.add(tag);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Prepare nodes data with different levels
|
|
const nodes = [
|
|
// Level 0: Current post node
|
|
{
|
|
id: slug,
|
|
label: title,
|
|
type: "post",
|
|
level: 0,
|
|
category: category,
|
|
tags: tags,
|
|
content: content, // Add content for the current post (as HTML string)
|
|
url: `/posts/${slug}/`
|
|
},
|
|
// Level 1: Tag nodes
|
|
...tags.map(tag => ({
|
|
id: `tag-${tag}`,
|
|
label: tag,
|
|
type: "tag",
|
|
level: 1,
|
|
url: `/tag/${tag}/`
|
|
})),
|
|
// Level 1: Related post nodes
|
|
...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}`,
|
|
label: tag,
|
|
type: "tag",
|
|
level: 2,
|
|
url: `/tag/${tag}/`
|
|
})),
|
|
// Level 2: Posts related to tags (Posts connected to Level 1 tags)
|
|
...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 => ({
|
|
id: `tag-${tag}`,
|
|
label: tag.toString(),
|
|
type: "tag",
|
|
level: 2,
|
|
url: `/tag/${tag.toString()}/`
|
|
}))
|
|
];
|
|
|
|
// Create edges connecting nodes
|
|
const edges = [
|
|
// Level 0 to Level 1: Current post to its tags
|
|
...tags.map(tag => ({
|
|
source: slug,
|
|
target: `tag-${tag}`,
|
|
type: "post-tag"
|
|
})),
|
|
// Level 0 to Level 1: Current post to related posts
|
|
...relatedPosts.map(post => ({
|
|
source: slug,
|
|
target: post.slug,
|
|
type: "post-related"
|
|
})),
|
|
// Level 1 to Level 2: Related posts to their tags
|
|
...relatedPosts.flatMap(post =>
|
|
(post.data.tags || []).map(tag => ({
|
|
source: post.slug,
|
|
target: `tag-${tag}`,
|
|
type: "post-tag" // Re-using post-tag type for simplicity
|
|
}))
|
|
),
|
|
// Level 1 to Level 2: Tags to related posts
|
|
...level2Posts.flatMap(post =>
|
|
tags.filter(tag => post.data.tags?.includes(tag)).map(tag => ({
|
|
source: `tag-${tag}`,
|
|
target: post.slug,
|
|
type: "tag-post" // New type for tag -> post connection
|
|
}))
|
|
)
|
|
];
|
|
|
|
|
|
// Prepare graph data object
|
|
const graphData = { nodes, edges };
|
|
|
|
// Define colors for categories
|
|
const predefinedColors = {
|
|
'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
|
|
'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
|
|
'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
|
|
'Storage': '#8B5CF6', 'Obsidian': '#7C3AED', 'Tutorial': '#3B82F6',
|
|
'Uncategorized': '#A0AEC0'
|
|
};
|
|
---
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Fullscreen Toggle Button -->
|
|
<button id={`${graphId}-fullscreen-toggle`} class="fullscreen-toggle" aria-label="Toggle fullscreen">
|
|
<svg id={`${graphId}-fullscreen-enter`} class="fullscreen-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
<polyline points="9 21 3 21 3 15"></polyline>
|
|
<line x1="21" y1="3" x2="14" y2="10"></line>
|
|
<line x1="3" y1="21" x2="10" y2="14"></line>
|
|
</svg>
|
|
<svg id={`${graphId}-fullscreen-exit`} class="fullscreen-icon hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="4 14 10 14 10 20"></polyline>
|
|
<polyline points="20 10 14 10 14 4"></polyline>
|
|
<line x1="14" y1="10" x2="21" y2="3"></line>
|
|
<line x1="3" y1="21" x2="10" y2="14"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
<button id={`${graphId}-exit-fullscreen`} class="exit-fullscreen" aria-label="Exit fullscreen">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="fullscreen-content">
|
|
<!-- Fullscreen graph container -->
|
|
<div id={`${graphId}-fullscreen-graph`} class="fullscreen-graph"></div>
|
|
|
|
<!-- 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>
|
|
<button id={`${graphId}-close-info`} class="close-info" aria-label="Close info panel">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="info-content">
|
|
<div class="info-type">
|
|
<span class="info-label">Type:</span>
|
|
<span id={`${graphId}-info-type`} class="info-value type-value">Post</span>
|
|
</div>
|
|
|
|
<div id={`${graphId}-info-category-container`} class="info-category">
|
|
<span class="info-label">Category:</span>
|
|
<span id={`${graphId}-info-category`} class="info-value category-value">Category</span>
|
|
</div>
|
|
|
|
<div id={`${graphId}-info-tags-container`} class="info-tags">
|
|
<span class="info-label">Tags:</span>
|
|
<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>
|
|
</div>
|
|
|
|
<a href="#" id={`${graphId}-info-link`} class="info-link" target="_blank" rel="noopener noreferrer">View Content</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Minimal CSS -->
|
|
<style>
|
|
/* Base styles for mini graph - 30% larger */
|
|
.knowledge-graph-wrapper {
|
|
width: 100%; /* Width will be set by style attribute */
|
|
position: relative;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.graph-title {
|
|
font-size: 1.1rem;
|
|
margin-bottom: 0.75rem;
|
|
color: var(--text-primary, #e2e8f0);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.mini-graph-container {
|
|
width: 100%;
|
|
aspect-ratio: 1 / 1; /* Create square aspect ratio */
|
|
border-radius: 8px;
|
|
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 */
|
|
.fullscreen-toggle {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 6px;
|
|
background: rgba(30, 41, 59, 0.7);
|
|
border: 1px solid var(--border-primary, rgba(56, 189, 248, 0.3));
|
|
color: var(--text-primary, #e2e8f0);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
z-index: 2;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
opacity: 0; /* Hidden by default, shown on hover */
|
|
}
|
|
|
|
.knowledge-graph-wrapper:hover .fullscreen-toggle {
|
|
opacity: 0.7; /* Show on hover */
|
|
}
|
|
|
|
.fullscreen-toggle:hover {
|
|
opacity: 1;
|
|
background: rgba(30, 41, 59, 0.9);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.fullscreen-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
/* Fullscreen container */
|
|
.fullscreen-container {
|
|
display: none; /* Hidden by default */
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: var(--bg-primary, #0f172a);
|
|
z-index: 9999;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.fullscreen-container.active {
|
|
display: flex;
|
|
}
|
|
|
|
.fullscreen-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.75rem 1.5rem;
|
|
border-bottom: 1px solid var(--border-primary, rgba(56, 189, 248, 0.2));
|
|
background: rgba(15, 23, 42, 0.8);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.fullscreen-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary, #e2e8f0);
|
|
}
|
|
|
|
.exit-fullscreen {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 6px;
|
|
background: rgba(30, 41, 59, 0.7);
|
|
border: 1px solid var(--border-primary, rgba(56, 189, 248, 0.2));
|
|
color: var(--text-primary, #e2e8f0);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.exit-fullscreen:hover {
|
|
background: rgba(30, 41, 59, 0.9);
|
|
}
|
|
|
|
.fullscreen-content {
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.fullscreen-graph {
|
|
flex: 1;
|
|
height: 100%;
|
|
border-right: 1px solid var(--border-primary, rgba(56, 189, 248, 0.2));
|
|
}
|
|
|
|
/* Info panel */
|
|
.info-panel {
|
|
width: 0; /* Hidden by default */
|
|
height: 100%;
|
|
background: var(--bg-secondary, #1e293b);
|
|
transition: width 0.3s ease;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.info-panel.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 {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid var(--border-primary, rgba(56, 189, 248, 0.1));
|
|
}
|
|
|
|
.info-title {
|
|
font-size: 1.2rem;
|
|
margin: 0;
|
|
color: var(--text-primary, #e2e8f0);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.close-info {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-secondary, #94a3b8);
|
|
cursor: pointer;
|
|
padding: 5px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.close-info:hover {
|
|
color: var(--text-primary, #e2e8f0);
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.info-content {
|
|
padding: 1.5rem;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.info-type, .info-category, .info-tags, .info-connections, .content-preview-container {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.info-label {
|
|
display: block;
|
|
color: var(--text-secondary, #94a3b8);
|
|
font-size: 0.85rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.type-value {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.post-type {
|
|
background-color: rgba(59, 130, 246, 0.15);
|
|
color: #3B82F6;
|
|
}
|
|
|
|
.tag-type {
|
|
background-color: rgba(16, 185, 129, 0.15);
|
|
color: #10B981;
|
|
}
|
|
|
|
.category-value {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.tags-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.tag {
|
|
background: rgba(16, 185, 129, 0.1);
|
|
color: #10B981;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.tag:hover {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.connections-list {
|
|
padding-left: 0;
|
|
list-style: none;
|
|
margin: 0;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.connections-list li {
|
|
color: var(--text-secondary, #94a3b8);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.connections-list a {
|
|
color: var(--accent-primary, #38bdf8);
|
|
text-decoration: none;
|
|
transition: color 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.connections-list a:hover {
|
|
color: var(--accent-secondary, #06b6d4);
|
|
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));
|
|
color: var(--bg-primary, #0f172a);
|
|
font-weight: 500;
|
|
padding: 0.6rem 1.25rem;
|
|
border-radius: 6px;
|
|
text-decoration: none;
|
|
text-align: center;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 10px rgba(6, 182, 212, 0.2);
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.info-link:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 15px rgba(6, 182, 212, 0.3);
|
|
}
|
|
|
|
/* Media queries for mobile responsiveness */
|
|
@media screen and (max-width: 768px) {
|
|
.fullscreen-content {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.fullscreen-graph {
|
|
height: 60%;
|
|
border-right: none;
|
|
border-bottom: 1px solid var(--border-primary, rgba(56, 189, 248, 0.2));
|
|
}
|
|
|
|
.info-panel {
|
|
width: 100%;
|
|
height: 0; /* Start hidden */
|
|
}
|
|
|
|
.info-panel.active {
|
|
width: 100%;
|
|
height: 40%; /* Take remaining space */
|
|
}
|
|
|
|
.content-preview {
|
|
max-height: 150px; /* Smaller height on mobile */
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<!-- Standalone initialization script -->
|
|
<script define:vars={{ graphId, graphData, predefinedColors }}>
|
|
// Wait for page to fully load
|
|
window.addEventListener('load', function() {
|
|
// Retry initialization multiple times in case Cytoscape or the DOM isn't ready yet
|
|
let retries = 0;
|
|
const maxRetries = 5;
|
|
const retryInterval = 500; // ms
|
|
|
|
function initGraph() {
|
|
// Ensure Cytoscape is loaded
|
|
if (typeof cytoscape === 'undefined') {
|
|
console.warn(`[MiniGraph] Cytoscape not loaded, retry ${retries+1}/${maxRetries}...`);
|
|
if (retries < maxRetries) {
|
|
retries++;
|
|
setTimeout(initGraph, retryInterval);
|
|
} else {
|
|
console.error("[MiniGraph] Cytoscape library not available after multiple attempts.");
|
|
const container = document.getElementById(graphId);
|
|
if (container) {
|
|
container.innerHTML = '<div style="padding:10px;color:#a0aec0;text-align:center;">Graph library not loaded</div>';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Verify container exists
|
|
const container = document.getElementById(graphId);
|
|
if (!container) {
|
|
console.error(`[MiniGraph] Container #${graphId} not found.`);
|
|
return;
|
|
}
|
|
|
|
// Fullscreen elements
|
|
const fullscreenToggle = document.getElementById(`${graphId}-fullscreen-toggle`);
|
|
const fullscreenContainer = document.getElementById(`${graphId}-fullscreen`);
|
|
const fullscreenGraph = document.getElementById(`${graphId}-fullscreen-graph`);
|
|
const exitFullscreenBtn = document.getElementById(`${graphId}-exit-fullscreen`);
|
|
const fullscreenEnterIcon = document.getElementById(`${graphId}-fullscreen-enter`);
|
|
const fullscreenExitIcon = document.getElementById(`${graphId}-fullscreen-exit`);
|
|
const infoPanel = document.getElementById(`${graphId}-info-panel`);
|
|
const closeInfoBtn = document.getElementById(`${graphId}-close-info`);
|
|
|
|
// State variables
|
|
let isFullscreen = false;
|
|
let cy = null; // Mini graph instance
|
|
let cyFullscreen = null; // Fullscreen graph instance
|
|
let originalStyles = {}; // To restore page layout after exiting fullscreen
|
|
let selectedNode = null; // Currently selected node for fullscreen interactions
|
|
|
|
try {
|
|
// Check if we have any nodes
|
|
if (graphData.nodes.length === 0) {
|
|
container.innerHTML = '<div style="padding:10px;color:#a0aec0;text-align:center;">No connections</div>';
|
|
return;
|
|
}
|
|
|
|
// Generate node colors based on type and category
|
|
const nodeElements = graphData.nodes.map(node => {
|
|
let nodeColor = '#3B82F6'; // Default blue for posts
|
|
|
|
if (node.type === 'tag') {
|
|
nodeColor = '#10B981'; // Green for tags
|
|
} else if (node.type === 'post' && node.category) {
|
|
// Use category color for posts if available
|
|
nodeColor = predefinedColors[node.category] || '#3B82F6';
|
|
}
|
|
|
|
// Adjust opacity based on level
|
|
const levelOpacity = node.level === 0 ? 1 :
|
|
node.level === 1 ? 0.85 : 0.7;
|
|
|
|
// Calculate node size based on level
|
|
const nodeSize = node.level === 0 ? 30 :
|
|
node.level === 1 ? 22 : 16;
|
|
|
|
return {
|
|
data: {
|
|
id: node.id,
|
|
label: node.label,
|
|
type: node.type,
|
|
level: node.level,
|
|
category: node.category,
|
|
tags: node.tags || [],
|
|
content: node.content || '', // Include content for post nodes
|
|
color: nodeColor,
|
|
opacity: levelOpacity,
|
|
size: nodeSize,
|
|
url: node.url || '#'
|
|
}
|
|
};
|
|
});
|
|
|
|
// Generate edge elements
|
|
const edgeElements = graphData.edges.map((edge, index) => ({
|
|
data: {
|
|
id: `e${index}`,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
type: edge.type
|
|
}
|
|
}));
|
|
|
|
// Combine nodes and edges
|
|
const elements = [...nodeElements, ...edgeElements];
|
|
|
|
// Define common style array to be used by both graph instances
|
|
const graphStyles = [
|
|
// Base node style
|
|
{
|
|
selector: 'node',
|
|
style: {
|
|
'background-color': 'data(color)',
|
|
'label': 'data(label)',
|
|
'width': 'data(size)',
|
|
'height': 'data(size)',
|
|
'font-size': '8px',
|
|
'color': '#E2E8F0',
|
|
'text-valign': 'bottom',
|
|
'text-halign': 'center',
|
|
'text-margin-y': 5,
|
|
'text-wrap': 'ellipsis',
|
|
'text-max-width': '60px',
|
|
'text-background-opacity': 0.7,
|
|
'text-background-color': '#0F1219',
|
|
'text-background-padding': '2px',
|
|
'text-background-shape': 'roundrectangle',
|
|
'border-width': 2,
|
|
'border-color': '#0F1219',
|
|
'border-opacity': 0.8,
|
|
'opacity': 'data(opacity)'
|
|
}
|
|
},
|
|
// Post node styles
|
|
{
|
|
selector: 'node[type="post"]',
|
|
style: {
|
|
'shape': 'ellipse',
|
|
'text-max-width': '80px'
|
|
}
|
|
},
|
|
// Current post node (Level 0)
|
|
{
|
|
selector: 'node[type="post"][level=0]',
|
|
style: {
|
|
'border-width': 3,
|
|
'border-color': '#38BDF8'
|
|
}
|
|
},
|
|
// Tag node styles
|
|
{
|
|
selector: 'node[type="tag"]',
|
|
style: {
|
|
'shape': 'diamond',
|
|
'width': 'mapData(level, 0, 2, 25, 15)',
|
|
'height': 'mapData(level, 0, 2, 25, 15)'
|
|
}
|
|
},
|
|
// Edge styles
|
|
{
|
|
selector: 'edge',
|
|
style: {
|
|
'width': 1.5,
|
|
'line-color': 'rgba(226, 232, 240, 0.2)',
|
|
'curve-style': 'bezier',
|
|
'opacity': 0.6
|
|
}
|
|
},
|
|
// Post-tag edges
|
|
{
|
|
selector: 'edge[type="post-tag"]',
|
|
style: {
|
|
'line-color': 'rgba(16, 185, 129, 0.7)',
|
|
'line-style': 'solid'
|
|
}
|
|
},
|
|
// Related post edges
|
|
{
|
|
selector: 'edge[type="post-related"]',
|
|
style: {
|
|
'line-color': 'rgba(59, 130, 246, 0.7)',
|
|
'line-style': 'dashed'
|
|
}
|
|
},
|
|
// Level 2 connections
|
|
{
|
|
selector: 'edge[type="tag-post"]',
|
|
style: {
|
|
'line-color': 'rgba(16, 185, 129, 0.4)',
|
|
'line-style': 'dotted',
|
|
'opacity': 0.5
|
|
}
|
|
},
|
|
// Highlight styles
|
|
{
|
|
selector: '.highlighted',
|
|
style: {
|
|
'background-color': 'data(color)',
|
|
'border-color': '#FFFFFF',
|
|
'border-width': 3,
|
|
'text-background-opacity': 0.9,
|
|
'color': '#FFFFFF',
|
|
'opacity': 1,
|
|
'z-index': 20
|
|
}
|
|
},
|
|
{
|
|
selector: '.faded',
|
|
style: {
|
|
'opacity': 0.2,
|
|
'text-opacity': 0.3
|
|
}
|
|
},
|
|
{
|
|
selector: 'node:selected',
|
|
style: {
|
|
'border-width': 4,
|
|
'border-color': '#FFFFFF'
|
|
}
|
|
}
|
|
];
|
|
|
|
// Initialize mini graph with cose layout
|
|
cy = cytoscape({
|
|
container,
|
|
elements,
|
|
style: graphStyles,
|
|
layout: {
|
|
name: 'cose',
|
|
idealEdgeLength: 50,
|
|
nodeOverlap: 20,
|
|
refresh: 20,
|
|
fit: true,
|
|
padding: 15,
|
|
randomize: false,
|
|
componentSpacing: 70,
|
|
nodeRepulsion: 400000,
|
|
edgeElasticity: 100,
|
|
nestingFactor: 5,
|
|
gravity: 80,
|
|
numIter: 1000,
|
|
initialTemp: 200,
|
|
coolingFactor: 0.95,
|
|
minTemp: 1.0,
|
|
animate: false
|
|
},
|
|
minZoom: 0.5,
|
|
maxZoom: 2,
|
|
wheelSensitivity: 0.2
|
|
});
|
|
|
|
// Fit graph to container
|
|
cy.fit(undefined, 20);
|
|
|
|
// Add event listeners
|
|
cy.on('tap', 'node', function(evt) {
|
|
const node = evt.target;
|
|
highlightNode(node, cy);
|
|
// Navigate on click in mini-graph
|
|
const url = node.data('url');
|
|
if (url && url !== '#') {
|
|
window.location.href = url;
|
|
}
|
|
});
|
|
|
|
// Add fullscreen toggle functionality if the button exists
|
|
if (fullscreenToggle && fullscreenContainer) {
|
|
fullscreenToggle.addEventListener('click', function() {
|
|
enterFullscreenMode();
|
|
});
|
|
}
|
|
|
|
// Exit fullscreen when button is clicked
|
|
if (exitFullscreenBtn) {
|
|
exitFullscreenBtn.addEventListener('click', function() {
|
|
exitFullscreenMode();
|
|
});
|
|
}
|
|
|
|
// Close info panel when close button is clicked
|
|
if (closeInfoBtn && infoPanel) {
|
|
closeInfoBtn.addEventListener('click', function() {
|
|
infoPanel.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
// Toggle fullscreen mode by keyboard ESC
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && isFullscreen) {
|
|
exitFullscreenMode();
|
|
}
|
|
});
|
|
|
|
// Function to enter fullscreen mode
|
|
function enterFullscreenMode() {
|
|
if (isFullscreen) return;
|
|
|
|
isFullscreen = true;
|
|
|
|
// Show fullscreen container
|
|
fullscreenContainer.classList.add('active');
|
|
|
|
// Update fullscreen toggle icons
|
|
fullscreenEnterIcon.classList.add('hidden');
|
|
fullscreenExitIcon.classList.remove('hidden');
|
|
|
|
// Store original styles of important page elements to restore later
|
|
saveOriginalStyles();
|
|
|
|
// Initialize fullscreen graph if not already done
|
|
if (!cyFullscreen && fullscreenGraph) {
|
|
initFullscreenGraph();
|
|
} else if (cyFullscreen) {
|
|
// If already initialized, just resize and fit
|
|
setTimeout(() => {
|
|
cyFullscreen.resize();
|
|
cyFullscreen.fit(undefined, 30);
|
|
cyFullscreen.center();
|
|
}, 50); // Small delay for transition
|
|
}
|
|
|
|
// Prevent body scroll
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
// Function to exit fullscreen mode
|
|
function exitFullscreenMode() {
|
|
if (!isFullscreen) return;
|
|
|
|
isFullscreen = false;
|
|
|
|
// Hide fullscreen container
|
|
fullscreenContainer.classList.remove('active');
|
|
|
|
// Update fullscreen toggle icons
|
|
fullscreenEnterIcon.classList.remove('hidden');
|
|
fullscreenExitIcon.classList.add('hidden');
|
|
|
|
// Close info panel
|
|
if (infoPanel) {
|
|
infoPanel.classList.remove('active');
|
|
}
|
|
|
|
// Restore original page layout
|
|
restoreOriginalStyles();
|
|
|
|
// Allow body scroll again
|
|
document.body.style.overflow = '';
|
|
|
|
// Resize mini graph after exit
|
|
setTimeout(() => {
|
|
if (cy) {
|
|
cy.resize();
|
|
cy.fit(undefined, 20);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
// Save original styles of page elements before entering fullscreen
|
|
function saveOriginalStyles() {
|
|
originalStyles = {
|
|
bodyOverflow: document.body.style.overflow,
|
|
// Add other elements that need to be restored later
|
|
};
|
|
|
|
// Check for sidebar, content containers, etc. and save their styles
|
|
const sidebar = document.querySelector('.post-sidebar'); // Use specific class
|
|
const mainContent = document.querySelector('.post-main-column'); // Use specific class
|
|
|
|
if (sidebar) {
|
|
originalStyles.sidebar = {
|
|
element: sidebar,
|
|
display: sidebar.style.display,
|
|
visibility: sidebar.style.visibility,
|
|
position: sidebar.style.position,
|
|
zIndex: sidebar.style.zIndex,
|
|
width: sidebar.style.width, // Save width too
|
|
flex: sidebar.style.flex,
|
|
gridColumn: sidebar.style.gridColumn
|
|
};
|
|
}
|
|
|
|
if (mainContent) {
|
|
originalStyles.mainContent = {
|
|
element: mainContent,
|
|
marginLeft: mainContent.style.marginLeft,
|
|
width: mainContent.style.width,
|
|
maxWidth: mainContent.style.maxWidth
|
|
};
|
|
}
|
|
}
|
|
|
|
// Restore original styles when exiting fullscreen
|
|
function restoreOriginalStyles() {
|
|
document.body.style.overflow = originalStyles.bodyOverflow || '';
|
|
|
|
// Restore sidebar if it exists
|
|
if (originalStyles.sidebar && originalStyles.sidebar.element) {
|
|
const sidebar = originalStyles.sidebar.element;
|
|
Object.assign(sidebar.style, originalStyles.sidebar); // Restore all saved styles
|
|
sidebar.style.removeProperty('position'); // Ensure position is reset if it was static
|
|
}
|
|
|
|
// Restore main content if it exists
|
|
if (originalStyles.mainContent && originalStyles.mainContent.element) {
|
|
const mainContent = originalStyles.mainContent.element;
|
|
Object.assign(mainContent.style, originalStyles.mainContent);
|
|
}
|
|
|
|
// Force reflow to ensure styles apply correctly
|
|
setTimeout(() => {
|
|
const contentArea = document.querySelector('.post-content');
|
|
if (contentArea) {
|
|
const display = contentArea.style.display;
|
|
contentArea.style.display = 'none';
|
|
void contentArea.offsetHeight; // Trigger reflow
|
|
contentArea.style.display = display;
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
// Initialize the fullscreen graph
|
|
function initFullscreenGraph() {
|
|
if (!fullscreenGraph) return;
|
|
|
|
// Create a new cytoscape instance for fullscreen with the same data
|
|
cyFullscreen = cytoscape({
|
|
container: fullscreenGraph,
|
|
elements: elements,
|
|
style: graphStyles,
|
|
layout: {
|
|
name: 'cose',
|
|
idealEdgeLength: 80,
|
|
nodeOverlap: 20,
|
|
refresh: 20,
|
|
fit: true,
|
|
padding: 30,
|
|
randomize: false,
|
|
componentSpacing: 100,
|
|
nodeRepulsion: 450000,
|
|
edgeElasticity: 100,
|
|
nestingFactor: 5,
|
|
gravity: 80,
|
|
numIter: 1000,
|
|
initialTemp: 200,
|
|
coolingFactor: 0.95,
|
|
minTemp: 1.0,
|
|
animate: false
|
|
},
|
|
minZoom: 0.2,
|
|
maxZoom: 3,
|
|
wheelSensitivity: 0.3
|
|
});
|
|
|
|
// Fit graph to container
|
|
cyFullscreen.fit(undefined, 30);
|
|
|
|
// Add fullscreen interactions
|
|
addFullscreenInteractions();
|
|
}
|
|
|
|
// Add interactions for fullscreen mode
|
|
function addFullscreenInteractions() {
|
|
// Click on node to show info
|
|
cyFullscreen.on('tap', 'node', function(evt) {
|
|
const node = evt.target;
|
|
selectedNode = node; // Store the selected node
|
|
const nodeData = node.data();
|
|
|
|
// Highlight the selected node
|
|
highlightNode(node, cyFullscreen);
|
|
|
|
// Show info panel
|
|
showNodeInfo(nodeData);
|
|
});
|
|
|
|
// Click on background to reset
|
|
cyFullscreen.on('tap', function(evt) {
|
|
if (evt.target === cyFullscreen) {
|
|
// Remove highlights
|
|
cyFullscreen.elements().removeClass('highlighted faded');
|
|
selectedNode = null;
|
|
|
|
// Hide info panel
|
|
if (infoPanel) {
|
|
infoPanel.classList.remove('active');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Highlight a node and its connections
|
|
function highlightNode(node, cyInstance) {
|
|
// Remove previous highlights
|
|
cyInstance.elements().removeClass('highlighted faded');
|
|
|
|
// Highlight selected node and its neighborhood
|
|
node.addClass('highlighted');
|
|
node.neighborhood().addClass('highlighted');
|
|
|
|
// Fade the rest
|
|
cyInstance.elements().difference(node.neighborhood().union(node)).addClass('faded');
|
|
}
|
|
|
|
// Enhanced showNodeInfo function to properly display content previews
|
|
function showNodeInfo(nodeData) {
|
|
if (!infoPanel) return;
|
|
|
|
// Get elements inside the panel
|
|
const infoTitleEl = document.getElementById(`${graphId}-info-title`);
|
|
const infoTypeEl = document.getElementById(`${graphId}-info-type`);
|
|
const categoryContainerEl = document.getElementById(`${graphId}-info-category-container`);
|
|
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`);
|
|
|
|
// Set node title
|
|
if (infoTitleEl) infoTitleEl.textContent = nodeData.label;
|
|
|
|
// Set node type
|
|
if (infoTypeEl) {
|
|
infoTypeEl.textContent = nodeData.type.charAt(0).toUpperCase() + nodeData.type.slice(1);
|
|
infoTypeEl.className = `info-value type-value ${nodeData.type}-type`;
|
|
}
|
|
|
|
// Set category if it's a post
|
|
if (categoryContainerEl && categoryEl) {
|
|
if (nodeData.type === 'post' && nodeData.category) {
|
|
categoryContainerEl.style.display = 'block';
|
|
categoryEl.textContent = nodeData.category;
|
|
const catColor = predefinedColors[nodeData.category] || '#A0AEC0';
|
|
categoryEl.style.backgroundColor = `${catColor}33`;
|
|
categoryEl.style.color = catColor;
|
|
} else {
|
|
categoryContainerEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Set tags
|
|
if (tagsContainerEl && tagsEl) {
|
|
if (nodeData.tags && nodeData.tags.length > 0) {
|
|
tagsContainerEl.style.display = 'block';
|
|
tagsEl.innerHTML = '';
|
|
nodeData.tags.forEach(tag => {
|
|
const tagEl = document.createElement('span');
|
|
tagEl.className = 'tag';
|
|
tagEl.textContent = tag;
|
|
tagEl.addEventListener('click', () => {
|
|
const tagNode = cyFullscreen.getElementById(`tag-${tag}`);
|
|
if (tagNode.length > 0) {
|
|
selectedNode = tagNode; // Update selected node
|
|
highlightNode(tagNode, cyFullscreen);
|
|
showNodeInfo(tagNode.data());
|
|
}
|
|
});
|
|
tagsEl.appendChild(tagEl);
|
|
});
|
|
} else {
|
|
tagsContainerEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 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 = '';
|
|
const currentNode = cyFullscreen.getElementById(nodeData.id); // Get current node from fullscreen instance
|
|
const neighbors = currentNode.neighborhood('node');
|
|
|
|
if (neighbors.length > 0) {
|
|
neighbors.forEach(neighbor => {
|
|
const neighborData = neighbor.data();
|
|
const li = document.createElement('li');
|
|
const a = document.createElement('a');
|
|
a.href = '#';
|
|
a.textContent = neighborData.label;
|
|
a.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
selectedNode = neighbor; // Update selected node
|
|
highlightNode(neighbor, cyFullscreen);
|
|
showNodeInfo(neighborData);
|
|
});
|
|
li.appendChild(a);
|
|
connectionsEl.appendChild(li);
|
|
});
|
|
} else {
|
|
const li = document.createElement('li');
|
|
li.textContent = 'No connections';
|
|
connectionsEl.appendChild(li);
|
|
}
|
|
}
|
|
|
|
// Set link based on node type - dynamic text and URL
|
|
if (linkEl) {
|
|
if (nodeData.type === 'post') {
|
|
linkEl.textContent = 'Read Post';
|
|
linkEl.href = nodeData.url || `/posts/${nodeData.id}/`;
|
|
} else if (nodeData.type === 'tag') {
|
|
linkEl.textContent = 'View Tag';
|
|
linkEl.href = nodeData.url || `/tag/${nodeData.label}/`;
|
|
} else {
|
|
linkEl.textContent = 'View Content';
|
|
linkEl.href = nodeData.url || '#';
|
|
}
|
|
}
|
|
|
|
// Show the panel
|
|
infoPanel.classList.add('active');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[MiniGraph] Error initializing graph:', error);
|
|
container.innerHTML = '<div style="padding:10px;color:#a0aec0;text-align:center;">Error loading graph</div>';
|
|
}
|
|
}
|
|
|
|
// Start initialization attempt
|
|
initGraph();
|
|
});
|
|
</script> |